Skip to content

Instantly share code, notes, and snippets.

@tonetheman
Created September 1, 2026 14:14
Show Gist options
  • Select an option

  • Save tonetheman/5be4c39ac1080cd40e4d8841bb160b1a to your computer and use it in GitHub Desktop.

Select an option

Save tonetheman/5be4c39ac1080cd40e4d8841bb160b1a to your computer and use it in GitHub Desktop.
dump chrome passwords on OSX
"""
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