Created
May 13, 2018 20:20
-
-
Save Evidlo/8bc69fb58dcd0c9e6ce748b6f2cb59f7 to your computer and use it in GitHub Desktop.
KDBX V3 decryption example. AES only. No compression or entry protection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/env python3 | |
# Evan Widloski - 2018-04-11 | |
# keepass decrypt experimentation | |
# only works on AES encrypted database with unprotected entries | |
# Useful reference: https://gist.github.com/msmuenchen/9318327 | |
# https://framagit.org/okhin/pygcrypt/#use | |
# https://github.com/libkeepass/libkeepass/tree/master/libkeepass | |
import struct | |
database = 'test.kdbx' | |
password = b'shatpass' | |
# password = None | |
keyfile = 'test.key' | |
# keyfile = None | |
b = [] | |
with open('test.kdbx', 'rb') as f: | |
b = bytearray(f.read()) | |
# ---------- Header Stuff ---------- | |
# file magic number (4 bytes) | |
magic = b[0:4] | |
# keepass version (2 bytes) | |
version = b[4:8] | |
# database minor version (2 bytes) | |
minor_version = b[8:10] | |
# database major version (2 bytes) | |
major_version = b[10:12] | |
# header item lookup table | |
header_item_ids = {0: 'end', | |
1: 'comment', | |
2: 'cipher_id', | |
3: 'compression_flags', | |
4: 'master_seed', | |
5: 'transform_seed', | |
6: 'transform_rounds', | |
7: 'encryption_iv', | |
8: 'protected_stream_key', | |
9: 'stream_start_bytes', | |
10: 'inner_random_stream_id' | |
} | |
# read dynamic header | |
# offset of first header byte | |
offset = 12 | |
# dict containing header items | |
header = {} | |
# loop until end of header | |
while b[offset] != 0: | |
# read size of item (2 bytes) | |
size = struct.unpack('<H', b[offset + 1:offset + 3])[0] | |
# insert item into header dict | |
header[header_item_ids[b[offset]]] = b[offset + 3:offset + 3 + size] | |
# move to next header item | |
# (1 byte for header item id, 2 bytes for item size, `size` bytes for data) | |
offset += 1 + 2 + size | |
# move from `end` to start of payload | |
size = struct.unpack('<H', b[offset + 1:offset + 3])[0] | |
offset += 1 + 2 + size | |
# ---------- Payload Stuff ---------- | |
from pygcrypt.ciphers import Cipher | |
from pygcrypt.context import Context | |
import hashlib | |
import zlib | |
from lxml import etree | |
import base64 | |
encrypted_payload = b[offset:] | |
# hash the password | |
if password: | |
password_composite = hashlib.sha256(password).digest() | |
else: | |
password_composite = b'' | |
# hash the keyfile | |
if keyfile: | |
# try to read XML keyfile | |
try: | |
with open(keyfile, 'r') as f: | |
tree = etree.parse(f).getroot() | |
keyfile_composite = base64.b64decode(tree.find('Key/Data').text) | |
# otherwise, try to read plain keyfile | |
except Exception as e: | |
try: | |
with open(keyfile, 'rb') as f: | |
key = f.read() | |
# if the length is 32 bytes we assume it is the key | |
if len(key) == 32: | |
keyfile_composite = key | |
# if the length is 64 bytes we assume the key is hex encoded | |
if len(key) == 64: | |
keyfile_composite = key.decode('hex') | |
# anything else may be a file to hash for the key | |
keyfile_composite = hashlib.sha256(key).digest() | |
except: | |
raise IOError('Could not read keyfile') | |
else: | |
keyfile_composite = b'' | |
# create composite key from password and keyfile composites | |
key_composite = hashlib.sha256(password_composite + keyfile_composite).digest() | |
# set up a context for AES128-ECB encryption to find transformed_key | |
context = Context() | |
cipher = Cipher(b'AES', u'ECB') | |
context.cipher = cipher | |
context.key = bytes(header['transform_seed']) | |
context.iv = b'\x00' * 16 | |
# get the number of rounds from the header and transform the key_composite | |
rounds = struct.unpack('<Q', header['transform_rounds'])[0] | |
transformed_key = key_composite | |
for _ in range(0, rounds): | |
transformed_key = context.cipher.encrypt(transformed_key) | |
# combine the transformed key with the header master seed to find the master_key | |
transformed_key = hashlib.sha256(transformed_key).digest() | |
master_key = hashlib.sha256(bytes(header['master_seed']) + transformed_key).digest() | |
# set up a context for AES128-CBC decryption to find the decrypted payload | |
context = Context() | |
cipher = Cipher(b'AES', u'CBC') | |
context.cipher = cipher | |
context.key = master_key | |
context.iv = bytes(header['encryption_iv']) | |
raw_payload_area = context.cipher.decrypt(bytes(encrypted_payload)) | |
# verify decryption | |
if header['stream_start_bytes'] != raw_payload_area[:len(header['stream_start_bytes'])]: | |
raise IOError('Decryption failed') | |
# remove stream start bytes | |
offset = len(header['stream_start_bytes']) | |
payload_data = b'' | |
# read payload block data, block by block | |
while True: | |
# read index of block (4 bytes) | |
block_index = struct.unpack('<I', raw_payload_area[offset:offset + 4])[0] | |
# read block_data sha256 hash (32 bytes) | |
block_hash = raw_payload_area[offset + 4:offset + 36] | |
# read block_data length (4 bytes) | |
block_length = struct.unpack('<I', raw_payload_area[offset + 36:offset + 40])[0] | |
# read block_data | |
block_data = raw_payload_area[offset + 40:offset + 40 + block_length] | |
# check if last block | |
if block_hash == b'\x00' * 32 and block_length == 0: | |
break | |
# verify block validity | |
if block_hash != hashlib.sha256(block_data).digest(): | |
raise IOError('Block hash verification failed') | |
# append verified block_data and move to next block | |
payload_data += block_data | |
offset += 40 + block_length | |
# check if payload_data is compressed | |
if struct.unpack('<I', header['compression_flags']): | |
# decompress using gzip | |
xml_data = zlib.decompress(payload_data, 16 + 15) | |
else: | |
xml_data = payload_data |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment