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