Skip to content

Instantly share code, notes, and snippets.

@RunasSudo
Created November 20, 2025 08:58
Show Gist options
  • Select an option

  • Save RunasSudo/7eb153d0adc0a42a1a43264d273faaeb to your computer and use it in GitHub Desktop.

Select an option

Save RunasSudo/7eb153d0adc0a42a1a43264d273faaeb to your computer and use it in GitHub Desktop.
Decrypts a password-protected Microsoft Access database (using Agile Encryption)
#!/usr/bin/env python3
# Copyright (C) 2025 Lee Yingtong Li
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import base64
from hashlib import sha512
import struct
import sys
import xml.etree.ElementTree as ET
from Crypto.Cipher import AES, ARC4
# Get CLI args
if len(sys.argv) < 4:
print('Decrypts a password-protected Microsoft Access database (using Agile Encryption)')
print()
print('Usage: python decrypt_accdb.py /path/to/input.accdb /path/to/output.accdb password')
input_filename = sys.argv[1]
output_filename = sys.argv[2]
db_password = sys.argv[3]
# Constants, etc.
PAGE_SIZE = 0x1000
class DatabaseFormatError(Exception): pass
class DatabaseIntegrityError(Exception): pass
class DecryptDatabaseError(Exception): pass
with open(input_filename, 'rb') as input_file:
# Read the header page
header_page = input_file.read(PAGE_SIZE)
# Validate the file format
# https://web.archive.org/web/20230730041037/http://jabakobob.net/mdb/first-page.html
# https://github.com/mdbtools/mdbtools/blob/dev/HACKING.md
# Magic number for header page (database definition page)
if header_page[0x0:0x4] != b'\x00\x01\x00\x00':
raise DatabaseFormatError('Unexpected header page magic number - is this a Microsoft Access database?')
# We only support the new ACE format
if header_page[0x4:0x14] != b'Standard ACE DB\0':
if header_page[0x4:0x14] == b'Standard Jet DB\0':
raise NotImplementedError('Jet (MDB) database format is not supported')
raise DatabaseFormatError('Unknown file format ID')
# Decrypt encoding key with RC4 key
# Pre-generate the cipher stream so we can easily reuse for decryption and encryption
header_page_decrypted = bytearray(header_page)
cipher = ARC4.new(b'\xc7\xda\x39\x6b')
rc4_cipher_stream = cipher.decrypt(b'\0' * 0x80)
header_page_decrypted[0x18:0x98] = bytes(a ^ b for a, b in zip(header_page[0x18:0x98], rc4_cipher_stream))
db_encoding_key = header_page_decrypted[0x3e:0x42]
# Read EncryptionInfo stream
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/87020a34-e73f-4139-99bc-bbdf6cf6fa55
(encryption_info_len,) = struct.unpack('<H', header_page[0x299:0x29b])
encryption_info = header_page[0x29b:0x29b+encryption_info_len]
# Read EncryptionVersionInfo struct
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/122d11d9-9aff-47bd-8ae8-2996bdda3bdd
(v_major, v_minor) = struct.unpack('<HH', encryption_info[0x0:0x4])
# We only support Agile Encryption
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/87020a34-e73f-4139-99bc-bbdf6cf6fa55
if v_major != 0x4 or v_minor != 0x4:
if v_major == 0x1 and v_minor == 0x1:
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/76aeedb0-4d59-487f-8bd8-fb6860a60df7
raise NotImplementedError('RC4 Encryption is not supported')
if v_major in (0x2, 0x3, 0x4) and v_minor == 0x2:
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/2895eba1-acb1-4624-9bde-2cdad3fea015
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/36cfb17f-9b15-4a9b-911a-f401f60b3991
raise NotImplementedError('Standard Encryption/RC4 CryptoAPI Encryption are not supported')
if v_major in (0x3, 0x4) and v_minor == 0x3:
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/a922e41e-63f2-4701-8521-7f5d221a7ce0
raise NotImplementedError('Extensible Encryption is not supported')
raise DatabaseFormatError('Unknown encryption version - only Agile Encryption is supported')
# Validate Reserved field
if encryption_info[0x4:0x8] != b'\x40\x00\x00\x00':
raise DatabaseFormatError('Invalid Reserved field of Agile Encryption EncryptionInfo')
# Parse XmlEncryptionDescription
xml_encryption_descriptor = ET.fromstring(encryption_info[0x8:].decode('utf-8'))
# Check that there is one keyEncryptor and it is a password keyEncryptor
key_encryptors = xml_encryption_descriptor.find('{http://schemas.microsoft.com/office/2006/encryption}keyEncryptors').findall('{http://schemas.microsoft.com/office/2006/encryption}keyEncryptor')
if len(key_encryptors) != 1:
raise NotImplementedError('More than 1 keyEncryptor is unsupported')
key_encryptor = key_encryptors[0]
if key_encryptor.get('uri') != 'http://schemas.microsoft.com/office/2006/keyEncryptor/password':
raise NotImplementedError('Non-password keyEncryptor is unsupported')
encrypted_key = key_encryptor.find('{http://schemas.microsoft.com/office/2006/keyEncryptor/password}encryptedKey')
# Validate cipher for encrypted master key
# Only AES-CBC with SHA512 is supported
if encrypted_key.get('hashAlgorithm') != 'SHA512':
raise NotImplementedError('Only SHA256 is supported')
if encrypted_key.get('cipherAlgorithm') != 'AES':
raise NotImplementedError('Only AES is supported')
if encrypted_key.get('cipherChaining') != 'ChainingModeCBC':
raise NotImplementedError('Only AES-CBC is supported')
# Get cipher parameters for encrypted master key
password_salt = base64.b64decode(encrypted_key.get('saltValue'))
password_spin_count = int(encrypted_key.get('spinCount'))
password_key_bits = int(encrypted_key.get('keyBits'))
if str(len(password_salt)) != encrypted_key.get('saltSize'):
raise DatabaseIntegrityError('Unexpected length of password saltValue')
if password_key_bits % 8 != 0:
raise DatabaseIntegrityError('Invalid number of password keyBits')
def get_encryption_key_from_password(block_key):
# Derives an AES-CBC encryption key from the password and block_key
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/74d60145-a0f0-44be-99ce-c65d211b4eb7
# UTF-16LE as per https://sourceforge.net/p/jackcessencrypt/code/199/tree/trunk/src/main/java/com/healthmarketscience/jackcess/crypt/impl/OfficeCryptCodecHandler.java#l260
hash_n = sha512(password_salt + db_password.encode('utf-16le')).digest()
for iterator in range(password_spin_count):
iterator_bytes = struct.pack('<I', iterator)
assert len(iterator_bytes) == 4
hash_n = sha512(iterator_bytes + hash_n).digest()
hash_final = sha512(hash_n + block_key).digest()
if len(hash_final) < password_key_bits//8:
hash_final = hash_final + (b'0x36' * (password_key_bits//8 - len(hash_final)))
if len(hash_final) > password_key_bits//8:
hash_final = hash_final[:password_key_bits//8]
return hash_final
# Verify password
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/a57cb947-554f-4e5e-b150-3f2978225e92
# encryptedVerifierHashInput
cipher = AES.new(get_encryption_key_from_password(b'\xfe\xa7\xd2\x76\x3b\x4b\x9e\x79'), AES.MODE_CBC, iv=password_salt)
verifier_hash_input = cipher.decrypt(base64.b64decode(encrypted_key.get('encryptedVerifierHashInput')))
# encryptedVerifierHashValue
cipher = AES.new(get_encryption_key_from_password(b'\xd7\xaa\x0f\x6d\x30\x61\x34\x4e'), AES.MODE_CBC, iv=password_salt)
verifier_hash_value = cipher.decrypt(base64.b64decode(encrypted_key.get('encryptedVerifierHashValue')))
# Verify the password
if sha512(verifier_hash_input).digest() != verifier_hash_value:
raise DecryptDatabaseError('Incorrect database password')
# encryptedKeyValue
cipher = AES.new(get_encryption_key_from_password(b'\x14\x6e\x0b\xe7\xab\xac\xd0\xd6'), AES.MODE_CBC, iv=password_salt)
db_master_key = cipher.decrypt(base64.b64decode(encrypted_key.get('encryptedKeyValue')))
# Validate cipher for database encryption
# Only AES-CBC with SHA512 is supported
key_data = xml_encryption_descriptor.find('{http://schemas.microsoft.com/office/2006/encryption}keyData')
if key_data.get('hashAlgorithm') != 'SHA512':
raise NotImplementedError('Only SHA256 is supported')
if key_data.get('cipherAlgorithm') != 'AES':
raise NotImplementedError('Only AES is supported')
if key_data.get('cipherChaining') != 'ChainingModeCBC':
raise NotImplementedError('Only AES-CBC is supported')
# Get cipher parameters for database encryption
db_salt = base64.b64decode(key_data.get('saltValue'))
db_block_size = int(key_data.get('blockSize'))
if str(len(db_salt)) != key_data.get('saltSize'):
raise DatabaseIntegrityError('Unexpected length of database saltValue')
def get_iv(page_number):
# Derives an AES-CBC IV from the page number
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/11672b8c-b9f0-4bb7-9b2d-3f1695286436
# CONTRARY to https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/9e61da63-8ddb-4c0a-b25d-f85d990f44c8 we do not directly use the "zero-based segment number as a blockKey"
# https://sourceforge.net/p/jackcessencrypt/code/199/tree/trunk/src/main/java/com/healthmarketscience/jackcess/crypt/impl/office/AgileEncryptionProvider.java#l135
page_number_bytes = struct.pack('<I', page_number)
block_key = bytes((a ^ b for a, b in zip(page_number_bytes, db_encoding_key)))
iv = sha512(db_salt + block_key).digest()
if len(iv) < db_block_size:
iv = iv + (b'\x36' * (db_block_size - len(iv)))
if len(iv) > db_block_size:
iv = iv[:db_block_size]
return iv
# Decrypt data!
with open(output_filename, 'wb') as output_file:
# Generate a new header page with encryption flags unset
header_page_no_encryption = bytearray(header_page)
# Set the bytes at 0x3e:0x42 to the equivalent bytes from RC4 cipherstream
# So that when XORed, the bytes are zeroed - removing the encryption
header_page_no_encryption[0x3e:0x42] = rc4_cipher_stream[0x3e-0x18:0x42-0x18]
output_file.write(header_page_no_encryption)
# Decrypt the subsequent pages
page_num = 0
while True:
page = input_file.read(PAGE_SIZE)
page_num += 1
if not page:
# End of file
break
# https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/9e61da63-8ddb-4c0a-b25d-f85d990f44c8
cipher = AES.new(db_master_key, AES.MODE_CBC, iv=get_iv(page_num))
payload = cipher.decrypt(page)
# Payload is just the raw Access data
output_file.write(payload)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment