I'm trying to decrypt Chrome's cookie SQLite DB, and move the decrypted cookies to another computer (browser), and re-encrypt the DB, and replicate sessions.
Here is what I plan:
- Decrypt AES key from
Local StateinC:\Users\[username]\AppData\Local\Google\Chrome\User Data\Local Stateusing DPAPI - Use decrypted key to decrypt Cookie DB in
C:\Users\[username]\AppData\Local\Google\Chrome\User Data\Default\Network\Cookies - Copy the decrypted Cookie DB to another computer
- Generate random AES key/nonce and encrypt the plaintext Cookie DB transferred on the other computer. Substitute original Cookies DB on the other computer.
- Encrypt AES key using DPAPI and substitute associated entry in
Local Stateon the other computer.
And I have the following 2 Python files to do things described above:
encrypt.py:
from win32.win32crypt import CryptProtectDataimport base64import sqlite3import osfrom Cryptodome.Cipher.AES import new, MODE_GCM # pip install pycryptodomeximport decryptimport jsondef encrypt_dpapi_blob(decrypted_blob): encrypted_blob = CryptProtectData(decrypted_blob, DataDescr="Google Chrome", OptionalEntropy=None, Reserved=None, PromptStruct=None, Flags=0) encrypted_blob = b'DPAPI'+ encrypted_blob encrypted_blob_base64 = base64.b64encode(encrypted_blob) return encrypted_blob_base64def encrypt_cookies(cookies_db, key): sqlite3.enable_callback_tracebacks(True) conn = sqlite3.connect(cookies_db) query = "SELECT name, encrypted_value FROM cookies" cursor = conn.execute(query) query_res = cursor.fetchall() for row in query_res: cookie_name, decrypted_value = row # print(f"Encrypting cookie: {cookie_name}") if decrypted_value is None or len(decrypted_value) == 0: # print("No decrypted value found.") continue aes_cipher = new(key=key, mode=MODE_GCM, nonce=decrypted_value[3:15]) encrypted_value = aes_cipher.encrypt(decrypted_value[15: -16]) # print(f"Encrypted cookie:\n {decrypt.bytes_to_hex(encrypted_value)}\n {encrypted_value}") verification_tag = decrypted_value[-16:] # print(f"Verification tag:\n {decrypt.bytes_to_hex(verification_tag)}\n {verification_tag}") nonce = decrypted_value[3:15] # print(f"Nonce:\n {decrypt.bytes_to_hex(nonce)}\n {nonce}") encrypted_cookie = b'\x76\x31\x30'+\ nonce +\ encrypted_value +\ verification_tag query = f"UPDATE cookies SET encrypted_value = ? WHERE name = \"{cookie_name}\"" params = [encrypted_cookie] cursor.execute(query, params) # print("") conn.commit() conn.close()if __name__ == "__main__": cookies_db = os.path.join(os.getcwd(), "Cookies") # print(f"Decrypted key:\n {decrypt.bytes_to_hex(key)}\n {key}") key = os.urandom(32) encrypt_cookies(cookies_db, key) encrypted_key = encrypt_dpapi_blob(key) print(f"Encrypted key:\n {str(encrypted_key, 'utf-8')}") local_state = json.load(open('Local State')) local_state['os_crypt']['encrypted_key'] = encrypted_key.decode() json.dump(local_state, open('Local State', 'w'))decrypt.py:
from win32.win32crypt import CryptUnprotectDataimport base64import sqlite3import osfrom Cryptodome.Cipher.AES import new, MODE_GCM # pip install pycryptodomeximport sysimport jsondef decrypt_dpapi_blob(encrypted_blob): encrypted_blob = base64.b64decode(encrypted_blob)[5:] # Leading bytes "DPAPI" need to be removed decrypt_res = CryptUnprotectData(encrypted_blob, None, None, None, 0) return decrypt_resdef decrypt_cookies(cookies_db, key): sqlite3.enable_callback_tracebacks(True) conn = sqlite3.connect(cookies_db) query = "SELECT name, encrypted_value FROM cookies" cursor = conn.execute(query) query_res = cursor.fetchall() for row in query_res: cookie_name, encrypted_value = row # print(f"Decrypting cookie: {cookie_name}") if encrypted_value is None or len(encrypted_value) == 0: # print("No encrypted value found.") continue aes_cipher = new(key=key, mode=MODE_GCM, nonce=encrypted_value[3:15]) decrypted_value = aes_cipher.decrypt(encrypted_value[15: -16]) # print(f"Decrypted cookie:\n {bytes_to_hex(decrypted_value)}\n {decrypted_value}") if cookie_name == "BITBUCKETSESSIONID": print(f"Decrypted cookie (bitbucket): {decrypted_value.decode()}") verification_tag = encrypted_value[-16:] # print(f"Verification tag:\n {bytes_to_hex(verification_tag)}\n {verification_tag}") nonce = encrypted_value[3:15] # print(f"Nonce:\n {bytes_to_hex(nonce)}\n {nonce}") decrypted_cookie = b'\x76\x31\x30'+\ nonce +\ decrypted_value +\ verification_tag query = f"UPDATE cookies SET encrypted_value = ? WHERE name = \"{cookie_name}\"" params = [decrypted_cookie] cursor.execute(query, params) # print("") conn.commit() conn.close()def bytes_to_hex(byte_data): return f"b'{''.join(f'\\x{byte:02x}' for byte in byte_data)}'"if __name__ == "__main__": encrypted_key_base64 = json.load(open('Local State'))['os_crypt']['encrypted_key'] # print(f"Encrypted key:\n {encrypted_key_base64}") try: decrypted_key = decrypt_dpapi_blob(encrypted_key_base64)[1] print(f"Decrypted key:\n {bytes_to_hex(decrypted_key)}") except Exception as e: print("Decryption failed:", str(e)) sys.exit(1) # get current working directory path cookies_db = os.path.join(os.getcwd(), "Cookies") decrypt_cookies(cookies_db, decrypted_key) # print(f"Decrypted key:\n {bytes_to_hex(decrypted_key)}")With these functions, I can get the plaintext cookie and verified that, if I manually copy the cookie text in Chrome, I can get the target session.
if cookie_name == "BITBUCKETSESSIONID": print(f"Decrypted cookie (bitbucket): {decrypted_value.decode()}")Decrypting & Encrypting back and forth work without problem as well.
However, if I substitute the modified Cookies file and Local State file, Chrome will not read the migrated cookie.
May I know what is wrong here?
As suggested by Topaco in the comments, I modified my functions in the following ways:
- Using existing local AES key on the other computer
- Generate new random nonce (
nonce = os.urandom(12)) - Change
encrypttoencrypt_and_digest, anddecrypttodecrypt_and_verify - Store new verification tag returned by
encrypt_and_digest& nonce inencrypted_cookie
... and here are the new functions:encrypt.py:
from win32.win32crypt import CryptProtectDataimport base64import sqlite3import osfrom Cryptodome.Cipher.AES import new, MODE_GCM # pip install pycryptodomeximport decryptimport jsonfrom os.path import expandvarsdef encrypt_dpapi_blob(decrypted_blob): encrypted_blob = CryptProtectData(decrypted_blob, DataDescr="Google Chrome", OptionalEntropy=None, Reserved=None, PromptStruct=None, Flags=0) encrypted_blob = b'DPAPI'+ encrypted_blob encrypted_blob_base64 = base64.b64encode(encrypted_blob) return encrypted_blob_base64def encrypt_cookies(cookies_db, key): sqlite3.enable_callback_tracebacks(True) conn = sqlite3.connect(cookies_db) query = "SELECT name, encrypted_value FROM cookies" cursor = conn.execute(query) query_res = cursor.fetchall() for row in query_res: cookie_name, decrypted_value = row # print(f"Encrypting cookie: {cookie_name}") if decrypted_value is None or len(decrypted_value) == 0: # print("No decrypted value found.") continue nonce = os.urandom(12) aes_cipher = new(key=key, mode=MODE_GCM, nonce=nonce) # encrypted_value = aes_cipher.encrypt(decrypted_value[15: -16]) # wrong encrypted_value, verification_tag = aes_cipher.encrypt_and_digest(decrypted_value[15: -16]) # print(f"Encrypted cookie:\n {decrypt.bytes_to_hex(encrypted_value)}\n {encrypted_value}") # verification_tag = decrypted_value[-16:] # wrong # print(f"Verification tag:\n {decrypt.bytes_to_hex(verification_tag)}\n {verification_tag}") # nonce = decrypted_value[3:15] # wrong # print(f"Nonce:\n {decrypt.bytes_to_hex(nonce)}\n {nonce}") encrypted_cookie = b'\x76\x31\x30'+\ nonce +\ encrypted_value +\ verification_tag query = f"UPDATE cookies SET encrypted_value = ? WHERE name = \"{cookie_name}\"" params = [encrypted_cookie] cursor.execute(query, params) # print("") conn.commit() conn.close()def get_local_state_key(): local_state = json.load(open(expandvars('%LOCALAPPDATA%/Google/Chrome/User Data/Local State'))) encrypted_key = local_state['os_crypt']['encrypted_key'] decrypted_key = decrypt.decrypt_dpapi_blob(encrypted_key)[1] return decrypted_key# Example usageif __name__ == "__main__": cookies_db = os.path.join(os.getcwd(), "Cookies") # print(f"Decrypted key:\n {decrypt.bytes_to_hex(key)}\n {key}") # key = os.urandom(32) # Using existing key key = get_local_state_key() encrypt_cookies(cookies_db, key) # encrypted_key = encrypt_dpapi_blob(key) # print(f"Encrypted key:\n {str(encrypted_key, 'utf-8')}") # wrong # local_state = json.load(open('Local State')) # local_state['os_crypt']['encrypted_key'] = encrypted_key.decode() # json.dump(local_state, open('Local State', 'w'))decrypt.py:
from win32.win32crypt import CryptUnprotectDataimport base64import sqlite3import osfrom Cryptodome.Cipher.AES import new, MODE_GCM # pip install pycryptodomeximport sysimport jsonimport encryptdef decrypt_dpapi_blob(encrypted_blob): encrypted_blob = base64.b64decode(encrypted_blob)[5:] # Leading bytes "DPAPI" need to be removed decrypt_res = CryptUnprotectData(encrypted_blob, None, None, None, 0) return decrypt_resdef decrypt_cookies(cookies_db, key): sqlite3.enable_callback_tracebacks(True) conn = sqlite3.connect(cookies_db) query = "SELECT name, encrypted_value FROM cookies" cursor = conn.execute(query) query_res = cursor.fetchall() for row in query_res: cookie_name, encrypted_value = row # print(f"Decrypting cookie: {cookie_name}") if encrypted_value is None or len(encrypted_value) == 0: # print("No encrypted value found.") continue aes_cipher = new(key=key, mode=MODE_GCM, nonce=encrypted_value[3:15]) # decrypted_value = aes_cipher.decrypt(encrypted_value[15: -16]) # wrong decrypted_value = aes_cipher.decrypt_and_verify(encrypted_value[15: -16], encrypted_value[-16:]) # print(f"Decrypted cookie:\n {bytes_to_hex(decrypted_value)}\n {decrypted_value}") if cookie_name == "BITBUCKETSESSIONID": print(f"Decrypted cookie (bitbucket): {decrypted_value.decode()}") verification_tag = encrypted_value[-16:] # print(f"Verification tag:\n {bytes_to_hex(verification_tag)}\n {verification_tag}") nonce = encrypted_value[3:15] # print(f"Nonce:\n {bytes_to_hex(nonce)}\n {nonce}") decrypted_cookie = b'\x76\x31\x30'+\ nonce +\ decrypted_value +\ verification_tag query = f"UPDATE cookies SET encrypted_value = ? WHERE name = \"{cookie_name}\"" params = [decrypted_cookie] cursor.execute(query, params) # print("") conn.commit() conn.close()# Custom function to display all bytes in the \x[something] formatdef bytes_to_hex(byte_data): return f"b'{''.join(f'\\x{byte:02x}' for byte in byte_data)}'"# Example usageif __name__ == "__main__": # encrypted_key_base64 = json.load(open('Local State'))['os_crypt']['encrypted_key'] # print(f"Encrypted key:\n {encrypted_key_base64}") try: decrypted_key = encrypt.get_local_state_key() print(f"Decrypted key:\n {bytes_to_hex(decrypted_key)}") except Exception as e: print("Decryption failed:", str(e)) sys.exit(1) # get current working directory path cookies_db = os.path.join(os.getcwd(), "Cookies") decrypt_cookies(cookies_db, decrypted_key) # print(f"Decrypted key:\n {bytes_to_hex(decrypted_key)}")