commit 15a0de51965218baa429795f010a870c04950bdf Author: Herbert Poul Date: Wed Aug 21 02:04:27 2019 +0200 initial commit. we can already decrypt the xml. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e23330 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# https://gist.github.com/hpoul/b78f7a1b3cde988f3ce4d12e954367eb +# +# IDEA: Allow some configuration, which is shared across users. + +/.idea/* +!.idea/runConfigurations +!.idea/runConfigurations/* +!.idea/vcs.xml +!.idea/dictionaries +!.idea/dictionaries/* +!.idea/inspectionProfiles/* +!.idea/codeStyles +!.idea/codeStyles/* +*.iml + +# Java/Kotlin/Android + +/.gradle +/build +/out + +# JavaScript/Node.JS + +/node_modules + +#### project specific ignores below + + +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ +ios/.generated/ +ios/Flutter/Generated.xcconfig +ios/Runner/GeneratedPluginRegistrant.* + +/pubspec.lock + +# Directory created by dartdoc +doc/api/ diff --git a/.idea/dictionaries/herbert.xml b/.idea/dictionaries/herbert.xml new file mode 100644 index 0000000..5d20749 --- /dev/null +++ b/.idea/dictionaries/herbert.xml @@ -0,0 +1,8 @@ + + + + consts + kdbx + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..687440b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version, created by Stagehand diff --git a/README.md b/README.md new file mode 100644 index 0000000..00b409d --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# kdbx.dart + +KeepassX format implementation in pure dart. + +Very much based on https://github.com/keeweb/kdbxweb/ + +## Usage + +TODO + +## Features and bugs + +* Only supports v3. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..976c29b --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,172 @@ +# Defines a default set of lint rules enforced for +# projects at Google. For details and rationale, +# see https://github.com/dart-lang/pedantic#enabled-lints. +include: package:pedantic/analysis_options.yaml + +analyzer: + strong-mode: + implicit-casts: false + implicit-dynamic: false + errors: + # treat missing required parameters as a warning (not a hint) + missing_required_param: warning + # treat missing returns as a warning (not a hint) + missing_return: warning + # allow having TODOs in the code + todo: ignore + +linter: + rules: + # these rules are documented on and in the same order as + # the Dart Lint rules page to make maintenance easier + # http://dart-lang.github.io/linter/lints/ + + # HP mostly in sync with https://github.com/flutter/flutter/blob/master/analysis_options.yaml + + - always_declare_return_types + - always_put_control_body_on_new_line + # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 + - always_require_non_null_named_parameters + #- always_specify_types + - annotate_overrides + # - avoid_annotating_with_dynamic # not yet tested + # - avoid_as + - avoid_bool_literals_in_conditional_expressions + # - avoid_catches_without_on_clauses # not yet tested + # - avoid_catching_errors # not yet tested + # - avoid_classes_with_only_static_members # not yet tested + # - avoid_double_and_int_checks # only useful when targeting JS runtime + - avoid_empty_else + - avoid_field_initializers_in_const_classes + - avoid_function_literals_in_foreach_calls + # - avoid_implementing_value_types # not yet tested + - avoid_init_to_null + # - avoid_js_rounded_ints # only useful when targeting JS runtime + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters # not yet tested + # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + # - avoid_returning_null # not yet tested + # - avoid_returning_null_for_future # not yet tested + - avoid_returning_null_for_void + # - avoid_returning_this # not yet tested + # - avoid_setters_without_getters # not yet tested + # - avoid_shadowing_type_parameters # not yet tested + # - avoid_single_cascade_in_expression_statements # not yet tested + - avoid_slow_async_io + - avoid_types_as_parameter_names + # - avoid_types_on_closure_parameters # not yet tested + - avoid_unused_constructor_parameters + - avoid_void_async + - await_only_futures + - camel_case_types + - cancel_subscriptions + # - cascade_invocations # not yet tested + # - close_sinks # not reliable enough + # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 + # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 + - control_flow_in_finally + - curly_braces_in_flow_control_structures + # - diagnostic_describe_all_properties # not yet tested + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + # - file_names # not yet tested + # - flutter_style_todos TODO(HP) + - hash_and_equals + - implementation_imports + # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 + - iterable_contains_unrelated_type + # - join_return_with_assignment # not yet tested + - library_names + - library_prefixes + # - lines_longer_than_80_chars # not yet tested + - list_remove_unrelated_type + # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + # - null_closures # not yet tested + # - omit_local_variable_types # opposite of always_specify_types + # - one_member_abstracts # too many false positives + # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + # - parameter_assignments # we do this commonly + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + # - prefer_asserts_with_message # not yet tested + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + # - prefer_constructors_over_static_methods # not yet tested + - prefer_contains + # - prefer_double_quotes # opposite of prefer_single_quotes + - prefer_equal_for_default_values + # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods + - prefer_final_fields + # - prefer_final_in_for_each # not yet tested + - prefer_final_locals + # - prefer_for_elements_to_map_fromIterable # not yet tested + - prefer_foreach + # - prefer_function_declarations_over_variables # not yet tested + - prefer_generic_function_type_aliases + # - prefer_if_elements_to_conditional_expressions # not yet tested + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + # - prefer_int_literals # not yet tested + # - prefer_interpolation_to_compose_strings # not yet tested + - prefer_is_empty + - prefer_is_not_empty + - prefer_iterable_whereType + # - prefer_mixin # https://github.com/dart-lang/language/issues/32 + # - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932 + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + # - provide_deprecation_message # not yet tested + # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml + - recursive_getters + - slash_for_doc_comments + # - sort_child_properties_last # not yet tested + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + # - type_annotate_public_apis # subset of always_specify_types + - type_init_formals + # - unawaited_futures # https://github.com/flutter/flutter/issues/5793 + # - unnecessary_await_in_return # not yet tested + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498 + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + #- unnecessary_parenthesis HP: I like parenthesis :-) + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + # - unsafe_html # not yet tested + - use_full_hex_values_for_flutter_colors + # - use_function_type_syntax_for_parameters # not yet tested + - use_rethrow_when_possible + # - use_setters_to_change_properties # not yet tested + # - use_string_buffers # https://github.com/dart-lang/linter/pull/664 + # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review + - valid_regexps + # - void_checks # not yet tested + diff --git a/example/kdbx_example.dart b/example/kdbx_example.dart new file mode 100644 index 0000000..cb932ef --- /dev/null +++ b/example/kdbx_example.dart @@ -0,0 +1,6 @@ +import 'package:kdbx/kdbx.dart'; + +void main() { + var awesome = Awesome(); + print('awesome: ${awesome.isAwesome}'); +} diff --git a/lib/kdbx.dart b/lib/kdbx.dart new file mode 100644 index 0000000..f20b988 --- /dev/null +++ b/lib/kdbx.dart @@ -0,0 +1,8 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library kdbx; + +export 'src/kdbx_base.dart'; + +// TODO: Export any libraries intended for clients of this package. diff --git a/lib/src/crypto/protected_value.dart b/lib/src/crypto/protected_value.dart new file mode 100644 index 0000000..452aae8 --- /dev/null +++ b/lib/src/crypto/protected_value.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; + +class ProtectedValue { + ProtectedValue(this._value, this._salt); + + factory ProtectedValue.fromString(String value) { + final Uint8List valueBytes = utf8.encode(value) as Uint8List; + final Uint8List salt = _randomBytes(valueBytes.length); + + return ProtectedValue(_xor(valueBytes, salt), salt); + } + + static final random = Random.secure(); + + final Uint8List _value; + final Uint8List _salt; + + Uint8List get binaryValue => _xor(_value, _salt); + Uint8List get hash => sha256.convert(binaryValue).bytes as Uint8List; + + static Uint8List _randomBytes(int length) { + return Uint8List.fromList(List.generate(length, (i) => random.nextInt(0xff))); + } + static Uint8List _xor(Uint8List a, Uint8List b) { + assert(a.length == b.length); + final ret = Uint8List(a.length); + for (int i = 0 ; i < a.length ; i++) { + ret[i] = a[i] ^ b[i]; + } + return ret; + } +} diff --git a/lib/src/kdbx_base.dart b/lib/src/kdbx_base.dart new file mode 100644 index 0000000..e8a6f15 --- /dev/null +++ b/lib/src/kdbx_base.dart @@ -0,0 +1,6 @@ +// TODO: Put public facing types in this file. + +/// Checks if you are awesome. Spoiler: you are. +class Awesome { + bool get isAwesome => true; +} diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart new file mode 100644 index 0000000..56a0982 --- /dev/null +++ b/lib/src/kdbx_header.dart @@ -0,0 +1,335 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:convert/convert.dart' as convert; +import 'package:crypto/crypto.dart' as crypto; +import 'package:kdbx/src/crypto/protected_value.dart'; +import 'package:logging/logging.dart'; +import 'package:pointycastle/export.dart'; + +final _logger = Logger('kdbx.header'); + +class Consts { + static const FileMagic = 0x9AA2D903; + + static const Sig2Kdbx = 0xB54BFB67; +} + +enum Compression { + /// id: 0 + none, + + /// id: 1 + gzip, +} + +enum HeaderFields { + EndOfHeader, + Comment, + CipherID, + CompressionFlags, + MasterSeed, + TransformSeed, + TransformRounds, + EncryptionIV, + ProtectedStreamKey, + StreamStartBytes, + InnerRandomStreamID, + KdfParameters, + PublicCustomData, +} + +class HeaderField { + HeaderField(this.field, this.bytes); + + final HeaderFields field; + final ByteBuffer bytes; + + String get name => field.toString(); +} + +String _toHex(int val) => '0x${val.toRadixString(16)}'; + +String _toHexList(Uint8List list) => list.map((val) => _toHex(val)).join(' '); + +class KdbxHeader { + KdbxHeader({this.sig1, this.sig2, this.versionMinor, this.versionMajor, this.fields}); + + static Future read(ReaderHelper reader) async { + // reading signature + final sig1 = reader.readUint32(); + final sig2 = reader.readUint32(); + if (!(sig1 == Consts.FileMagic && sig2 == Consts.Sig2Kdbx)) { + throw UnsupportedError('Unsupported file structure. ${_toHex(sig1)}, ${_toHex(sig2)}'); + } + + // reading version + final versionMinor = reader.readUint16(); + final versionMajor = reader.readUint16(); + + _logger.finer('Reading version: $versionMajor.$versionMinor'); + final headerFields = Map.fromEntries(readField(reader, versionMajor).map((field) => MapEntry(field.field, field))); + return KdbxHeader( + sig1: sig1, + sig2: sig2, + versionMinor: versionMinor, + versionMajor: versionMajor, + fields: headerFields, + ); + } + + static Iterable readField(ReaderHelper reader, int versionMajor) sync* { + while (true) { + final headerId = reader.readUint8(); + int size = versionMajor >= 4 ? reader.readUint32() : reader.readUint16(); + _logger.finer('Read header ${HeaderFields.values[headerId]}'); + final bodyBytes = size > 0 ? reader.readBytes(size) : null; + if (headerId > 0) { + yield HeaderField(HeaderFields.values[headerId], bodyBytes); + } else { + break; + } + } + } + + final int sig1; + final int sig2; + final int versionMinor; + final int versionMajor; + final Map fields; + + Compression get compression { + switch (fields[HeaderFields.CompressionFlags].bytes.asUint32List().single) { + case 0: + return Compression.none; + case 1: + return Compression.gzip; + default: + throw KdbxUnsupportedException('compression'); + } + } +} + +class Credentials { + Credentials(this._password); + + final ProtectedValue _password; + + Uint8List getHash() { + final output = convert.AccumulatorSink(); + final input = crypto.sha256.startChunkedConversion(output); + input.add(_password.hash); + input.close(); + return output.events.single.bytes as Uint8List; + } +} + +class KdbxException implements Exception {} + +class KdbxInvalidKeyException implements KdbxException {} + +class KdbxCorruptedFileException implements KdbxException {} + +class KdbxUnsupportedException implements KdbxException { + KdbxUnsupportedException(this.hint); + + final String hint; +} + +class KdbxFormat { + static Future read(Uint8List input, Credentials credentials) async { + final reader = ReaderHelper(input); + final header = await KdbxHeader.read(reader); + _loadV3(header, reader, credentials); + } + + static void _loadV3(KdbxHeader header, ReaderHelper reader, Credentials credentials) { +// _getMasterKeyV3(header, credentials); + final pwHash = credentials.getHash(); + final seed = header.fields[HeaderFields.TransformSeed].bytes.asUint8List(); + final rounds = header.fields[HeaderFields.TransformRounds].bytes.asUint64List().first; + final masterSeed = header.fields[HeaderFields.MasterSeed].bytes; + final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes; + _logger.finer('Rounds: $rounds'); + final cipher = ECBBlockCipher(AESFastEngine()); + final encryptedPayload = reader.readRemaining(); + cipher.init(true, KeyParameter(seed)); + + var transformedKey = pwHash; + for (int i = 0; i < rounds; i++) { + transformedKey = AesHelper._processBlocks(cipher, transformedKey); + } + transformedKey = crypto.sha256.convert(transformedKey).bytes as Uint8List; + final masterKey = + crypto.sha256.convert(Uint8List.fromList(masterSeed.asUint8List() + transformedKey)).bytes as Uint8List; + final decryptCipher = CBCBlockCipher(AESFastEngine()); + decryptCipher.init(false, ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List())); +// final decrypted = decryptCipher.process(encryptedPayload); + final decrypted = AesHelper._processBlocks(decryptCipher, encryptedPayload); + + final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes; + + print('streamStart: ${_toHexList(streamStart.asUint8List())}'); + print('actual : ${_toHexList(decrypted.sublist(0, streamStart.lengthInBytes))}'); + + if (!_eq(streamStart.asUint8List(), decrypted.sublist(0, streamStart.lengthInBytes))) { + throw KdbxInvalidKeyException(); + } + final content = decrypted.sublist(streamStart.lengthInBytes); + final blocks = HashedBlockReader.readBlocks(ReaderHelper(content)); + + print('compression: ${header.compression}'); + if (header.compression == Compression.gzip) { + final xml = GZipCodec().decode(blocks); + final string = utf8.decode(xml); + print('xml: $string'); + } + +// final result = utf8.decode(decrypted); +// final aesEngine = AESFastEngine(); +// aesEngine.init(true, KeyParameter(seed)); +// final key = AesHelper.deriveKey(keyComposite.bytes as Uint8List, salt: seed, iterationCount: rounds, derivedKeyLength: 32); +// final masterKey = Uint8List.fromList(key + masterSeed.asUint8List()); +// print('key length: ${key.length} + ${masterSeed.lengthInBytes} = ${masterKey.lengthInBytes} (${masterKey.lengthInBytes} bytes)'); + +// final result = AesHelper.decrypt(masterKey, reader.readRemaining()); + print('before : ${_toHexList(encryptedPayload)}'); + } + + static void _getMasterKeyV3(KdbxHeader header, Credentials credentials) { + final pwHash = credentials.getHash(); + final seed = header.fields[HeaderFields.TransformSeed].bytes.asUint8List(); + final rounds = header.fields[HeaderFields.TransformRounds].bytes.asUint64List().first; + final masterSeed = header.fields[HeaderFields.MasterSeed].bytes; + final key = AesHelper.deriveKey(pwHash, salt: seed, iterationCount: rounds); + } +} + +bool _eq(Uint8List a, Uint8List b) { + if (a.length != b.length) { + return false; + } + for (int i = a.length - 1; i >= 0; i--) { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +class HashedBlockReader { + static Uint8List readBlocks(ReaderHelper reader) => + Uint8List.fromList(readNextBlock(reader).expand((x) => x).toList()); + + static Iterable readNextBlock(ReaderHelper reader) sync* { + while (true) { + final blockIndex = reader.readUint32(); + final blockHash = reader.readBytes(32); + final blockSize = reader.readUint32(); + if (blockSize > 0) { + final blockData = reader.readBytes(blockSize).asUint8List(); + if (!_eq(crypto.sha256.convert(blockData).bytes as Uint8List, blockHash.asUint8List())) { + throw KdbxCorruptedFileException(); + } + yield blockData; + } else { + break; + } + } + } +} + +class ReaderHelper { + ReaderHelper(this.data); + + final Uint8List data; + int pos = 0; + + ByteBuffer _nextByteBuffer(int byteCount) => data.sublist(pos, pos += byteCount).buffer; + + int readUint32() => _nextByteBuffer(4).asUint32List().first; + + int readUint16() => _nextByteBuffer(2).asUint16List().first; + + int readUint8() => data[pos++]; + + ByteBuffer readBytes(int size) => _nextByteBuffer(size); + + Uint8List readRemaining() => data.sublist(pos); +} + +/// https://gist.github.com/proteye/e54eef1713e1fe9123d1eb04c0a5cf9b +class AesHelper { + static const CBC_MODE = 'CBC'; + static const CFB_MODE = 'CFB'; + + // AES key size + static const KEY_SIZE = 32; // 32 byte key for AES-256 + static const ITERATION_COUNT = 1000; + + static Uint8List deriveKey( + Uint8List password, { + Uint8List salt, + int iterationCount = ITERATION_COUNT, + int derivedKeyLength = KEY_SIZE, + }) { + Pbkdf2Parameters params = Pbkdf2Parameters(salt, iterationCount, derivedKeyLength); + KeyDerivator keyDerivator = PBKDF2KeyDerivator(HMac(SHA256Digest(), 16)); + keyDerivator.init(params); + + return keyDerivator.process(password); + } + + static String decrypt(Uint8List derivedKey, Uint8List cipherIvBytes, {String mode = CBC_MODE}) { +// Uint8List derivedKey = deriveKey(password); + KeyParameter keyParam = KeyParameter(derivedKey); + BlockCipher aes = AESFastEngine(); + +// Uint8List cipherIvBytes = base64.decode(ciphertext); + Uint8List iv = Uint8List(aes.blockSize)..setRange(0, aes.blockSize, cipherIvBytes); + + BlockCipher cipher; + ParametersWithIV params = ParametersWithIV(keyParam, iv); + switch (mode) { + case CBC_MODE: + cipher = CBCBlockCipher(aes); + break; + case CFB_MODE: + cipher = CFBBlockCipher(aes, aes.blockSize); + break; + default: + throw ArgumentError('incorrect value of the "mode" parameter'); + break; + } + cipher.init(false, params); + + int cipherLen = cipherIvBytes.length - aes.blockSize; + Uint8List cipherBytes = new Uint8List(cipherLen)..setRange(0, cipherLen, cipherIvBytes, aes.blockSize); + Uint8List paddedText = _processBlocks(cipher, cipherBytes); + Uint8List textBytes = unpad(paddedText); + + return String.fromCharCodes(textBytes); + } + + static Uint8List unpad(Uint8List src) { + final pad = PKCS7Padding(); + pad.init(null); + + int padLength = pad.padCount(src); + int len = src.length - padLength; + + return Uint8List(len)..setRange(0, len, src); + } + + static Uint8List _processBlocks(BlockCipher cipher, Uint8List inp) { + var out = Uint8List(inp.lengthInBytes); + + for (var offset = 0; offset < inp.lengthInBytes;) { + var len = cipher.processBlock(inp, offset, out, offset); + offset += len; + } + + return out; + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..67fba76 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,19 @@ +name: kdbx +description: A starting point for Dart libraries or applications. +# version: 1.0.0 +# homepage: https://www.example.com +# author: herbert + +environment: + sdk: '>=2.4.0 <3.0.0' + +dependencies: +# path: ^1.6.0 + logging: '>=0.11.3+2 <1.0.0' + crypto: '>=2.0.0 <3.0.0' + pointycastle: ^1.0.1 + +dev_dependencies: + logging_appenders: '>=0.1.0 <1.0.0' + pedantic: ^1.7.0 + test: ^1.6.0 diff --git a/test/.vscode/settings.json b/test/.vscode/settings.json new file mode 100644 index 0000000..7ec05a1 --- /dev/null +++ b/test/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/Users/herbert/dev/kdbx.dart/test/venv/bin/python3.7" +} \ No newline at end of file diff --git a/test/FooBar.kdbx b/test/FooBar.kdbx new file mode 100644 index 0000000..0329487 Binary files /dev/null and b/test/FooBar.kdbx differ diff --git a/test/kdbx3_decrypt.py b/test/kdbx3_decrypt.py new file mode 100644 index 0000000..67c4afe --- /dev/null +++ b/test/kdbx3_decrypt.py @@ -0,0 +1,174 @@ +#!/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 = 'FooBar.kdbx' +password = b'FooBar' +# password = None +#keyfile = 'test3.key' +keyfile = None + +b = [] +with open(database, '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('