Browse Source

initial commit. we can already decrypt the xml.

remove-cryptography-dependency
Herbert Poul 5 years ago
commit
15a0de5196
  1. 43
      .gitignore
  2. 8
      .idea/dictionaries/herbert.xml
  3. 6
      .idea/vcs.xml
  4. 3
      CHANGELOG.md
  5. 13
      README.md
  6. 172
      analysis_options.yaml
  7. 6
      example/kdbx_example.dart
  8. 8
      lib/kdbx.dart
  9. 36
      lib/src/crypto/protected_value.dart
  10. 6
      lib/src/kdbx_base.dart
  11. 335
      lib/src/kdbx_header.dart
  12. 19
      pubspec.yaml
  13. 3
      test/.vscode/settings.json
  14. BIN
      test/FooBar.kdbx
  15. 174
      test/kdbx3_decrypt.py
  16. 26
      test/kdbx_test.dart

43
.gitignore vendored

@ -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/

8
.idea/dictionaries/herbert.xml

@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="herbert">
<words>
<w>consts</w>
<w>kdbx</w>
</words>
</dictionary>
</component>

6
.idea/vcs.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

3
CHANGELOG.md

@ -0,0 +1,3 @@
## 1.0.0
- Initial version, created by Stagehand

13
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.

172
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

6
example/kdbx_example.dart

@ -0,0 +1,6 @@
import 'package:kdbx/kdbx.dart';
void main() {
var awesome = Awesome();
print('awesome: ${awesome.isAwesome}');
}

8
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.

36
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;
}
}

6
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;
}

335
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<KdbxHeader> 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<HeaderField> 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<HeaderFields, HeaderField> 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<crypto.Digest>();
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<void> 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<Uint8List> 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;
}
}

19
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 <herbert@poul.at>
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

3
test/.vscode/settings.json vendored

@ -0,0 +1,3 @@
{
"python.pythonPath": "/Users/herbert/dev/kdbx.dart/test/venv/bin/python3.7"
}

BIN
test/FooBar.kdbx

Binary file not shown.

174
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('<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

26
test/kdbx_test.dart

@ -0,0 +1,26 @@
import 'dart:io';
import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/kdbx_header.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:test/test.dart';
void main() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen(PrintAppender().logListener());
group('A group of tests', () {
Awesome awesome;
setUp(() {
awesome = Awesome();
});
test('First Test', () async {
final data = await File('test/FooBar.kdbx').readAsBytes();
await KdbxFormat.read(data, Credentials(ProtectedValue.fromString('FooBar')));
expect(awesome.isAwesome, isTrue);
});
});
}
Loading…
Cancel
Save