Skip to content

Decryption for new chrome failed #42

@ghost

Description

Here is a new script if you want to add some tweaks
script is working as designed: it detects Chrome's new "v20" encryption format, logs a warning for each unsupported password, and provides summary statistics at the end. However, as of now, there is no public method to decrypt Chrome passwords with the v20 prefix—this is a change in Chrome's security model.

--- C#-Style Output Formatting Utilities (from cBrowseUtilis.cs) ---

def format_password_csharp(p):
return f"Hostname: {p.get('url','')}\nUsername: {p.get('username','')}\nPassword: {p.get('password','')}\n\n"

--- AES-GCM Decrypt Function (crypt2.cs equivalent) ---

def aes_gcm_decrypt(key, iv, aad, ciphertext, tag):
"""Decrypt AES-GCM using key, iv, aad, ciphertext, tag (crypt2.cs logic)."""
try:
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
if aad:
cipher.update(aad)
decrypted = cipher.decrypt_and_verify(ciphertext, tag)
return decrypted
except Exception as e:
logging.error(f"AES-GCM decryption (crypt2.cs) failed: {e}")
return None
import os
import re
import sys
import json
import csv
import sqlite3
import argparse
import logging
import shutil
import base64
import time
import subprocess
import signal
import getpass
import requests
import websocket
from pathlib import Path
from datetime import datetime, timedelta
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed

--- Dependency Check and Setup ---

try:
from tqdm import tqdm
except ImportError:
def tqdm(iterable, *args, **kwargs):
print("Warning: 'tqdm' not found. For a progress bar, run: pip install tqdm", file=sys.stderr)
return iterable

try:
if sys.platform == 'win32':
import win32crypt
try:
from Crypto.Cipher import AES # For pycryptodome or pycrypto
except ImportError:
print("Warning: 'pycryptodome' is required for cookie decryption. Please install it by running: pip install pycryptodome", file=sys.stderr)
AES = None
except ImportError:
if sys.platform == 'win32':
print("Warning: 'pycryptodomex' and 'pywin32' are required on Windows for cookie decryption.", file=sys.stderr)
print("Please install them by running: pip install pycryptodomex pywin32", file=sys.stderr)

--- Configuration ---

EMAIL_REGEX = re.compile(r"(?i)[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}")
MAX_FILE_SIZE = 15 * 1024 * 1024 # 15MB
LOG_FORMAT = '%(asctime)s - %(levelname)s - [%(threadName)s] - %(message)s'
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT, stream=sys.stderr)

--- Browser & System Specific Paths ---

def get_browser_paths():
"""Returns a dictionary of browser user data paths based on the OS."""
home = Path.home()
appdata = Path(os.environ.get('APPDATA', home / 'AppData/Roaming'))
local_appdata = Path(os.environ.get('LOCALAPPDATA', home / 'AppData/Local'))

paths = {
    'chrome': {
        'win32': local_appdata / 'Google/Chrome/User Data',
        'linux': home / '.config/google-chrome',
        'darwin': home / 'Library/Application Support/Google/Chrome'
    },
    'edge': {
        'win32': local_appdata / 'Microsoft/Edge/User Data',
        'linux': home / '.config/microsoft-edge',
        'darwin': home / 'Library/Application Support/Microsoft Edge'
    },
    'firefox': {
        'win32': appdata / 'Mozilla/Firefox/Profiles',
        'linux': home / '.mozilla/firefox',
        'darwin': home / 'Library/Application Support/Firefox/Profiles'
    }
}
return {k: v.get(sys.platform) for k, v in paths.items()}

--- Decryption Functions (Windows Only) ---

def get_master_key(browser_path: Path):
"""Retrieves the AES master key for Chrome/Edge decryption on Windows."""
if sys.platform != 'win32': return None
local_state_path = browser_path / 'Local State'
logging.info(f"Looking for Local State at: {local_state_path}")
if not local_state_path.exists():
logging.error(f"Local State file not found at: {local_state_path}")
return None
try:
with open(local_state_path, 'r', encoding='utf-8') as f:
state = json.load(f)
encrypted_key = state['os_crypt']['encrypted_key']
key = base64.b64decode(encrypted_key)[5:]
master_key = win32crypt.CryptUnprotectData(key, None, None, None, 0)[1]
logging.info("Master key successfully retrieved and decrypted.")
return master_key
except Exception as e:
logging.error(f"Failed to get master key for {browser_path.name}: {e}")
return None

def decrypt_value(value, master_key):
"""Decrypts a value using the master key on Windows."""
if sys.platform != 'win32' or not master_key:
logging.error("Decryption not supported: platform is not win32 or master key is missing.")
return "Decryption not supported"
# Ensure value is bytes
if value is None:
logging.warning("Encrypted value is None.")
return ""
if isinstance(value, memoryview):
value = value.tobytes()
elif not isinstance(value, bytes):
try:
value = bytes(value)
except Exception:
logging.error(f"Encrypted value is not bytes-like: {type(value)}")
return ""
if not value:
logging.warning("Encrypted value is empty.")
return ""
# Log first 16 bytes and length for debugging
logging.debug(f"Decrypting value: type={type(value)}, len={len(value)}, head={value[:16].hex() if isinstance(value, bytes) else str(value)[:16]}")
try:
# Log the prefix for every encrypted value
prefix = value[:3]
logging.info(f"Chrome encrypted value prefix: {prefix!r} (hex: {prefix.hex()}) head={value[:16].hex()} len={len(value)}")
# Chromium AES-GCM: [v10|v11][12 bytes IV][ciphertext][16 bytes tag]
if prefix in (b'v10', b'v11'):
if len(value) < 3+12+16:
logging.error(f"Encrypted value too short for AES-GCM: len={len(value)}, head={value[:16].hex()}")
return ""
iv = value[3:15]
ciphertext_tag = value[15:]
if len(ciphertext_tag) < 16:
logging.error(f"Ciphertext too short for tag split: len={len(ciphertext_tag)}, head={ciphertext_tag[:16].hex()}")
return ""
ciphertext = ciphertext_tag[:-16]
tag = ciphertext_tag[-16:]
decrypted = aes_gcm_decrypt(master_key, iv, None, ciphertext, tag)
if decrypted is not None:
return decrypted.decode('utf-8', errors='replace')
else:
logging.error(f"crypt2.cs AES-GCM decryption failed: iv={iv.hex()}, tag={tag.hex()}, ciphertext_head={ciphertext[:16].hex()}, prefix={prefix}")
return "Could not decrypt"
else:
# Log unknown prefix for debugging
logging.warning(f"Unknown Chrome encrypted value prefix: {prefix!r}, head={value[:16].hex()}, len={len(value)}. Trying DPAPI fallback.")
decrypted = win32crypt.CryptUnprotectData(value, None, None, None, 0)[1].decode('utf-8', errors='replace')
return decrypted
except Exception as e1:
logging.warning(f"AES-GCM/DPAPI decryption failed: {e1}. head={value[:16].hex() if isinstance(value, bytes) else str(value)[:16]}")
try:
decrypted = win32crypt.CryptUnprotectData(value, None, None, None, 0)[1].decode('utf-8', errors='replace')
return decrypted
except Exception as e2:
logging.error(f"DPAPI decryption also failed: {e2}. head={value[:16].hex() if isinstance(value, bytes) else str(value)[:16]}")
return "Could not decrypt"

--- Timestamp & Path Helpers ---

def chrome_time_to_datetime(timestamp):
if timestamp > 0:
try:
return str(datetime(1601, 1, 1) + timedelta(microseconds=timestamp))
except OverflowError:
return "Never"
return "N/A"

def find_sqlite_files(path: Path, pattern: str):
"""Finds all SQLite files matching a pattern in a directory."""
if path and path.exists():
return list(path.glob(pattern))
return []

--- Artifact Extraction Functions ---

def extract_from_db(db_path: Path, query: str, browser_info: dict, decrypt_fn=None, master_key=None):
"""Generic function to extract data from a SQLite database."""
results = []
temp_db = Path(f"./temp_{db_path.name}_{os.getpid()}.db")
try:
shutil.copy2(db_path, temp_db)
conn = sqlite3.connect(f'file:{temp_db}?mode=ro', uri=True)
cursor = conn.cursor()
cursor.execute(query)
for row in cursor.fetchall():
row_dict = {desc[0]: val for desc, val in zip(cursor.description, row)}
if decrypt_fn and 'encrypted_value' in row_dict:
row_dict['value'] = decrypt_fn(row_dict.pop('encrypted_value'), master_key)
row_dict.update(browser_info)
results.append(row_dict)
conn.close()
except PermissionError as e:
logging.error(f"PermissionError: Could not access {db_path.name} for {browser_info['browser']} ({browser_info['profile']}). This file is likely locked by the browser. Please close the browser and try again. Details: {e}")
except sqlite3.Error as e:
logging.warning(f"Could not read from {db_path.name} for {browser_info['browser']} ({browser_info['profile']}): {e}")
finally:
if temp_db.exists():
os.remove(temp_db)
return results

--- Chrome DevTools Protocol Cookie Extraction (for new Chrome) ---

def process_browser_profile(profile_path: Path, browser_name: str, tasks: list, decrypt: bool):
"""Extracts artifacts from a single browser profile."""
profile_name = profile_path.name
logging.info(f"Scanning {browser_name.title()} profile: {profile_name}")
master_key = get_master_key(profile_path.parent) if decrypt else None
results = defaultdict(list)
browser_info = {'browser': browser_name, 'profile': profile_name}

# Password extraction (new feature)
if 'passwords' in tasks:
    login_db = profile_path / 'Login Data'
    if login_db.exists() and browser_name == 'chrome' and profile_name == 'Default':
        logging.info("Extracting Chrome saved passwords from Login Data...")
        query = "SELECT origin_url, username_value, password_value FROM logins"
        temp_db = Path(f"./temp_{login_db.name}_{os.getpid()}.db")
        total_passwords = 0
        unique_sites = set()
        unsupported_count = 0
        try:
            shutil.copy2(login_db, temp_db)
            conn = sqlite3.connect(f'file:{temp_db}?mode=ro', uri=True)
            cursor = conn.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                url, username, encrypted_password = row
                password = None
                # Try decrypting password (if possible)
                if decrypt and encrypted_password:
                    try:
                        # Detect unsupported encryption
                        prefix = encrypted_password[:3]
                        if prefix not in (b'v10', b'v11'):
                            logging.warning(f"Unsupported Chrome encryption format: prefix={prefix!r} hex={prefix.hex()} (only v10/v11 supported). Password will not be decrypted.")
                            password = '[UNSUPPORTED ENCRYPTION]'
                            unsupported_count += 1
                        else:
                            password = decrypt_value(encrypted_password, get_master_key(profile_path.parent))
                    except Exception:
                        password = None
                # Fallback: try plaintext
                if not password and encrypted_password:
                    try:
                        password = encrypted_password.decode('utf-8', errors='ignore')
                    except Exception:
                        password = ''
                results['passwords'].append({
                    'url': url,
                    'username': username,
                    'password': password,
                    'browser': browser_name,
                    'profile': profile_name
                })
                total_passwords += 1
                unique_sites.add(url)
            conn.close()
            logging.info(f"Summary: {total_passwords} passwords found, {len(unique_sites)} unique sites.")
            if unsupported_count > 0:
                logging.warning(f"{unsupported_count} password(s) could not be decrypted due to unsupported encryption format.")
        except Exception as ex:
            logging.error(f"Failed to extract passwords: {ex}")
        finally:
            if temp_db.exists():
                os.remove(temp_db)

if 'history' in tasks:
    history_db = profile_path / 'History'
    if history_db.exists():
        query = "SELECT url, title, visit_count, last_visit_time FROM urls"
        history = extract_from_db(history_db, query, browser_info)
        for h in history: h['last_visit_time'] = chrome_time_to_datetime(h['last_visit_time'])
        results['history'].extend(history)
        
return results

--- Main Application Logic ---

def save_results(data: dict, output_path: Path, output_format: str):
"""Saves all extracted data to a file."""
logging.info(f"Saving results for tasks: {', '.join(data.keys())} to {output_path}")
try:
with output_path.open('w', encoding='utf-8', newline='') as f:
if output_format == 'json':
json.dump(data, f, indent=4)
elif output_format == 'csv':
# For CSV, write each task's data to a separate file
base, _ = os.path.splitext(output_path)
for task, items in data.items():
if not items: continue
task_path = Path(f"{base}_{task}.csv")
with task_path.open('w', encoding='utf-8', newline='') as task_f:
writer = csv.DictWriter(task_f, fieldnames=items[0].keys())
writer.writeheader()
writer.writerows(items)
logging.info(f"Saved {task} data to {task_path}")
else: # TXT format
for task, items in data.items():
f.write(f"--- {task.upper()} ---\n\n")
if not items:
f.write("No items found for this task.\n\n")
continue
for item in items:
for key, val in item.items():
f.write(f"{str(key).title():<18}: {val}\n")
f.write("-" * 40 + "\n")
f.write("\n")
logging.info("Results successfully saved.")
except IOError as e:
logging.critical(f"Fatal: Error writing to output file: {e}")

def main():

parser = argparse.ArgumentParser(
    description='A tool to extract browser passwords.',
    epilog='Example: python %(prog)s --output report.json --format json'
)
parser.add_argument('--output', type=Path, required=True, help='Output file path. For CSV, this is a base name.')
parser.add_argument('--format', choices=['txt', 'json', 'csv'], default='txt', help='Output format for results.')
parser.add_argument('--csharp-output', action='store_true', help='Also write C#-style output files for each artifact type.')
parser.add_argument('--no-decrypt', action='store_true', help='Disable password value decryption.')

args = parser.parse_args()

master_results = defaultdict(list)
browser_tasks = ['passwords']

# Only process Chrome, skip Edge and others
logging.info(f"--- Starting Browser Tasks: passwords ---")
browser_paths = get_browser_paths()
# Only process Chrome
chrome_path = browser_paths.get('chrome')
if chrome_path and chrome_path.exists():
    profile_paths = find_sqlite_files(chrome_path, '*/History')
    profile_dirs = {p.parent for p in profile_paths}
    if not profile_dirs:
        profile_dirs.add(chrome_path / 'Default')

    for profile_dir in profile_dirs:
        if profile_dir.exists():
            profile_results = process_browser_profile(profile_dir, 'chrome', browser_tasks, not args.no_decrypt)
            for task, items in profile_results.items():
                master_results[task].extend(items)

if any(master_results.values()):
    save_results(master_results, args.output, args.format)
    # C#-style output is no longer supported.
else:
    logging.warning("Scan complete. No data was extracted for the specified tasks.")

logging.info("All tasks finished.")

if name == "main":
main()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions