diff --git a/libs/chromium/chromium/helpers.py b/libs/chromium/chromium/helpers.py index 451ba4d..69a0ff8 100644 --- a/libs/chromium/chromium/helpers.py +++ b/libs/chromium/chromium/helpers.py @@ -367,7 +367,10 @@ def try_decrypt_with_all_keys( return None, None -def parse_abe_blob(abe_data: bytes) -> dict | None: +def byte_xor(ba1, ba2): + return bytes([_a ^ _b for _a, _b in zip(ba1, ba2)]) + +def parse_abe_blob(abe_data: bytes, chromekey: bytes | None = None) -> dict | None: """Parse ABE (App-Bound Encryption) blob data. Args: @@ -392,10 +395,17 @@ def parse_abe_blob(abe_data: bytes) -> dict | None: abe_parsed["tag"] = content[12 + 32 : 12 + 32 + 16] else: # Version 3 # Version|encAES|IV|ciphertext|tag, 1|32|12|32|16 bytes + # adapted from: + # https://github.com/KingOfTheNOPs/cookie-monster/blob/4ec4b3555682ac1e71a6428a4b0f45b2cf1fd8f7/decrypt.py + # https://github.com/tijldeneut/diana/blob/b9473b5004ecf1d7bdd5852232b5cd06a5378e5e/diana-browserdec.py abe_parsed["encrAES"] = content[:32] abe_parsed["iv"] = content[32 : 32 + 12] abe_parsed["cipherdata"] = content[32 + 12 : 32 + 12 + 32] abe_parsed["tag"] = content[32 + 12 + 32 : 32 + 12 + 32 + 16] + + if chromekey: + xor_key = bytes.fromhex("CCF8A1CEC56605B8517552BA1A2D061C03A29E90274FB2FCF59BA4B75C392390") + abe_parsed["xored_aes_key"] = byte_xor(chromekey, xor_key) return abe_parsed except Exception as e: logger.warning("Failed to parse ABE blob", error=str(e)) @@ -424,8 +434,11 @@ def derive_abe_key(abe_data: dict) -> bytes | None: nonce=abe_data["iv"], ) elif abe_data["version"] == 3: - # TODO: Version 3 requires CNG decryption of encrypted AES key - logger.warning("ABE version 3 not yet supported - requires CNG decryption") + if abe_data["xored_aes_key"]: + cipher = AES.new(abe_data["xored_aes_key"], AES.MODE_GCM, nonce=abe_data["iv"]) + else: + # Version 3 requires CNG decryption of encrypted AES key + logger.warning("xored_aes_key not present, ABE version 3 requires CNG decrypted (or memory-extracted) 'Google Chromekey1'") return None else: logger.warning("Unknown ABE version", version=abe_data["version"]) diff --git a/libs/chromium/chromium/local_state.py b/libs/chromium/chromium/local_state.py index 00b08e1..830ad7f 100644 --- a/libs/chromium/chromium/local_state.py +++ b/libs/chromium/chromium/local_state.py @@ -119,6 +119,29 @@ def _add_user_masterkey_link(file_enriched, username: str | None, masterkey_guid add_file_linking(file_enriched.source, file_enriched.path, masterkey_path, "windows:user_masterkey") +def _get_chromekey_from_source(source: str, pg_conn) -> bytes | None: + """Get decrypted Chrome key from chrome_keys table by source. + + Args: + source: Source value to match + pg_conn: PostgreSQL connection + + Returns: + Decrypted key bytes if found and decrypted, None otherwise + """ + try: + with pg_conn.cursor() as cur: + cur.execute( + "SELECT key_bytes_dec FROM chromium.chrome_keys WHERE source = %s AND key_is_decrypted = TRUE", + (source,), + ) + result = cur.fetchone() + return result[0] if result else None + except Exception as e: + logger.warning("Failed to lookup chrome key from source", error=str(e)) + return None + + async def _insert_state_keys( file_enriched, username: str | None, @@ -135,6 +158,9 @@ async def _insert_state_keys( # Extract os_crypt section os_crypt = data.get("os_crypt", {}) + # Get Chrome key from chrome_keys table if available + chromekey = _get_chromekey_from_source(file_enriched.source, pg_conn) + key_bytes_dec = b"" key_is_decrypted = False app_bound_key_dec_inter = b"" @@ -220,7 +246,7 @@ async def _insert_state_keys( abe_blob_bytes = await dpapi_manager.decrypt_blob(user_blob) if abe_blob_bytes: # Parse and derive the final ABE key - abe_parsed = parse_abe_blob(abe_blob_bytes) + abe_parsed = parse_abe_blob(abe_blob_bytes, chromekey) if abe_parsed: app_bound_key_dec = derive_abe_key(abe_parsed) if app_bound_key_dec: