Herbert Poul
5 years ago
commit
15a0de5196
16 changed files with 858 additions and 0 deletions
@ -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/ |
@ -0,0 +1,8 @@
|
||||
<component name="ProjectDictionaryState"> |
||||
<dictionary name="herbert"> |
||||
<words> |
||||
<w>consts</w> |
||||
<w>kdbx</w> |
||||
</words> |
||||
</dictionary> |
||||
</component> |
@ -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> |
@ -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. |
@ -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 |
||||
|
@ -0,0 +1,6 @@
|
||||
import 'package:kdbx/kdbx.dart'; |
||||
|
||||
void main() { |
||||
var awesome = Awesome(); |
||||
print('awesome: ${awesome.isAwesome}'); |
||||
} |
@ -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. |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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 |
@ -0,0 +1,3 @@
|
||||
{ |
||||
"python.pythonPath": "/Users/herbert/dev/kdbx.dart/test/venv/bin/python3.7" |
||||
} |
Binary file not shown.
@ -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 |
@ -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…
Reference in new issue