Created
September 1, 2026 14:14
-
-
Save tonetheman/5be4c39ac1080cd40e4d8841bb160b1a to your computer and use it in GitHub Desktop.
dump chrome passwords on OSX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Use this to get your stored passwords that Chrome knows | |
| You still need to know your keychain passwords to run this script | |
| The subprocess command will trigger that password request | |
| this will not work on windows | |
| python3 -m venv ./venv | |
| source ./venv/bin/activate | |
| pip install crytography | |
| """ | |
| import os | |
| import sqlite3 | |
| import shutil | |
| import hashlib | |
| import subprocess | |
| from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | |
| from cryptography.hazmat.backends import default_backend | |
| def get_macos_safe_storage_key(): | |
| """Queries the macOS Keychain securely for Chrome's master storage key.""" | |
| try: | |
| # Calls the native macOS security tool to pull the Chrome Safe Storage password | |
| # NOTE: This will trigger a pop-up prompt asking for your macOS account password. | |
| output = subprocess.check_output( | |
| "security find-generic-password -w -s 'Chrome Safe Storage'", | |
| shell=True, | |
| stderr=subprocess.DEVNULL | |
| ) | |
| return output.strip() | |
| except subprocess.CalledProcessError: | |
| print("[!] Error: Could not retrieve Chrome Safe Storage key. Did you deny the prompt?") | |
| return None | |
| def decrypt_password_macos(encrypted_blob, safe_storage_key): | |
| """Derives the AES key and decrypts the Chrome password blob (AES-128-CBC).""" | |
| if not encrypted_blob or len(encrypted_blob) <= 3: | |
| return "" | |
| try: | |
| # Chrome on Mac adds a 'v10' or 'v11' prefix; strip it to get raw ciphertext | |
| raw_ciphertext = encrypted_blob[3:] | |
| # Chrome derives its 128-bit key via PBKDF2 using hardcoded constants | |
| derived_key = hashlib.pbkdf2_hmac( | |
| 'sha1', | |
| safe_storage_key, | |
| b'saltysalt', | |
| 1003, | |
| dklen=16 | |
| ) | |
| # macOS Chrome historically defaults to a static Initialization Vector (16 spaces) | |
| iv = b' ' * 16 | |
| # Set up AES-128-CBC decryptor | |
| backend = default_backend() | |
| cipher = Cipher(algorithms.AES(derived_key), modes.CBC(iv), backend=backend) | |
| decryptor = cipher.decryptor() | |
| decrypted_padded = decryptor.update(raw_ciphertext) + decryptor.finalize() | |
| # Strip PKCS7 padding to reveal the cleartext password | |
| padding_len = decrypted_padded[-1] | |
| decrypted_text = decrypted_padded[:-padding_len].decode('utf-8', errors='ignore') | |
| return decrypted_text | |
| except Exception as e: | |
| return f"[Decryption Failed: {e}]" | |
| def main(): | |
| # macOS Chrome local database path | |
| # TODO change path here as needed | |
| # I copied the sqlite3 db to a local path | |
| login_data_path = os.path.expanduser( | |
| "./login_data" | |
| # "~/Library/Application Support/Google/Chrome/Default/Login Data" | |
| ) | |
| temp_db_path = "ChromeLoginData_mac_temp.db" | |
| if not os.path.exists(login_data_path): | |
| print(f"[!] Target file not found at {login_data_path}. Is Chrome using a custom Profile path?") | |
| return | |
| # Copy the database to prevent database locking while Chrome is active | |
| shutil.copyfile(login_data_path, temp_db_path) | |
| try: | |
| safe_storage_key = get_macos_safe_storage_key() | |
| if not safe_storage_key: | |
| return | |
| conn = sqlite3.connect(temp_db_path) | |
| cursor = conn.cursor() | |
| # Grab rows from the SQLite logins table | |
| cursor.execute("SELECT origin_url, username_value, password_value FROM logins") | |
| for row in cursor.fetchall(): | |
| url = row[0] | |
| username = row[1] | |
| encrypted_password = row[2] | |
| if username or encrypted_password: | |
| decrypted_password = decrypt_password_macos(encrypted_password, safe_storage_key) | |
| print(f"URL: {url}\nUser: {username}\nPass: {decrypted_password}\n{'-'*40}") | |
| conn.close() | |
| finally: | |
| # Erase the temporary database file copy | |
| if os.path.exists(temp_db_path): | |
| os.remove(temp_db_path) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment