pythongoogle-chromeencryptioncookiesdpapi

Decrypt & Re-Encrypt Chrome cookies


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:

  1. Decrypt AES key from Local State in C:\Users\[username]\AppData\Local\Google\Chrome\User Data\Local State using DPAPI
  2. Use decrypted key to decrypt Cookie DB in C:\Users\[username]\AppData\Local\Google\Chrome\User Data\Default\Network\Cookies
  3. Copy the decrypted Cookie DB to another computer
  4. Generate random AES key/nonce and encrypt the plaintext Cookie DB transferred on the other computer. Substitute original Cookies DB on the other computer.
  5. Encrypt AES key using DPAPI and substitute associated entry in Local State on the other computer.

And I have the following 2 Python files to do things described above:

encrypt.py:

from win32.win32crypt import CryptProtectData
import base64
import sqlite3
import os
from Cryptodome.Cipher.AES import new, MODE_GCM # pip install pycryptodomex
import decrypt
import json

def 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_base64

def 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 CryptUnprotectData
import base64
import sqlite3
import os
from Cryptodome.Cipher.AES import new, MODE_GCM # pip install pycryptodomex
import sys
import json


def 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_res

def 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:

  1. Using existing local AES key on the other computer
  2. Generate new random nonce (nonce = os.urandom(12))
  3. Change encrypt to encrypt_and_digest, and decrypt to decrypt_and_verify
  4. Store new verification tag returned by encrypt_and_digest & nonce in encrypted_cookie

... and here are the new functions: encrypt.py:

from win32.win32crypt import CryptProtectData
import base64
import sqlite3
import os
from Cryptodome.Cipher.AES import new, MODE_GCM # pip install pycryptodomex
import decrypt
import json
from os.path import expandvars

def 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_base64

def 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 usage
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)
    # 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 CryptUnprotectData
import base64
import sqlite3
import os
from Cryptodome.Cipher.AES import new, MODE_GCM # pip install pycryptodomex
import sys
import json
import encrypt

def 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_res

def 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] format
def bytes_to_hex(byte_data):
    return f"b'{''.join(f'\\x{byte:02x}' for byte in byte_data)}'"

# Example usage
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 = 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)}")

Solution

  • As suggested by Topaco in the comments, and according to my own attempts I modified my functions in the following ways:

    1. Using existing local AES key on the other computer
    2. Generate new random nonce (nonce = os.urandom(12))
    3. Change encrypt to encrypt_and_digest, and decrypt to decrypt_and_verify
    4. Store new verification tag returned by encrypt_and_digest & nonce in encrypted_cookie
    5. Substitute double quotes by single quotes in SQL query ... and here are the new functions:

    encrypt.py:

    import argparse
    import json
    import os
    import base64
    import sqlite3
    from os.path import expandvars
    
    from Cryptodome.Cipher.AES import new, MODE_GCM # pip install pycryptodomex
    from win32.win32crypt import CryptProtectData # pip install pywin32
    
    import decrypt
    
    
    def 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_base64
    
    
    def 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
            if decrypted_value is None or len(decrypted_value) == 0:
                continue
            
            nonce = os.urandom(12)
            aes_cipher = new(key=key, mode=MODE_GCM, nonce=nonce)
            encrypted_value, verification_tag = aes_cipher.encrypt_and_digest(decrypted_value)
    
            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)
    
        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
    
    
    if __name__ == "__main__":
        # Arg: cookies_db
        parser = argparse.ArgumentParser()
        parser.add_argument("--cookies", help="Name of the cookies database file", default="Cookies")
        args = parser.parse_args()
    
        cookies_db = os.path.join(os.getcwd(), args.cookies)
        key = get_local_state_key()
        encrypt_cookies(cookies_db, key)
    

    decrypt.py:

    import argparse
    import base64
    import os
    import sqlite3
    import sys
    
    from Cryptodome.Cipher.AES import new, MODE_GCM # pip install pycryptodomex
    from win32.win32crypt import CryptUnprotectData # pip install pywin32
    
    import encrypt
    
    
    def 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_res
    
    
    def 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
            if encrypted_value is None or len(encrypted_value) == 0:
                continue
    
            verification_tag = encrypted_value[-16:]
            aes_cipher = new(key=key, mode=MODE_GCM, nonce=encrypted_value[3:15])
            decrypted_value = aes_cipher.decrypt_and_verify(ciphertext=encrypted_value[15: -16], received_mac_tag=verification_tag)
            # if cookie_name == "BITBUCKETSESSIONID":
            #     print(f"Decrypted cookie (bitbucket):\n  {decrypted_value.decode()}")
            #     print(f"Verification tag (bitbucket):\n  {bytes_to_hex(verification_tag)}")
            #     print(f"Nonce (bitbucket):\n  {bytes_to_hex(nonce)}")
            query = f"UPDATE cookies SET encrypted_value = ? WHERE name = '{cookie_name}'"
            params = [decrypted_value]
            cursor.execute(query, params)
    
        conn.commit()
        conn.close()
    
    
    if __name__ == "__main__":
        try:
            decrypted_key = encrypt.get_local_state_key()
        except Exception as e:
            print("Decryption failed:", str(e))
            sys.exit(1)
        
        # Arg: cookies_db
        parser = argparse.ArgumentParser()
        parser.add_argument("--cookies", help="Name of the cookies database file", default="Cookies")
        args = parser.parse_args()
        cookies_db = os.path.join(os.getcwd(), args.cookies)
    
        decrypt_cookies(cookies_db, decrypted_key)
    

    And now these functions can be used to migrate cookies between computers without error.