Compare commits

...

31 Commits

Author SHA1 Message Date
Сергей Марков 8a95283931 add aurora platform 12 months ago
Марков Сергей Викторович 676c162770 new platform (aurora) is add 12 months ago
Herbert Poul 2968ec486b migrate to latest dart version. 1 year ago
Herbert Poul e0774d502c fix coverage script. 3 years ago
Herbert Poul 48da32961f upgrade depencencies. release 2.3.0 3 years ago
Herbert Poul caa1be954c update analysis to use flutter_lints instead of pedantic package. 3 years ago
Herbert Poul 50718adc32 make bytes in InnerHeaderField non-nullable. 3 years ago
Herbert Poul 5cf6b8db93 fix nullability for KdbxBinary 3 years ago
Herbert Poul 36007bbe4a add support for CustomData in entries. 3 years ago
Herbert Poul 96793a5206 Imlement generating of keyfiles (keyx format) 3 years ago
Herbert Poul e2fd1686c4 move credentials classes into their own file. 3 years ago
Herbert Poul cb11abef41 make credentials changable. 3 years ago
Herbert Poul 5694c6c468 allow save methods to return a object 3 years ago
Herbert Poul 157a85acbc - Mark objects only as clean when saving was successful. 3 years ago
Herbert Poul 90bc4d3138 If argon2 ffi implementation is not available, fallback to pointycastle (dart-only) implementation. 3 years ago
Herbert Poul e7e342abb1 move exceptions into their own package. 3 years ago
Herbert Poul 3570757fae Throw KdbxInvalidFileStructure for invalid files 3 years ago
Herbert Poul bc46c500ac fix missing curly brace. 3 years ago
Herbert Poul 65bf16bfcd improve merge debug summary 3 years ago
Herbert Poul 2edd3b57ba export MergeContext 3 years ago
Herbert Poul 011c40d31b better debugging output for merging. 3 years ago
Herbert Poul 70c80ee527 add easy way to check whether a given object is located in the recycle bin. 3 years ago
Herbert Poul 09fd6878b1 kdbx4.1: keep track of PreviousParentGroup 3 years ago
Herbert Poul 36563e84c7 fix merging of incoming deleted objects. 3 years ago
Herbert Poul f5030aba1a pubspec cleanup. 3 years ago
Herbert Poul 59078d6ec8 implement permanently deleting entries and groups. 3 years ago
Herbert Poul 6511ce973e improve nnbd, add debugging to aes decryption 3 years ago
Herbert Poul 1353eea3d1 trigger event after all modifications, not just after the first one. 3 years ago
Herbert Poul 248ca9b1db Upgrade dependencies. 3 years ago
Herbert Poul d3c024c7f5 make KdbxCustomIcon properties non-nullable. 3 years ago
Herbert Poul 6c477ec240 never return null for kdfType 3 years ago
  1. 4
      .github/workflows/dart.yml
  2. 31
      CHANGELOG.md
  3. 6
      _tool/test-coverage.sh
  4. 194
      analysis_options.yaml
  5. 192
      example/pubspec.lock
  6. 2
      example/pubspec.yaml
  7. 23
      lib/kdbx.dart
  8. 43
      lib/src/credentials/credentials.dart
  9. 156
      lib/src/credentials/keyfile.dart
  10. 15
      lib/src/crypto/key_encrypter_kdf.dart
  11. 4
      lib/src/crypto/protected_value.dart
  12. 2
      lib/src/internal/crypto_utils.dart
  13. 37
      lib/src/internal/pointycastle_argon2.dart
  14. 28
      lib/src/kdbx_binary.dart
  15. 16
      lib/src/kdbx_custom_data.dart
  16. 23
      lib/src/kdbx_dao.dart
  17. 9
      lib/src/kdbx_deleted_object.dart
  18. 22
      lib/src/kdbx_entry.dart
  19. 36
      lib/src/kdbx_exceptions.dart
  20. 40
      lib/src/kdbx_file.dart
  21. 231
      lib/src/kdbx_format.dart
  22. 9
      lib/src/kdbx_group.dart
  23. 62
      lib/src/kdbx_header.dart
  24. 20
      lib/src/kdbx_meta.dart
  25. 92
      lib/src/kdbx_object.dart
  26. 14
      lib/src/kdbx_var_dictionary.dart
  27. 4
      lib/src/kdbx_xml.dart
  28. 12
      lib/src/utils/byte_utils.dart
  29. 7
      lib/src/utils/print_utils.dart
  30. 28
      lib/src/utils/sequence.dart
  31. 23
      pubspec.yaml
  32. 61
      test/deleted_objects_test.dart
  33. 4
      test/icon/kdbx_customicon_test.dart
  34. 2
      test/internal/byte_utils_test.dart
  35. 39
      test/internal/test_utils.dart
  36. 20
      test/kdbx4_test.dart
  37. 59
      test/kdbx4_test_pointycastle.dart
  38. 48
      test/kdbx_binaries_test.dart
  39. 57
      test/kdbx_dirty_save_test.dart
  40. 8
      test/kdbx_history_test.dart
  41. 16
      test/kdbx_test.dart
  42. 10
      test/kdbx_upgrade_test.dart
  43. 45
      test/keyfile/keyfile_create_test.dart
  44. 58
      test/merge/kdbx_merge_test.dart

4
.github/workflows/dart.yml

@ -22,9 +22,9 @@ jobs:
codesign --remove-signature $(which dart) codesign --remove-signature $(which dart)
if: startsWith(matrix.os, 'macos') if: startsWith(matrix.os, 'macos')
- name: Install dependencies - name: Install dependencies
run: pub get run: dart pub get
- name: Run tests - name: Run tests
run: pub run test run: dart run test
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

31
CHANGELOG.md

@ -1,3 +1,34 @@
## 2.4.0
- Migrate to latest dart version.
## 2.3.0
- Mark objects only as clean when saving was successful.
- Only mark objects as clean if they have not been modified since we started saving.
- Make credentials changeable.
- Add support for CustomData in entries.
- Upgrade dependencies.
## 2.2.0
- If argon2 ffi implementation is not available, fallback to pointycastle (dart-only)
implementation.
## 2.1.1
- Throw KdbxInvalidFileStructure for invalid files.
## 2.1.0
- Implement permanently removing entries and groups.
- Fix merging of files with incoming deleted objects.
## 2.0.0+1
- Small Null-safety improvement.
- add debugging to AES decryption.
## 2.0.0 ## 2.0.0
- Null-safety migration - Null-safety migration

6
_tool/test-coverage.sh

@ -14,8 +14,8 @@ set -xeu
cd "${0%/*}"/.. cd "${0%/*}"/..
pub get dart pub get
pub global activate coverage dart pub global activate coverage
fail=false fail=false
dart test --coverage coverage || fail=true dart test --coverage coverage || fail=true
@ -25,7 +25,7 @@ echo "fail=$fail"
# shellcheck disable=SC2046 # shellcheck disable=SC2046
jq -s '{coverage: [.[].coverage] | flatten}' $(find coverage -name '*.json' | xargs) > coverage/merged_json.cov jq -s '{coverage: [.[].coverage] | flatten}' $(find coverage -name '*.json' | xargs) > coverage/merged_json.cov
pub global run coverage:format_coverage --packages=.packages -i coverage/merged_json.cov -l --report-on lib --report-on test > coverage/lcov.info dart pub global run coverage:format_coverage -i coverage/merged_json.cov -l --report-on lib --report-on test > coverage/lcov.info
bash <(curl -s https://codecov.io/bash) -f coverage/lcov.info bash <(curl -s https://codecov.io/bash) -f coverage/lcov.info

194
analysis_options.yaml

@ -1,12 +1,9 @@
# Defines a default set of lint rules enforced for # I kind of prefer the flutter lints, so use it for now,
# projects at Google. For details and rationale, # instead of manually copying it over 🤷
# see https://github.com/dart-lang/pedantic#enabled-lints.
include: package:pedantic/analysis_options.yaml include: package:flutter_lints/flutter.yaml
analyzer: analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
errors: errors:
# treat missing required parameters as a warning (not a hint) # treat missing required parameters as a warning (not a hint)
missing_required_param: warning missing_required_param: warning
@ -14,159 +11,42 @@ analyzer:
missing_return: warning missing_return: warning
# allow having TODOs in the code # allow having TODOs in the code
todo: ignore todo: ignore
language:
strict-casts: true
strict-raw-types: true
linter: linter:
rules: rules:
# these rules are documented on and in the same order as avoid_print: false
# the Dart Lint rules page to make maintenance easier
# http://dart-lang.github.io/linter/lints/ # TODO: rename constants.
constant_identifier_names: false
# HP mostly in sync with https://github.com/flutter/flutter/blob/master/analysis_options.yaml always_declare_return_types: true
prefer_single_quotes: true
unawaited_futures: true
unsafe_html: true
- always_declare_return_types always_put_control_body_on_new_line: true
- always_put_control_body_on_new_line avoid_bool_literals_in_conditional_expressions: true
# - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 avoid_field_initializers_in_const_classes: true
- always_require_non_null_named_parameters avoid_function_literals_in_foreach_calls: true
#- always_specify_types avoid_slow_async_io: true
- annotate_overrides avoid_unused_constructor_parameters: true
# - avoid_annotating_with_dynamic # not yet tested avoid_void_async: true
# - avoid_as cancel_subscriptions: true
- avoid_bool_literals_in_conditional_expressions directives_ordering: true
# - avoid_catches_without_on_clauses # not yet tested no_adjacent_strings_in_list: true
# - avoid_catching_errors # not yet tested package_api_docs: true
# - avoid_classes_with_only_static_members # not yet tested prefer_asserts_in_initializer_lists: true
# - avoid_double_and_int_checks # only useful when targeting JS runtime prefer_final_in_for_each: true
- avoid_empty_else prefer_final_locals: true
- avoid_field_initializers_in_const_classes prefer_foreach: true
- avoid_function_literals_in_foreach_calls sort_constructors_first: true
# - avoid_implementing_value_types # not yet tested sort_unnamed_constructors_first: true
- avoid_init_to_null test_types_in_equals: true
# - avoid_js_rounded_ints # only useful when targeting JS runtime throw_in_finally: true
- avoid_null_checks_in_equality_operators unnecessary_null_aware_assignments: true
# - avoid_positional_boolean_parameters # not yet tested unnecessary_statements: true
# - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) unrelated_type_equality_checks: true
- 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

192
example/pubspec.lock

@ -5,225 +5,313 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
url: "https://pub.dartlang.org" sha256: a92e39b291073bb840a72cf43d96d2a63c74e9a485d227833e8ea0054d16ad16
url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
argon2_ffi_base: argon2_ffi_base:
dependency: transitive dependency: transitive
description: description:
name: argon2_ffi_base path: "../../argon2_ffi_base"
url: "https://pub.dartlang.org" relative: true
source: hosted source: path
version: "1.0.0+2" version: "1.1.1"
args: args:
dependency: transitive dependency: transitive
description: description:
name: args name: args
url: "https://pub.dartlang.org" sha256: "3d82ff8620ec576fd38f6cec0df45a7c088b8704eb1c63d4c336392e5efca6ca"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "1.3.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
name: charcode name: charcode
url: "https://pub.dartlang.org" sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306
url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.3.1"
clock: clock:
dependency: transitive dependency: transitive
description: description:
name: clock name: clock
url: "https://pub.dartlang.org" sha256: "6021e0172ab6e6eaa1d391afed0a99353921f00c54385c574dc53e55d67c092c"
url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
url: "https://pub.dartlang.org" sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.17.2"
convert: convert:
dependency: transitive dependency: transitive
description: description:
name: convert name: convert
url: "https://pub.dartlang.org" sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d
url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
name: crypto name: crypto
url: "https://pub.dartlang.org" sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c
url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
dio: dio:
dependency: transitive dependency: transitive
description: description:
name: dio name: dio
url: "https://pub.dartlang.org" sha256: bf173c8bc66b776e3c2892b6ac56ac1a5ad73d21dd06d337f9fe656f63612947
url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
url: "https://pub.dartlang.org" sha256: "35d0f481d939de0d640b3db9a7aa36a52cd22054a798a73b4f50bdad5ce12678"
url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.1.2"
flutter:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493
url: "https://pub.dev"
source: hosted
version: "1.0.4"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
name: http_parser name: http_parser
url: "https://pub.dartlang.org" sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185
url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
intl: intl:
dependency: transitive dependency: transitive
description: description:
name: intl name: intl
url: "https://pub.dartlang.org" sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
url: "https://pub.dev"
source: hosted source: hosted
version: "0.17.0" version: "0.17.0"
isolates: isolates:
dependency: transitive dependency: transitive
description: description:
name: isolates name: isolates
url: "https://pub.dartlang.org" sha256: ce89e4141b27b877326d3715be2dceac7a7ba89f3229785816d2d318a75ddf28
url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3+8" version: "3.0.3+8"
js:
dependency: transitive
description:
name: js
sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174
url: "https://pub.dev"
source: hosted
version: "0.6.3"
kdbx: kdbx:
dependency: "direct main" dependency: "direct main"
description: description:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "2.0.0" version: "2.4.0"
lints:
dependency: transitive
description:
name: lints
sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c
url: "https://pub.dev"
source: hosted
version: "1.0.1"
logging: logging:
dependency: transitive dependency: transitive
description: description:
name: logging name: logging
url: "https://pub.dartlang.org" sha256: "0520a4826042a8a5d09ddd4755623a50d37ee536d79a70452aff8c8ad7bb6c27"
url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
logging_appenders: logging_appenders:
dependency: transitive dependency: transitive
description: description:
name: logging_appenders name: logging_appenders
url: "https://pub.dartlang.org" sha256: "013e8548b79e3b8dc0333f3efae706184356b5926c6bea59150efa126c91598c"
url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
url: "https://pub.dartlang.org" sha256: "2e2c34e631f93410daa3ee3410250eadc77ac6befc02a040eda8a123f34e6f5a"
url: "https://pub.dev"
source: hosted
version: "0.12.11"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.10" version: "0.5.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
url: "https://pub.dartlang.org" sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.9.1"
path: path:
dependency: transitive dependency: transitive
description: description:
name: path name: path
url: "https://pub.dartlang.org" sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb"
url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.0" version: "1.8.0"
pedantic:
dependency: "direct dev"
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
url: "https://pub.dartlang.org" sha256: "3abc4a0f06dccb2348ebdab9f5b9cc88bb64bfc830bed6351040ca42722044a6"
url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.2.0"
pointycastle: pointycastle:
dependency: transitive dependency: transitive
description: description:
name: pointycastle name: pointycastle
url: "https://pub.dartlang.org" sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.7.3"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:
name: quiver name: quiver
url: "https://pub.dartlang.org" sha256: "5e592c348a6c528fb8deb7cc7d85a7097ce65bf2349121ad004d1fc5d5905eaa"
url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
url: "https://pub.dartlang.org" sha256: d5f89a9e52b36240a80282b3dc0667dd36e53459717bb17b8fb102d30496606a
url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.1" version: "1.8.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
url: "https://pub.dartlang.org" sha256: f8d9f247e2f9f90e32d1495ff32dac7e4ae34ffa7194c5ff8fcc0fd0e52df774
url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.0" version: "1.10.0"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
url: "https://pub.dartlang.org" sha256: dd11571b8a03f7cadcf91ec26a77e02bfbd6bbba2a512924d3116646b4198fc4
url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
supercharged_dart: supercharged_dart:
dependency: transitive dependency: transitive
description: description:
name: supercharged_dart name: supercharged_dart
url: "https://pub.dartlang.org" sha256: "9d6d4fa1736d07f0506ce2713e5f9815b20bcd741c0d53e9b56c265458c3ce05"
url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
synchronized: synchronized:
dependency: transitive dependency: transitive
description: description:
name: synchronized name: synchronized
url: "https://pub.dartlang.org" sha256: "271977ff1e9e82ceefb4f08424b8839f577c1852e0726b5ce855311b46d3ef83"
url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" sha256: a88162591b02c1f3a3db3af8ce1ea2b374bd75a7bb8d5e353bcfbdc79d719830
url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
name: typed_data name: typed_data
url: "https://pub.dartlang.org" sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee"
url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
name: uuid name: uuid
url: "https://pub.dartlang.org" sha256: "0ea20bfc625477e17f08a92d112272a071609b275ce4ca10ad853e1426ca3758"
url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.4" version: "3.0.4"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
xml: xml:
dependency: transitive dependency: transitive
description: description:
name: xml name: xml
url: "https://pub.dartlang.org" sha256: "925e1d7923773fef2f90c5c9ad0f496630f63e03f974a0aaa5fb50c60640c570"
url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.1" version: "5.2.0"
sdks: sdks:
dart: ">=2.12.0 <3.0.0" dart: ">=3.1.0-185.0.dev <4.0.0"

2
example/pubspec.yaml

@ -12,4 +12,4 @@ dependencies:
path: ../ path: ../
dev_dependencies: dev_dependencies:
pedantic: '>=1.7.0 <2.0.0' flutter_lints: ">=1.0.4 <2.0.0"

23
lib/kdbx.dart

@ -1,6 +1,9 @@
/// dart library for reading keepass file format (kdbx). /// dart library for reading keepass file format (kdbx).
library kdbx; library kdbx;
export 'src/credentials/credentials.dart'
show Credentials, CredentialsPart, HashCredentials, PasswordCredentials;
export 'src/credentials/keyfile.dart' show KeyFileComposite, KeyFileCredentials;
export 'src/crypto/key_encrypter_kdf.dart' export 'src/crypto/key_encrypter_kdf.dart'
show KeyEncrypterKdf, KdfType, KdfField; show KeyEncrypterKdf, KdfType, KdfField;
export 'src/crypto/protected_value.dart' export 'src/crypto/protected_value.dart'
@ -10,25 +13,11 @@ export 'src/kdbx_consts.dart';
export 'src/kdbx_custom_data.dart'; export 'src/kdbx_custom_data.dart';
export 'src/kdbx_dao.dart' show KdbxDao; export 'src/kdbx_dao.dart' show KdbxDao;
export 'src/kdbx_entry.dart' show KdbxEntry, KdbxKey, KdbxKeyCommon; export 'src/kdbx_entry.dart' show KdbxEntry, KdbxKey, KdbxKeyCommon;
export 'src/kdbx_exceptions.dart';
export 'src/kdbx_file.dart'; export 'src/kdbx_file.dart';
export 'src/kdbx_format.dart' export 'src/kdbx_format.dart' show KdbxBody, MergeContext, KdbxFormat;
show
KdbxBody,
Credentials,
CredentialsPart,
HashCredentials,
KdbxFormat,
KeyFileComposite,
KeyFileCredentials,
PasswordCredentials;
export 'src/kdbx_group.dart' show KdbxGroup; export 'src/kdbx_group.dart' show KdbxGroup;
export 'src/kdbx_header.dart' export 'src/kdbx_header.dart' show KdbxVersion;
show
KdbxException,
KdbxInvalidKeyException,
KdbxCorruptedFileException,
KdbxUnsupportedException,
KdbxVersion;
export 'src/kdbx_meta.dart'; export 'src/kdbx_meta.dart';
export 'src/kdbx_object.dart' export 'src/kdbx_object.dart'
show show

43
lib/src/credentials/credentials.dart

@ -0,0 +1,43 @@
import 'dart:typed_data';
import 'package:kdbx/src/credentials/keyfile.dart';
import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/internal/extension_utils.dart';
abstract class CredentialsPart {
Uint8List getBinary();
}
abstract class Credentials {
factory Credentials(ProtectedValue password) =>
Credentials.composite(password, null); //PasswordCredentials(password);
factory Credentials.composite(ProtectedValue? password, Uint8List? keyFile) =>
KeyFileComposite(
password: password?.let((that) => PasswordCredentials(that)),
keyFile: keyFile == null ? null : KeyFileCredentials(keyFile),
);
factory Credentials.fromHash(Uint8List hash) => HashCredentials(hash);
Uint8List getHash();
}
class PasswordCredentials implements CredentialsPart {
PasswordCredentials(this._password);
final ProtectedValue _password;
@override
Uint8List getBinary() {
return _password.hash;
}
}
class HashCredentials implements Credentials {
HashCredentials(this.hash);
final Uint8List hash;
@override
Uint8List getHash() => hash;
}

156
lib/src/credentials/keyfile.dart

@ -0,0 +1,156 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:convert/convert.dart' as convert;
import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/credentials/credentials.dart';
import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/utils/byte_utils.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:xml/xml.dart' as xml;
final _logger = Logger('keyfile');
const _nodeVersion = 'Version';
const _nodeKey = 'Key';
const _nodeData = 'Data';
const _nodeMeta = 'Meta';
const _nodeKeyFile = 'KeyFile';
const _nodeHash = 'Hash';
class KeyFileCredentials implements CredentialsPart {
factory KeyFileCredentials(Uint8List keyFileContents) {
try {
final keyFileAsString = utf8.decode(keyFileContents);
if (_hexValuePattern.hasMatch(keyFileAsString)) {
return KeyFileCredentials._(ProtectedValue.fromBinary(
convert.hex.decode(keyFileAsString) as Uint8List));
}
final xmlContent = xml.XmlDocument.parse(keyFileAsString);
final metaVersion =
xmlContent.findAllElements(_nodeVersion).singleOrNull?.text;
final key = xmlContent.findAllElements(_nodeKey).single;
final dataString = key.findElements(_nodeData).single;
final encoded = dataString.text.replaceAll(RegExp(r'\s'), '');
Uint8List dataBytes;
if (metaVersion != null && metaVersion.startsWith('2.')) {
dataBytes = convert.hex.decode(encoded) as Uint8List;
assert((() {
final hash = dataString.getAttribute(_nodeHash);
if (hash == null) {
throw const FormatException('Keyfile must contain a hash.');
}
final expectedHashBytes = convert.hex.decode(hash);
final actualHash =
crypto.sha256.convert(dataBytes).bytes.sublist(0, 4) as Uint8List;
if (!ByteUtils.eq(expectedHashBytes, actualHash)) {
throw const FormatException(
'Corrupted keyfile. Hash does not match');
}
return true;
})());
} else {
dataBytes = base64.decode(encoded);
}
_logger.finer('Decoded base64 of keyfile.');
return KeyFileCredentials._(ProtectedValue.fromBinary(dataBytes));
} catch (e, stackTrace) {
_logger.warning(
'Unable to parse key file as hex or XML, use as is.', e, stackTrace);
final bytes = crypto.sha256.convert(keyFileContents).bytes as Uint8List;
return KeyFileCredentials._(ProtectedValue.fromBinary(bytes));
}
}
/// Creates a new random (32 bytes) keyfile value.
factory KeyFileCredentials.random() => KeyFileCredentials._(
ProtectedValue.fromBinary(ByteUtils.randomBytes(32)));
factory KeyFileCredentials.fromBytes(Uint8List bytes) =>
KeyFileCredentials._(ProtectedValue.fromBinary(bytes));
KeyFileCredentials._(this._keyFileValue);
static final RegExp _hexValuePattern =
RegExp(r'^[a-f\d]{64}', caseSensitive: false);
final ProtectedValue _keyFileValue;
@override
Uint8List getBinary() {
return _keyFileValue.binaryValue;
// return crypto.sha256.convert(_keyFileValue.binaryValue).bytes as Uint8List;
}
/// Generates a `.keyx` file as described for Keepass keyfile:
/// https://keepass.info/help/base/keys.html#keyfiles
Uint8List toXmlV2() {
return utf8.encode(toXmlV2String()) as Uint8List;
}
/// Generates a `.keyx` file as described for Keepass keyfile:
/// https://keepass.info/help/base/keys.html#keyfiles
@visibleForTesting
String toXmlV2String() {
final hash =
(crypto.sha256.convert(_keyFileValue.binaryValue).bytes as Uint8List)
.sublist(0, 4);
final hashHexString = hexFormatLikeKeepass(convert.hex.encode(hash));
final keyHexString =
hexFormatLikeKeepass(convert.hex.encode(_keyFileValue.binaryValue));
final builder = xml.XmlBuilder()
..processing('xml', 'version="1.0" encoding="utf-8"');
builder.element(_nodeKeyFile, nest: () {
builder.element(_nodeMeta, nest: () {
builder.element(_nodeVersion, nest: () {
builder.text('2.0');
});
});
builder.element(_nodeKey, nest: () {
builder.element(_nodeData, nest: () {
builder.attribute(_nodeHash, hashHexString);
builder.text(keyHexString);
});
});
});
return builder.buildDocument().toXmlString(pretty: true);
}
/// keypass has all-uppercase letters in pairs of 4 bytes (8 characters).
@visibleForTesting
static String hexFormatLikeKeepass(final String hexString) {
final hex = hexString.toUpperCase();
const groups = 8;
final remaining = hex.length % groups;
return [
for (var i = 0; i < hex.length ~/ groups; i++)
hex.substring(i * groups, i * groups + groups),
if (remaining != 0) hex.substring(hex.length - remaining)
].join(' ');
// range(0, hexString.length / 8).map((i) => hexString.substring(i*_groups, i*_groups + _groups));
// hexString.toUpperCase().chara
}
}
class KeyFileComposite implements Credentials {
KeyFileComposite({required this.password, required this.keyFile});
PasswordCredentials? password;
KeyFileCredentials? keyFile;
@override
Uint8List getHash() {
final buffer = [...?password?.getBinary(), ...?keyFile?.getBinary()];
return crypto.sha256.convert(buffer).bytes as Uint8List;
// final output = convert.AccumulatorSink<crypto.Digest>();
// final input = crypto.sha256.startChunkedConversion(output);
//// input.add(password.getHash());
// input.add(buffer);
// input.close();
// return output.events.single.bytes as Uint8List;
}
}

15
lib/src/crypto/key_encrypter_kdf.dart

@ -17,7 +17,7 @@ enum KdfType {
Aes, Aes,
} }
class KdfField<T> { class KdfField<T extends Object> {
KdfField(this.field, this.type); KdfField(this.field, this.type);
final String field; final String field;
@ -76,18 +76,15 @@ class KeyEncrypterKdf {
return KdbxUuid(uuid); return KdbxUuid(uuid);
} }
static KdfType? kdfTypeFor(VarDictionary kdfParameters) { static KdfType kdfTypeFor(VarDictionary kdfParameters) {
final uuid = KdfField.uuid.read(kdfParameters); final uuid = KdfField.uuid.read(kdfParameters);
if (uuid == null) { if (uuid == null) {
throw KdbxCorruptedFileException('No Kdf UUID'); throw KdbxCorruptedFileException('No Kdf UUID');
} }
final kdfUuid = base64.encode(uuid); final kdfUuid = base64.encode(uuid);
try { return kdfUuids[kdfUuid] ??
return kdfUuids[kdfUuid]; (() => throw KdbxCorruptedFileException(
} catch (e) { 'Invalid KDF UUID ${uuid.encodeBase64()}'))();
throw KdbxCorruptedFileException(
'Invalid KDF UUID ${uuid.encodeBase64()}');
}
} }
final Argon2 argon2; final Argon2 argon2;
@ -146,7 +143,7 @@ class KeyEncrypterKdf {
} }
static Uint8List _encryptAesSync(EncryptAesArgs args) { static Uint8List _encryptAesSync(EncryptAesArgs args) {
final cipher = ECBBlockCipher(AESFastEngine()) final cipher = ECBBlockCipher(AESEngine())
..init(true, KeyParameter(args.encryptionKey!)); ..init(true, KeyParameter(args.encryptionKey!));
var out1 = Uint8List.fromList(args.key); var out1 = Uint8List.fromList(args.key);
var out2 = Uint8List(args.key.length); var out2 = Uint8List(args.key.length);

4
lib/src/crypto/protected_value.dart

@ -46,7 +46,7 @@ class ProtectedValue implements StringValue {
return ProtectedValue(_xor(value, salt), salt); return ProtectedValue(_xor(value, salt), salt);
} }
static final random = Random.secure(); static final _random = Random.secure();
final Uint8List _value; final Uint8List _value;
final Uint8List _salt; final Uint8List _salt;
@ -57,7 +57,7 @@ class ProtectedValue implements StringValue {
static Uint8List _randomBytes(int length) { static Uint8List _randomBytes(int length) {
return Uint8List.fromList( return Uint8List.fromList(
List.generate(length, (i) => random.nextInt(0xff))); List.generate(length, (i) => _random.nextInt(0xff)));
} }
static Uint8List _xor(Uint8List a, Uint8List b) { static Uint8List _xor(Uint8List a, Uint8List b) {

2
lib/src/internal/crypto_utils.dart

@ -32,7 +32,7 @@ class AesHelper {
{String mode = CBC_MODE}) { {String mode = CBC_MODE}) {
// Uint8List derivedKey = deriveKey(password); // Uint8List derivedKey = deriveKey(password);
final KeyParameter keyParam = KeyParameter(derivedKey); final KeyParameter keyParam = KeyParameter(derivedKey);
final BlockCipher aes = AESFastEngine(); final BlockCipher aes = AESEngine();
// Uint8List cipherIvBytes = base64.decode(ciphertext); // Uint8List cipherIvBytes = base64.decode(ciphertext);
final Uint8List iv = Uint8List(aes.blockSize) final Uint8List iv = Uint8List(aes.blockSize)

37
lib/src/internal/pointycastle_argon2.dart

@ -0,0 +1,37 @@
import 'dart:typed_data';
import 'package:argon2_ffi_base/argon2_ffi_base.dart';
import 'package:pointycastle/export.dart' as pc;
/// Dart-only implementation using pointycastle's Argon KDF.
class PointyCastleArgon2 extends Argon2 {
const PointyCastleArgon2();
@override
bool get isFfi => false;
@override
bool get isImplemented => true;
pc.KeyDerivator argon2Kdf() => pc.Argon2BytesGenerator();
@override
Uint8List argon2(Argon2Arguments args) {
final kdf = argon2Kdf();
kdf.init(pc.Argon2Parameters(
args.type,
args.salt,
desiredKeyLength: args.length,
iterations: args.iterations,
memory: args.memory,
lanes: args.parallelism,
version: args.version,
));
return kdf.process(args.key);
}
@override
Future<Uint8List> argon2Async(Argon2Arguments args) {
return Future.value(argon2(args));
}
}

28
lib/src/kdbx_binary.dart

@ -9,16 +9,20 @@ import 'package:quiver/core.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
class KdbxBinary { class KdbxBinary {
KdbxBinary({this.isInline, this.isProtected, this.value}); KdbxBinary({
final bool? isInline; required this.isInline,
final bool? isProtected; required this.isProtected,
final Uint8List? value; required this.value,
});
final bool isInline;
final bool isProtected;
final Uint8List value;
int? _valueHashCode; int? _valueHashCode;
static KdbxBinary readBinaryInnerHeader(InnerHeaderField field) { static KdbxBinary readBinaryInnerHeader(InnerHeaderField field) {
final flags = field.bytes![0]; final flags = field.bytes[0];
final isProtected = flags & 0x01 == 0x01; final isProtected = flags & 0x01 == 0x01;
final value = Uint8List.sublistView(field.bytes!, 1); final value = Uint8List.sublistView(field.bytes, 1);
return KdbxBinary( return KdbxBinary(
isInline: false, isInline: false,
isProtected: isProtected, isProtected: isProtected,
@ -26,16 +30,16 @@ class KdbxBinary {
); );
} }
int get valueHashCode => _valueHashCode ??= hashObjects(value!); int get valueHashCode => _valueHashCode ??= hashObjects(value);
bool valueEqual(KdbxBinary other) => bool valueEqual(KdbxBinary other) =>
valueHashCode == other.valueHashCode && ByteUtils.eq(value!, value!); valueHashCode == other.valueHashCode && ByteUtils.eq(value, other.value);
InnerHeaderField writeToInnerHeader() { InnerHeaderField writeToInnerHeader() {
final writer = WriterHelper(); final writer = WriterHelper();
final flags = isProtected! ? 0x01 : 0x00; final flags = isProtected ? 0x01 : 0x00;
writer.writeUint8(flags); writer.writeUint8(flags);
writer.writeBytes(value!); writer.writeBytes(value);
return InnerHeaderField( return InnerHeaderField(
InnerHeaderFields.Binary, writer.output.takeBytes()); InnerHeaderFields.Binary, writer.output.takeBytes());
} }
@ -56,8 +60,8 @@ class KdbxBinary {
} }
void saveToXml(XmlElement valueNode) { void saveToXml(XmlElement valueNode) {
final content = base64.encode(gzip.encode(value!)); final content = base64.encode(gzip.encode(value));
valueNode.addAttributeBool(KdbxXml.ATTR_PROTECTED, isProtected!); valueNode.addAttributeBool(KdbxXml.ATTR_PROTECTED, isProtected);
valueNode.addAttributeBool(KdbxXml.ATTR_COMPRESSED, true); valueNode.addAttributeBool(KdbxXml.ATTR_COMPRESSED, true);
valueNode.children.add(XmlText(content)); valueNode.children.add(XmlText(content));
} }

16
lib/src/kdbx_custom_data.dart

@ -17,7 +17,7 @@ class KdbxCustomData extends KdbxNode {
})), })),
super.read(node); super.read(node);
static const String TAG_NAME = 'CustomData'; static const String TAG_NAME = KdbxXml.NODE_CUSTOM_DATA;
final Map<String, String> _data; final Map<String, String> _data;
@ -43,4 +43,18 @@ class KdbxCustomData extends KdbxNode {
); );
return el; return el;
} }
void merge(KdbxCustomData other, bool otherIsNewer) {
// merge custom data
for (final otherCustomDataEntry in other.entries) {
if (otherIsNewer || !containsKey(otherCustomDataEntry.key)) {
this[otherCustomDataEntry.key] = otherCustomDataEntry.value;
}
}
}
void overwriteFrom(KdbxCustomData other) {
_data.clear();
_data.addAll(other._data);
}
} }

23
lib/src/kdbx_dao.dart

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:kdbx/src/kdbx_entry.dart'; import 'package:kdbx/src/kdbx_entry.dart';
import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_file.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
@ -40,4 +41,26 @@ extension KdbxDao on KdbxFile {
toGroup.addEntry(kdbxObject); toGroup.addEntry(kdbxObject);
} }
} }
void deletePermanently(KdbxObject kdbxObject) {
final parent = kdbxObject.parent;
if (parent == null) {
throw StateError(
'Unable to delete object. Object as no parent, already deleted?');
}
final now = clock.now().toUtc();
if (kdbxObject is KdbxGroup) {
for (final object in kdbxObject.getAllGroupsAndEntries()) {
ctx.addDeletedObject(object.uuid, now);
}
parent.internalRemoveGroup(kdbxObject);
} else if (kdbxObject is KdbxEntry) {
ctx.addDeletedObject(kdbxObject.uuid, now);
parent.internalRemoveEntry(kdbxObject);
} else {
throw StateError('Invalid object type. ${kdbxObject.runtimeType}');
}
kdbxObject.times.locationChanged.set(now);
kdbxObject.internalChangeParent(null);
}
} }

9
lib/src/kdbx_deleted_object.dart

@ -1,12 +1,14 @@
import 'package:clock/clock.dart';
import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
class KdbxDeletedObject extends KdbxNode implements KdbxNodeContext { class KdbxDeletedObject extends KdbxNode implements KdbxNodeContext {
KdbxDeletedObject.create(this.ctx, KdbxUuid? uuid) : super.create(NODE_NAME) { KdbxDeletedObject.create(this.ctx, KdbxUuid? uuid, [DateTime? now])
: super.create(NODE_NAME) {
_uuid.set(uuid); _uuid.set(uuid);
deletionTime.setToNow(); deletionTime.set(now ?? clock.now().toUtc());
} }
KdbxDeletedObject.read(XmlElement node, this.ctx) : super.read(node); KdbxDeletedObject.read(XmlElement node, this.ctx) : super.read(node);
@ -16,7 +18,8 @@ class KdbxDeletedObject extends KdbxNode implements KdbxNodeContext {
@override @override
final KdbxReadWriteContext ctx; final KdbxReadWriteContext ctx;
KdbxUuid? get uuid => _uuid.get(); // all objects have to have a UUID.
KdbxUuid get uuid => _uuid.get()!;
UuidNode get _uuid => UuidNode(this, KdbxXml.NODE_UUID); UuidNode get _uuid => UuidNode(this, KdbxXml.NODE_UUID);
DateTimeUtcNode get deletionTime => DateTimeUtcNode(this, 'DeletionTime'); DateTimeUtcNode get deletionTime => DateTimeUtcNode(this, 'DeletionTime');
} }

22
lib/src/kdbx_entry.dart

@ -5,10 +5,11 @@ import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/internal/extension_utils.dart'; import 'package:kdbx/src/internal/extension_utils.dart';
import 'package:kdbx/src/kdbx_binary.dart'; import 'package:kdbx/src/kdbx_binary.dart';
import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_consts.dart';
import 'package:kdbx/src/kdbx_custom_data.dart';
import 'package:kdbx/src/kdbx_exceptions.dart';
import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_file.dart';
import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -56,6 +57,7 @@ bool kdbxKeyCommonAssertConsistency() {
class KdbxKey { class KdbxKey {
KdbxKey(this.key) : _canonicalKey = key.toLowerCase(); KdbxKey(this.key) : _canonicalKey = key.toLowerCase();
const KdbxKey._(this.key, this._canonicalKey); const KdbxKey._(this.key, this._canonicalKey);
const KdbxKey.origin(this.key, this._canonicalKey);
final String key; final String key;
final String _canonicalKey; final String _canonicalKey;
@ -76,7 +78,7 @@ class KdbxKey {
extension KdbxEntryInternal on KdbxEntry { extension KdbxEntryInternal on KdbxEntry {
KdbxEntry cloneInto(KdbxGroup otherGroup, {bool toHistoryEntry = false}) => KdbxEntry cloneInto(KdbxGroup otherGroup, {bool toHistoryEntry = false}) =>
KdbxEntry.create( KdbxEntry.create(
otherGroup.file!, otherGroup.file,
otherGroup, otherGroup,
isHistoryEntry: toHistoryEntry, isHistoryEntry: toHistoryEntry,
) )
@ -127,6 +129,7 @@ extension KdbxEntryInternal on KdbxEntry {
)); ));
_binaries.clear(); _binaries.clear();
_binaries.addAll(newBinaries); _binaries.addAll(newBinaries);
customData.overwriteFrom(other.customData);
times.overwriteFrom(other.times); times.overwriteFrom(other.times);
if (includeHistory) { if (includeHistory) {
for (final historyEntry in other.history) { for (final historyEntry in other.history) {
@ -157,6 +160,7 @@ class KdbxEntry extends KdbxObject {
KdbxGroup parent, { KdbxGroup parent, {
this.isHistoryEntry = false, this.isHistoryEntry = false,
}) : history = [], }) : history = [],
customData = KdbxCustomData.create(),
super.create(file.ctx, file, 'Entry', parent) { super.create(file.ctx, file, 'Entry', parent) {
icon.set(KdbxIcon.Key); icon.set(KdbxIcon.Key);
} }
@ -164,6 +168,10 @@ class KdbxEntry extends KdbxObject {
KdbxEntry.read(KdbxReadWriteContext ctx, KdbxGroup? parent, XmlElement node, KdbxEntry.read(KdbxReadWriteContext ctx, KdbxGroup? parent, XmlElement node,
{this.isHistoryEntry = false}) {this.isHistoryEntry = false})
: history = [], : history = [],
customData = node
.singleElement(KdbxXml.NODE_CUSTOM_DATA)
?.let((e) => KdbxCustomData.read(e)) ??
KdbxCustomData.create(),
super.read(ctx, parent, node) { super.read(ctx, parent, node) {
_strings.addEntries(node.findElements(KdbxXml.NODE_STRING).map((el) { _strings.addEntries(node.findElements(KdbxXml.NODE_STRING).map((el) {
final key = KdbxKey(el.findElements(KdbxXml.NODE_KEY).single.text); final key = KdbxKey(el.findElements(KdbxXml.NODE_KEY).single.text);
@ -210,8 +218,10 @@ class KdbxEntry extends KdbxObject {
StringNode get overrideURL => StringNode(this, 'OverrideURL'); StringNode get overrideURL => StringNode(this, 'OverrideURL');
StringNode get tags => StringNode(this, 'Tags'); StringNode get tags => StringNode(this, 'Tags');
final KdbxCustomData customData;
@override @override
set file(KdbxFile? file) { set file(KdbxFile file) {
super.file = file; super.file = file;
// TODO this looks like some weird workaround, get rid of the // TODO this looks like some weird workaround, get rid of the
// `file` reference. // `file` reference.
@ -233,7 +243,7 @@ class KdbxEntry extends KdbxObject {
@override @override
XmlElement toXml() { XmlElement toXml() {
final el = super.toXml(); final el = super.toXml()..replaceSingle(customData.toXml());
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_STRING); XmlUtils.removeChildrenByName(el, KdbxXml.NODE_STRING);
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_HISTORY); XmlUtils.removeChildrenByName(el, KdbxXml.NODE_HISTORY);
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_BINARY); XmlUtils.removeChildrenByName(el, KdbxXml.NODE_BINARY);
@ -258,7 +268,7 @@ class KdbxEntry extends KdbxObject {
final key = binaryEntry.key; final key = binaryEntry.key;
final binary = binaryEntry.value; final binary = binaryEntry.value;
final value = XmlElement(XmlName(KdbxXml.NODE_VALUE)); final value = XmlElement(XmlName(KdbxXml.NODE_VALUE));
if (binary.isInline!) { if (binary.isInline) {
binary.saveToXml(value); binary.saveToXml(value);
} else { } else {
final binaryIndex = ctx.findBinaryId(binary); final binaryIndex = ctx.findBinaryId(binary);
@ -345,7 +355,7 @@ class KdbxEntry extends KdbxObject {
value: bytes, value: bytes,
); );
modify(() { modify(() {
file!.ctx.addBinary(binary); file.ctx.addBinary(binary);
_binaries[key] = binary; _binaries[key] = binary;
}); });
return binary; return binary;

36
lib/src/kdbx_exceptions.dart

@ -0,0 +1,36 @@
class KdbxException implements Exception {}
class KdbxInvalidKeyException implements KdbxException {}
class KdbxCorruptedFileException implements KdbxException {
KdbxCorruptedFileException([this.message]);
final String? message;
@override
String toString() {
return 'KdbxCorruptedFileException{message: $message}';
}
}
class KdbxUnsupportedException implements KdbxException {
KdbxUnsupportedException(this.hint);
final String hint;
@override
String toString() {
return 'KdbxUnsupportedException{hint: $hint}';
}
}
class KdbxInvalidFileStructure implements KdbxException {
KdbxInvalidFileStructure(this.message);
final String message;
@override
String toString() {
return 'KdbxInvalidFileStructure{$message}';
}
}

40
lib/src/kdbx_file.dart

@ -2,13 +2,16 @@ import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:kdbx/src/credentials/credentials.dart';
import 'package:kdbx/src/crypto/protected_value.dart'; import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_consts.dart';
import 'package:kdbx/src/kdbx_dao.dart'; import 'package:kdbx/src/kdbx_dao.dart';
import 'package:kdbx/src/kdbx_exceptions.dart';
import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/utils/sequence.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:quiver/check.dart'; import 'package:quiver/check.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
@ -16,9 +19,16 @@ import 'package:xml/xml.dart' as xml;
final _logger = Logger('kdbx_file'); final _logger = Logger('kdbx_file');
typedef FileSaveCallback<T> = Future<T> Function(Uint8List bytes);
class KdbxFile { class KdbxFile {
KdbxFile( KdbxFile(
this.ctx, this.kdbxFormat, this.credentials, this.header, this.body) { this.ctx,
this.kdbxFormat,
this._credentials,
this.header,
this.body,
) {
for (final obj in _allObjects) { for (final obj in _allObjects) {
obj.file = this; obj.file = this;
} }
@ -37,7 +47,12 @@ class KdbxFile {
final KdbxFormat kdbxFormat; final KdbxFormat kdbxFormat;
final KdbxReadWriteContext ctx; final KdbxReadWriteContext ctx;
final Credentials credentials; Credentials get credentials => _credentials;
set credentials(Credentials credentials) {
body.meta.modify(() => _credentials = credentials);
}
Credentials _credentials;
final KdbxHeader header; final KdbxHeader header;
final KdbxBody body; final KdbxBody body;
final Set<KdbxObject> dirtyObjects = {}; final Set<KdbxObject> dirtyObjects = {};
@ -53,14 +68,27 @@ class KdbxFile {
Stream<Set<KdbxObject>> get dirtyObjectsChanged => Stream<Set<KdbxObject>> get dirtyObjectsChanged =>
_dirtyObjectsChanged.stream; _dirtyObjectsChanged.stream;
// ignore: prefer_function_declarations_over_variables
static final FileSaveCallback<Uint8List> _saveToBytes = (bytes) async {
return bytes;
};
// @Deprecated('Use [saveTo] instead.')
Future<Uint8List> save() async { Future<Uint8List> save() async {
return kdbxFormat.save(this); return kdbxFormat.save(this, _saveToBytes);
}
Future<T> saveTo<T>(FileSaveCallback<T> saveBytes) {
return kdbxFormat.save(this, saveBytes);
} }
/// Marks all dirty objects as clean. Called by [KdbxFormat.save]. /// Marks all dirty objects as clean. Called by [KdbxFormat.save].
void onSaved() { void onSaved(TimeSequence savedAt) {
dirtyObjects.clear(); final cleanedObjects = dirtyObjects.where((e) => e.clean(savedAt)).toList();
_dirtyObjectsChanged.add(const {});
dirtyObjects.removeAll(cleanedObjects);
_logger.finer('Saved. Remaining dirty objects: ${dirtyObjects.length}');
_dirtyObjectsChanged.add(dirtyObjects);
} }
Iterable<KdbxObject> get _allObjects => body.rootGroup Iterable<KdbxObject> get _allObjects => body.rootGroup

231
lib/src/kdbx_format.dart

@ -6,7 +6,6 @@ import 'dart:typed_data';
import 'package:archive/archive.dart'; import 'package:archive/archive.dart';
import 'package:argon2_ffi_base/argon2_ffi_base.dart'; import 'package:argon2_ffi_base/argon2_ffi_base.dart';
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:convert/convert.dart' as convert;
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/crypto/key_encrypter_kdf.dart'; import 'package:kdbx/src/crypto/key_encrypter_kdf.dart';
@ -14,11 +13,14 @@ import 'package:kdbx/src/crypto/protected_salt_generator.dart';
import 'package:kdbx/src/internal/consts.dart'; import 'package:kdbx/src/internal/consts.dart';
import 'package:kdbx/src/internal/crypto_utils.dart'; import 'package:kdbx/src/internal/crypto_utils.dart';
import 'package:kdbx/src/internal/extension_utils.dart'; import 'package:kdbx/src/internal/extension_utils.dart';
import 'package:kdbx/src/internal/pointycastle_argon2.dart';
import 'package:kdbx/src/kdbx_deleted_object.dart'; import 'package:kdbx/src/kdbx_deleted_object.dart';
import 'package:kdbx/src/kdbx_entry.dart'; import 'package:kdbx/src/kdbx_entry.dart';
import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
import 'package:kdbx/src/utils/byte_utils.dart'; import 'package:kdbx/src/utils/byte_utils.dart';
import 'package:kdbx/src/utils/sequence.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
@ -28,40 +30,6 @@ import 'package:xml/xml.dart' as xml;
final _logger = Logger('kdbx.format'); final _logger = Logger('kdbx.format');
abstract class Credentials {
factory Credentials(ProtectedValue password) =>
Credentials.composite(password, null); //PasswordCredentials(password);
factory Credentials.composite(ProtectedValue? password, Uint8List? keyFile) =>
KeyFileComposite(
password: password?.let((that) => PasswordCredentials(that)),
keyFile: keyFile == null ? null : KeyFileCredentials(keyFile),
);
factory Credentials.fromHash(Uint8List hash) => HashCredentials(hash);
Uint8List getHash();
}
class KeyFileComposite implements Credentials {
KeyFileComposite({required this.password, required this.keyFile});
PasswordCredentials? password;
KeyFileCredentials? keyFile;
@override
Uint8List getHash() {
final buffer = [...?password?.getBinary(), ...?keyFile?.getBinary()];
return crypto.sha256.convert(buffer).bytes as Uint8List;
// final output = convert.AccumulatorSink<crypto.Digest>();
// final input = crypto.sha256.startChunkedConversion(output);
//// input.add(password.getHash());
// input.add(buffer);
// input.close();
// return output.events.single.bytes as Uint8List;
}
}
/// Context used during reading and writing. /// Context used during reading and writing.
class KdbxReadWriteContext { class KdbxReadWriteContext {
KdbxReadWriteContext({ KdbxReadWriteContext({
@ -121,11 +89,11 @@ class KdbxReadWriteContext {
/// finds the ID of the given binary. /// finds the ID of the given binary.
/// if it can't be found, [KdbxCorruptedFileException] is thrown. /// if it can't be found, [KdbxCorruptedFileException] is thrown.
int findBinaryId(KdbxBinary binary) { int findBinaryId(KdbxBinary binary) {
assert(!binary.isInline!); assert(!binary.isInline);
final id = _binaries.indexOf(binary); final id = _binaries.indexOf(binary);
if (id < 0) { if (id < 0) {
throw KdbxCorruptedFileException('Unable to find binary.' throw KdbxCorruptedFileException('Unable to find binary.'
' (${binary.value!.length},${binary.isInline})'); ' (${binary.value.length},${binary.isInline})');
} }
return id; return id;
} }
@ -138,76 +106,12 @@ class KdbxReadWriteContext {
'Tried to remove binary which is not in this file.'); 'Tried to remove binary which is not in this file.');
} }
} }
}
abstract class CredentialsPart {
Uint8List getBinary();
}
class KeyFileCredentials implements CredentialsPart {
factory KeyFileCredentials(Uint8List keyFileContents) {
try {
final keyFileAsString = utf8.decode(keyFileContents);
if (_hexValuePattern.hasMatch(keyFileAsString)) {
return KeyFileCredentials._(ProtectedValue.fromBinary(
convert.hex.decode(keyFileAsString) as Uint8List));
}
final xmlContent = xml.XmlDocument.parse(keyFileAsString);
final metaVersion =
xmlContent.findAllElements('Version').singleOrNull?.text;
final key = xmlContent.findAllElements('Key').single;
final dataString = key.findElements('Data').single;
final encoded = dataString.text.replaceAll(RegExp(r'\s'), '');
Uint8List dataBytes;
if (metaVersion != null && metaVersion.startsWith('2.')) {
dataBytes = convert.hex.decode(encoded) as Uint8List;
} else {
dataBytes = base64.decode(encoded);
}
_logger.finer('Decoded base64 of keyfile.');
return KeyFileCredentials._(ProtectedValue.fromBinary(dataBytes));
} catch (e, stackTrace) {
_logger.warning(
'Unable to parse key file as hex or XML, use as is.', e, stackTrace);
final bytes = crypto.sha256.convert(keyFileContents).bytes as Uint8List;
return KeyFileCredentials._(ProtectedValue.fromBinary(bytes));
}
}
KeyFileCredentials._(this._keyFileValue);
static final RegExp _hexValuePattern =
RegExp(r'^[a-f\d]{64}', caseSensitive: false);
final ProtectedValue _keyFileValue;
@override
Uint8List getBinary() {
return _keyFileValue.binaryValue;
// return crypto.sha256.convert(_keyFileValue.binaryValue).bytes as Uint8List;
}
}
class PasswordCredentials implements CredentialsPart {
PasswordCredentials(this._password);
final ProtectedValue _password; void addDeletedObject(KdbxUuid uuid, [DateTime? now]) {
_deletedObjects.add(KdbxDeletedObject.create(this, uuid));
@override
Uint8List getBinary() {
return _password.hash;
} }
} }
class HashCredentials implements Credentials {
HashCredentials(this.hash);
final Uint8List hash;
@override
Uint8List getHash() => hash;
}
class KdbxBody extends KdbxNode { class KdbxBody extends KdbxNode {
KdbxBody.create(this.meta, this.rootGroup) : super.create('KeePassFile') { KdbxBody.create(this.meta, this.rootGroup) : super.create('KeePassFile') {
node.children.add(meta.node); node.children.add(meta.node);
@ -265,14 +169,14 @@ class KdbxBody extends KdbxNode {
KdbxFile kdbxFile, Uint8List? compressedBytes) async { KdbxFile kdbxFile, Uint8List? compressedBytes) async {
final byteWriter = WriterHelper(); final byteWriter = WriterHelper();
byteWriter.writeBytes( byteWriter.writeBytes(
kdbxFile.header.fields[HeaderFields.StreamStartBytes]!.bytes!); kdbxFile.header.fields[HeaderFields.StreamStartBytes]!.bytes);
HashedBlockReader.writeBlocks(ReaderHelper(compressedBytes), byteWriter); HashedBlockReader.writeBlocks(ReaderHelper(compressedBytes), byteWriter);
final bytes = byteWriter.output.toBytes(); final bytes = byteWriter.output.toBytes();
final masterKey = await KdbxFormat._generateMasterKeyV3( final masterKey = await KdbxFormat._generateMasterKeyV3(
kdbxFile.header, kdbxFile.credentials); kdbxFile.header, kdbxFile.credentials);
final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes, final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes,
kdbxFile.header.fields[HeaderFields.EncryptionIV]!.bytes!); kdbxFile.header.fields[HeaderFields.EncryptionIV]!.bytes);
return encrypted; return encrypted;
} }
@ -326,7 +230,7 @@ class KdbxBody extends KdbxNode {
if (ctx.findBinaryByValue(binary) == null) { if (ctx.findBinaryByValue(binary) == null) {
ctx.addBinary(binary); ctx.addBinary(binary);
mergeContext.trackChange(this, mergeContext.trackChange(this,
debug: 'adding new binary ${binary.value!.length}'); debug: 'adding new binary ${binary.value.length}');
} }
} }
meta.merge(other.meta); meta.merge(other.meta);
@ -334,7 +238,25 @@ class KdbxBody extends KdbxNode {
// remove deleted objects // remove deleted objects
for (final incomingDelete in incomingDeleted.values) { for (final incomingDelete in incomingDeleted.values) {
final object = mergeContext.objectIndex![incomingDelete.uuid!]; final object = mergeContext.objectIndex[incomingDelete.uuid];
if (object == null) {
mergeContext.trackWarning(
'Incoming deleted object not found locally ${incomingDelete.uuid}');
continue;
}
final parent = object.parent;
if (parent == null) {
mergeContext.trackWarning('Unable to delete object $object - '
'already deleted? (${incomingDelete.uuid})');
continue;
}
if (object is KdbxGroup) {
parent.internalRemoveGroup(object);
} else if (object is KdbxEntry) {
parent.internalRemoveEntry(object);
} else {
throw StateError('Invalid object type $object');
}
mergeContext.trackChange(object, debug: 'was deleted.'); mergeContext.trackChange(object, debug: 'was deleted.');
} }
@ -343,11 +265,11 @@ class KdbxBody extends KdbxNode {
_logger.info('Finished merging:\n${mergeContext.debugChanges()}'); _logger.info('Finished merging:\n${mergeContext.debugChanges()}');
final incomingObjects = other._createObjectIndex(); final incomingObjects = other._createObjectIndex();
_logger.info('Merged: ${mergeContext.merged} vs. ' _logger.info('Merged: ${mergeContext.merged} vs. '
'(local objects: ${mergeContext.objectIndex!.length}, ' '(local objects: ${mergeContext.objectIndex.length}, '
'incoming objects: ${incomingObjects.length})'); 'incoming objects: ${incomingObjects.length})');
// sanity checks // sanity checks
if (mergeContext.merged.keys.length != mergeContext.objectIndex!.length) { if (mergeContext.merged.keys.length != mergeContext.objectIndex.length) {
// TODO figure out what went wrong. // TODO figure out what went wrong.
} }
return mergeContext; return mergeContext;
@ -425,12 +347,28 @@ class MergeChange {
} }
} }
class MergeWarning {
MergeWarning(this.debug);
final String debug;
@override
String toString() {
return debug;
}
}
class MergeContext implements OverwriteContext { class MergeContext implements OverwriteContext {
MergeContext({this.objectIndex, this.deletedObjects}); MergeContext({required this.objectIndex, required this.deletedObjects});
final Map<KdbxUuid, KdbxObject>? objectIndex; final Map<KdbxUuid, KdbxObject> objectIndex;
final Map<KdbxUuid?, KdbxDeletedObject>? deletedObjects; final Map<KdbxUuid?, KdbxDeletedObject> deletedObjects;
final Map<KdbxUuid, KdbxObject> merged = {}; final Map<KdbxUuid, KdbxObject> merged = {};
final List<MergeChange> changes = []; final List<MergeChange> changes = [];
final List<MergeWarning> warnings = [];
int totalChanges() {
return deletedObjects.length + changes.length;
}
void markAsMerged(KdbxObject object) { void markAsMerged(KdbxObject object) {
if (merged.containsKey(object.uuid)) { if (merged.containsKey(object.uuid)) {
@ -449,6 +387,11 @@ class MergeContext implements OverwriteContext {
)); ));
} }
void trackWarning(String warning) {
_logger.warning(warning, StackTrace.current);
warnings.add(MergeWarning(warning));
}
String debugChanges() { String debugChanges() {
final group = final group =
changes.groupBy((element) => element.object, valueTransform: (x) => x); changes.groupBy((element) => element.object, valueTransform: (x) => x);
@ -459,6 +402,17 @@ class MergeContext implements OverwriteContext {
].join('\n ')) ].join('\n '))
.join('\n'); .join('\n');
} }
String debugSummary() {
return 'Changes: ${changes.length}, '
'Deleted: ${deletedObjects.length}, '
'Warnings: ${warnings.isEmpty ? 'None' : warnings.join(', ')}';
}
@override
String toString() {
return '$runtimeType{${debugSummary()}}';
}
} }
class _KeysV4 { class _KeysV4 {
@ -469,9 +423,13 @@ class _KeysV4 {
} }
class KdbxFormat { class KdbxFormat {
KdbxFormat([this.argon2]) : assert(kdbxKeyCommonAssertConsistency()); KdbxFormat([Argon2? argon2])
: assert(kdbxKeyCommonAssertConsistency()),
argon2 = argon2 == null || !argon2.isImplemented
? const PointyCastleArgon2()
: argon2;
final Argon2? argon2; final Argon2 argon2;
static bool dartWebWorkaround = false; static bool dartWebWorkaround = false;
/// Creates a new, empty [KdbxFile] with default settings. /// Creates a new, empty [KdbxFile] with default settings.
@ -482,7 +440,7 @@ class KdbxFormat {
String? generator, String? generator,
KdbxHeader? header, KdbxHeader? header,
}) { }) {
header ??= argon2 == null ? KdbxHeader.createV3() : KdbxHeader.createV4(); header ??= KdbxHeader.createV4();
final ctx = KdbxReadWriteContext(binaries: [], header: header); final ctx = KdbxReadWriteContext(binaries: [], header: header);
final meta = KdbxMeta.create( final meta = KdbxMeta.create(
databaseName: name, databaseName: name,
@ -516,10 +474,20 @@ class KdbxFormat {
} }
/// Saves the given file. /// Saves the given file.
Future<Uint8List> save(KdbxFile file) async { Future<T> save<T>(KdbxFile file, FileSaveCallback<T> saveBytes) async {
_logger.finer('Saving ${file.body.rootGroup.uuid} ' _logger.finer('Saving ${file.body.rootGroup.uuid} '
'(locked: ${file.saveLock.locked})'); '(locked: ${file.saveLock.locked})');
return file.saveLock.synchronized(() => _saveSynchronized(file)); return file.saveLock.synchronized(() async {
final savedAt = TimeSequence.now();
final bytes = await _saveSynchronized(file);
_logger.fine('Saving bytes.');
final ret = await saveBytes(bytes);
_logger.fine('Saved bytes.');
file.onSaved(savedAt);
return ret;
});
} }
Future<Uint8List> _saveSynchronized(KdbxFile file) async { Future<Uint8List> _saveSynchronized(KdbxFile file) async {
@ -537,7 +505,7 @@ class KdbxFormat {
throw UnsupportedError('Unsupported version ${header.version}'); throw UnsupportedError('Unsupported version ${header.version}');
} else if (file.header.version < KdbxVersion.V4) { } else if (file.header.version < KdbxVersion.V4) {
final streamKey = final streamKey =
file.header.fields[HeaderFields.ProtectedStreamKey]!.bytes!; file.header.fields[HeaderFields.ProtectedStreamKey]!.bytes;
final gen = ProtectedSaltGenerator(streamKey); final gen = ProtectedSaltGenerator(streamKey);
body.meta.headerHash.set(headerHash.buffer); body.meta.headerHash.set(headerHash.buffer);
@ -553,7 +521,6 @@ class KdbxFormat {
} else { } else {
throw UnsupportedError('Unsupported version ${header.version}'); throw UnsupportedError('Unsupported version ${header.version}');
} }
file.onSaved();
return output.toBytes(); return output.toBytes();
} }
@ -703,7 +670,7 @@ class KdbxFormat {
Uint8List transformContentV4ChaCha20( Uint8List transformContentV4ChaCha20(
KdbxHeader header, Uint8List encrypted, Uint8List cipherKey) { KdbxHeader header, Uint8List encrypted, Uint8List cipherKey) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV]!.bytes!; final encryptionIv = header.fields[HeaderFields.EncryptionIV]!.bytes;
final chaCha = ChaCha7539Engine() final chaCha = ChaCha7539Engine()
..init(true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv)); ..init(true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
return chaCha.process(encrypted); return chaCha.process(encrypted);
@ -726,7 +693,7 @@ class KdbxFormat {
Future<_KeysV4> _computeKeysV4( Future<_KeysV4> _computeKeysV4(
KdbxHeader header, Credentials credentials) async { KdbxHeader header, Credentials credentials) async {
final masterSeed = header.fields[HeaderFields.MasterSeed]!.bytes!; final masterSeed = header.fields[HeaderFields.MasterSeed]!.bytes;
final kdfParameters = header.readKdfParameters; final kdfParameters = header.readKdfParameters;
if (masterSeed.length != 32) { if (masterSeed.length != 32) {
throw const FormatException('Master seed must be 32 bytes.'); throw const FormatException('Master seed must be 32 bytes.');
@ -734,7 +701,7 @@ class KdbxFormat {
final credentialHash = credentials.getHash(); final credentialHash = credentials.getHash();
final key = final key =
await KeyEncrypterKdf(argon2!).encrypt(credentialHash, kdfParameters); await KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters);
// final keyWithSeed = Uint8List(65); // final keyWithSeed = Uint8List(65);
// keyWithSeed.replaceRange(0, masterSeed.length, masterSeed); // keyWithSeed.replaceRange(0, masterSeed.length, masterSeed);
@ -823,14 +790,16 @@ class KdbxFormat {
Uint8List _decryptContent( Uint8List _decryptContent(
KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) { KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV]!.bytes!; final encryptionIv = header.fields[HeaderFields.EncryptionIV]!.bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine()); final decryptCipher = CBCBlockCipher(AESEngine());
decryptCipher.init( decryptCipher.init(
false, ParametersWithIV(KeyParameter(masterKey), encryptionIv)); false, ParametersWithIV(KeyParameter(masterKey), encryptionIv));
_logger.finer('decrypting ${encryptedPayload.length} with block size '
'${decryptCipher.blockSize}');
final paddedDecrypted = final paddedDecrypted =
AesHelper.processBlocks(decryptCipher, encryptedPayload); AesHelper.processBlocks(decryptCipher, encryptedPayload);
final streamStart = header.fields[HeaderFields.StreamStartBytes]!.bytes!; final streamStart = header.fields[HeaderFields.StreamStartBytes]!.bytes;
if (paddedDecrypted.lengthInBytes < streamStart.lengthInBytes) { if (paddedDecrypted.lengthInBytes < streamStart.lengthInBytes) {
_logger.warning( _logger.warning(
@ -852,9 +821,9 @@ class KdbxFormat {
Uint8List _decryptContentV4( Uint8List _decryptContentV4(
KdbxHeader header, Uint8List cipherKey, Uint8List encryptedPayload) { KdbxHeader header, Uint8List cipherKey, Uint8List encryptedPayload) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV]!.bytes!; final encryptionIv = header.fields[HeaderFields.EncryptionIV]!.bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine()); final decryptCipher = CBCBlockCipher(AESEngine());
decryptCipher.init( decryptCipher.init(
false, ParametersWithIV(KeyParameter(cipherKey), encryptionIv)); false, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
final paddedDecrypted = final paddedDecrypted =
@ -867,8 +836,8 @@ class KdbxFormat {
/// TODO combine this with [_decryptContentV4] (or [_encryptDataAes]?) /// TODO combine this with [_decryptContentV4] (or [_encryptDataAes]?)
Uint8List _encryptContentV4Aes( Uint8List _encryptContentV4Aes(
KdbxHeader header, Uint8List cipherKey, Uint8List bytes) { KdbxHeader header, Uint8List cipherKey, Uint8List bytes) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV]!.bytes!; final encryptionIv = header.fields[HeaderFields.EncryptionIV]!.bytes;
final encryptCypher = CBCBlockCipher(AESFastEngine()); final encryptCypher = CBCBlockCipher(AESEngine());
encryptCypher.init( encryptCypher.init(
true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv)); true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
final paddedBytes = AesHelper.pad(bytes, encryptCypher.blockSize); final paddedBytes = AesHelper.pad(bytes, encryptCypher.blockSize);
@ -879,7 +848,7 @@ class KdbxFormat {
KdbxHeader header, Credentials credentials) async { KdbxHeader header, Credentials credentials) async {
final rounds = header.v3KdfTransformRounds; final rounds = header.v3KdfTransformRounds;
final seed = header.fields[HeaderFields.TransformSeed]!.bytes; final seed = header.fields[HeaderFields.TransformSeed]!.bytes;
final masterSeed = header.fields[HeaderFields.MasterSeed]!.bytes!; final masterSeed = header.fields[HeaderFields.MasterSeed]!.bytes;
_logger.finer( _logger.finer(
'Rounds: $rounds (${ByteUtils.toHexList(header.fields[HeaderFields.TransformRounds]!.bytes)})'); 'Rounds: $rounds (${ByteUtils.toHexList(header.fields[HeaderFields.TransformRounds]!.bytes)})');
final transformedKey = await KeyEncrypterKdf.encryptAesAsync( final transformedKey = await KeyEncrypterKdf.encryptAesAsync(
@ -893,7 +862,7 @@ class KdbxFormat {
static Uint8List _encryptDataAes( static Uint8List _encryptDataAes(
Uint8List masterKey, Uint8List payload, Uint8List encryptionIv) { Uint8List masterKey, Uint8List payload, Uint8List encryptionIv) {
final encryptCipher = CBCBlockCipher(AESFastEngine()); final encryptCipher = CBCBlockCipher(AESEngine());
encryptCipher.init( encryptCipher.init(
true, ParametersWithIV(KeyParameter(masterKey), encryptionIv)); true, ParametersWithIV(KeyParameter(masterKey), encryptionIv));
return AesHelper.processBlocks( return AesHelper.processBlocks(

9
lib/src/kdbx_group.dart

@ -57,6 +57,11 @@ class KdbxGroup extends KdbxObject {
List<KdbxEntry> getAllEntries() => List<KdbxEntry> getAllEntries() =>
getAllGroups().expand((g) => g.entries).toList(growable: false); getAllGroups().expand((g) => g.entries).toList(growable: false);
/// Returns all groups and entries. (Including the group itself).
Iterable<KdbxObject> getAllGroupsAndEntries() => <KdbxObject>[this]
.followedBy(entries)
.followedBy(groups.expand((g) => g.getAllGroupsAndEntries()));
List<KdbxGroup> get groups => List.unmodifiable(_groups); List<KdbxGroup> get groups => List.unmodifiable(_groups);
final List<KdbxGroup> _groups = []; final List<KdbxGroup> _groups = [];
@ -144,7 +149,7 @@ class KdbxGroup extends KdbxObject {
if (meObj == null) { if (meObj == null) {
// moved or deleted. // moved or deleted.
final movedObj = mergeContext.objectIndex![otherObj.uuid]; final movedObj = mergeContext.objectIndex[otherObj.uuid];
if (movedObj == null) { if (movedObj == null) {
// item was created in the other file. we have to import it // item was created in the other file. we have to import it
final newMeObject = importToHere(otherObj); final newMeObject = importToHere(otherObj);
@ -154,7 +159,7 @@ class KdbxGroup extends KdbxObject {
// item was moved. // item was moved.
if (otherObj.wasMovedAfter(movedObj)) { if (otherObj.wasMovedAfter(movedObj)) {
// item was moved in the other file, so we have to move it here. // item was moved in the other file, so we have to move it here.
file!.move(movedObj, this); file.move(movedObj, this);
mergeContext.trackChange(movedObj, debug: 'moved to another group'); mergeContext.trackChange(movedObj, debug: 'moved to another group');
} else { } else {
// item was moved in this file, so nothing to do. // item was moved in this file, so nothing to do.

62
lib/src/kdbx_header.dart

@ -4,6 +4,7 @@ import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/crypto/key_encrypter_kdf.dart'; import 'package:kdbx/src/crypto/key_encrypter_kdf.dart';
import 'package:kdbx/src/internal/consts.dart'; import 'package:kdbx/src/internal/consts.dart';
import 'package:kdbx/src/kdbx_binary.dart'; import 'package:kdbx/src/kdbx_binary.dart';
import 'package:kdbx/src/kdbx_exceptions.dart';
import 'package:kdbx/src/kdbx_var_dictionary.dart'; import 'package:kdbx/src/kdbx_var_dictionary.dart';
import 'package:kdbx/src/utils/byte_utils.dart'; import 'package:kdbx/src/utils/byte_utils.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -71,6 +72,7 @@ class KdbxVersion {
static const V3 = KdbxVersion._(3, 0); static const V3 = KdbxVersion._(3, 0);
static const V3_1 = KdbxVersion._(3, 1); static const V3_1 = KdbxVersion._(3, 1);
static const V4 = KdbxVersion._(4, 0); static const V4 = KdbxVersion._(4, 0);
static const V4_1 = KdbxVersion._(4, 1);
final int major; final int major;
final int minor; final int minor;
@ -133,7 +135,7 @@ class HeaderField implements HeaderFieldBase<HeaderFields> {
@override @override
final HeaderFields field; final HeaderFields field;
final Uint8List? bytes; final Uint8List bytes;
String get name => field.toString(); String get name => field.toString();
} }
@ -143,7 +145,7 @@ class InnerHeaderField implements HeaderFieldBase<InnerHeaderFields> {
@override @override
final InnerHeaderFields field; final InnerHeaderFields field;
final Uint8List? bytes; final Uint8List bytes;
String get name => field.toString(); String get name => field.toString();
} }
@ -321,10 +323,10 @@ class KdbxHeader {
void _writeInnerField(WriterHelper writer, InnerHeaderField value) { void _writeInnerField(WriterHelper writer, InnerHeaderField value) {
final field = value.field; final field = value.field;
_logger.finer( _logger.finer(
'Writing header $field (${field.index}) (${value.bytes!.lengthInBytes})'); 'Writing header $field (${field.index}) (${value.bytes.lengthInBytes})');
writer.writeUint8(field.index); writer.writeUint8(field.index);
_writeFieldSize(writer, value.bytes!.lengthInBytes); _writeFieldSize(writer, value.bytes.lengthInBytes);
writer.writeBytes(value.bytes!); writer.writeBytes(value.bytes);
} }
void _writeField(WriterHelper writer, HeaderFields field) { void _writeField(WriterHelper writer, HeaderFields field) {
@ -332,10 +334,10 @@ class KdbxHeader {
if (value == null) { if (value == null) {
return; return;
} }
_logger.finer('Writing header $field (${value.bytes!.lengthInBytes})'); _logger.finer('Writing header $field (${value.bytes.lengthInBytes})');
writer.writeUint8(field.index); writer.writeUint8(field.index);
_writeFieldSize(writer, value.bytes!.lengthInBytes); _writeFieldSize(writer, value.bytes.lengthInBytes);
writer.writeBytes(value.bytes!); writer.writeBytes(value.bytes);
} }
void _writeFieldSize(WriterHelper writer, int size) { void _writeFieldSize(WriterHelper writer, int size) {
@ -384,7 +386,7 @@ class KdbxHeader {
final sig1 = reader.readUint32(); final sig1 = reader.readUint32();
final sig2 = reader.readUint32(); final sig2 = reader.readUint32();
if (!(sig1 == Consts.FileMagic && sig2 == Consts.Sig2Kdbx)) { if (!(sig1 == Consts.FileMagic && sig2 == Consts.Sig2Kdbx)) {
throw UnsupportedError( throw KdbxInvalidFileStructure(
'Unsupported file structure. ${ByteUtils.toHex(sig1)}, ' 'Unsupported file structure. ${ByteUtils.toHex(sig1)}, '
'${ByteUtils.toHex(sig2)}'); '${ByteUtils.toHex(sig2)}');
} }
@ -427,7 +429,7 @@ class KdbxHeader {
ReaderHelper reader, ReaderHelper reader,
KdbxVersion version, KdbxVersion version,
List<TE> fields, List<TE> fields,
T Function(TE field, Uint8List? bytes) createField) => T Function(TE field, Uint8List bytes) createField) =>
Map<TE, T>.fromEntries(readField(reader, version, fields, createField) Map<TE, T>.fromEntries(readField(reader, version, fields, createField)
.map((field) => MapEntry(field.field, field))); .map((field) => MapEntry(field.field, field)));
@ -435,12 +437,13 @@ class KdbxHeader {
ReaderHelper reader, ReaderHelper reader,
KdbxVersion version, KdbxVersion version,
List<TE> fields, List<TE> fields,
T Function(TE field, Uint8List? bytes) createField) sync* { T Function(TE field, Uint8List bytes) createField) sync* {
while (true) { while (true) {
final headerId = reader.readUint8(); final headerId = reader.readUint8();
final bodySize = final bodySize =
version >= KdbxVersion.V4 ? reader.readUint32() : reader.readUint16(); version >= KdbxVersion.V4 ? reader.readUint32() : reader.readUint16();
final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null; final bodyBytes =
bodySize > 0 ? reader.readBytes(bodySize) : Uint8List(0);
// _logger.finer( // _logger.finer(
// 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}'); // 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}');
if (headerId > 0) { if (headerId > 0) {
@ -476,22 +479,21 @@ class KdbxHeader {
Cipher? get cipher { Cipher? get cipher {
if (version < KdbxVersion.V4) { if (version < KdbxVersion.V4) {
assert( assert(
CryptoConsts.cipherFromBytes(fields[HeaderFields.CipherID]!.bytes!) == CryptoConsts.cipherFromBytes(fields[HeaderFields.CipherID]!.bytes) ==
Cipher.aes); Cipher.aes);
return Cipher.aes; return Cipher.aes;
} }
try { try {
return CryptoConsts.cipherFromBytes( return CryptoConsts.cipherFromBytes(fields[HeaderFields.CipherID]!.bytes);
fields[HeaderFields.CipherID]!.bytes!);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_logger.warning( _logger.warning(
'Unable to find cipher. ' 'Unable to find cipher. '
'${fields[HeaderFields.CipherID]?.bytes?.encodeBase64()}', '${fields[HeaderFields.CipherID]?.bytes.encodeBase64()}',
e, e,
stackTrace); stackTrace);
throw KdbxCorruptedFileException( throw KdbxCorruptedFileException(
'Invalid cipher. ' 'Invalid cipher. '
'${fields[HeaderFields.CipherID]?.bytes?.encodeBase64()}', '${fields[HeaderFields.CipherID]?.bytes.encodeBase64()}',
); );
} }
} }
@ -556,32 +558,6 @@ class KdbxHeader {
} }
} }
class KdbxException implements Exception {}
class KdbxInvalidKeyException implements KdbxException {}
class KdbxCorruptedFileException implements KdbxException {
KdbxCorruptedFileException([this.message]);
final String? message;
@override
String toString() {
return 'KdbxCorruptedFileException{message: $message}';
}
}
class KdbxUnsupportedException implements KdbxException {
KdbxUnsupportedException(this.hint);
final String hint;
@override
String toString() {
return 'KdbxUnsupportedException{hint: $hint}';
}
}
class HashedBlockReader { class HashedBlockReader {
static const BLOCK_SIZE = 1024 * 1024; static const BLOCK_SIZE = 1024 * 1024;
static const HASH_SIZE = 32; static const HASH_SIZE = 32;

20
lib/src/kdbx_meta.dart

@ -5,6 +5,7 @@ import 'package:collection/collection.dart';
import 'package:kdbx/src/internal/extension_utils.dart'; import 'package:kdbx/src/internal/extension_utils.dart';
import 'package:kdbx/src/kdbx_binary.dart'; import 'package:kdbx/src/kdbx_binary.dart';
import 'package:kdbx/src/kdbx_custom_data.dart'; import 'package:kdbx/src/kdbx_custom_data.dart';
import 'package:kdbx/src/kdbx_exceptions.dart';
import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
@ -38,7 +39,7 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext {
KdbxMeta.read(xml.XmlElement node, this.ctx) KdbxMeta.read(xml.XmlElement node, this.ctx)
: customData = node : customData = node
.singleElement('CustomData') .singleElement(KdbxXml.NODE_CUSTOM_DATA)
?.let((e) => KdbxCustomData.read(e)) ?? ?.let((e) => KdbxCustomData.read(e)) ??
KdbxCustomData.create(), KdbxCustomData.create(),
binaries = node binaries = node
@ -171,8 +172,8 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext {
XmlElement(XmlName(KdbxXml.NODE_CUSTOM_ICONS)) XmlElement(XmlName(KdbxXml.NODE_CUSTOM_ICONS))
..children.addAll(customIcons.values.map( ..children.addAll(customIcons.values.map(
(e) => XmlUtils.createNode(KdbxXml.NODE_ICON, [ (e) => XmlUtils.createNode(KdbxXml.NODE_ICON, [
XmlUtils.createTextNode(KdbxXml.NODE_UUID, e.uuid!.uuid), XmlUtils.createTextNode(KdbxXml.NODE_UUID, e.uuid.uuid),
XmlUtils.createTextNode(KdbxXml.NODE_DATA, base64.encode(e.data!)) XmlUtils.createTextNode(KdbxXml.NODE_DATA, base64.encode(e.data))
]), ]),
)), )),
); );
@ -205,13 +206,8 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext {
recycleBinChanged.set(other.recycleBinChanged.get()); recycleBinChanged.set(other.recycleBinChanged.get());
} }
final otherIsNewer = other.settingsChanged.isAfter(settingsChanged); final otherIsNewer = other.settingsChanged.isAfter(settingsChanged);
// merge custom data // merge custom data
for (final otherCustomDataEntry in other.customData.entries) { customData.merge(other.customData, otherIsNewer);
if (otherIsNewer || !customData.containsKey(otherCustomDataEntry.key)) {
customData[otherCustomDataEntry.key] = otherCustomDataEntry.value;
}
}
// merge custom icons // merge custom icons
for (final otherCustomIcon in other._customIcons.values) { for (final otherCustomIcon in other._customIcons.values) {
_customIcons[otherCustomIcon.uuid] ??= otherCustomIcon; _customIcons[otherCustomIcon.uuid] ??= otherCustomIcon;
@ -222,11 +218,11 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext {
} }
class KdbxCustomIcon { class KdbxCustomIcon {
KdbxCustomIcon({this.uuid, this.data}); KdbxCustomIcon({required this.uuid, required this.data});
/// uuid of the icon, must be unique within each file. /// uuid of the icon, must be unique within each file.
final KdbxUuid? uuid; final KdbxUuid uuid;
/// Encoded png data of the image. will be base64 encoded into the kdbx file. /// Encoded png data of the image. will be base64 encoded into the kdbx file.
final Uint8List? data; final Uint8List data;
} }

92
lib/src/kdbx_object.dart

@ -10,6 +10,7 @@ import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_meta.dart'; import 'package:kdbx/src/kdbx_meta.dart';
import 'package:kdbx/src/kdbx_times.dart'; import 'package:kdbx/src/kdbx_times.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
import 'package:kdbx/src/utils/sequence.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:quiver/iterables.dart'; import 'package:quiver/iterables.dart';
@ -37,7 +38,7 @@ mixin Changeable<T> {
Stream<ChangeEvent<T>> get changes => _controller.stream; Stream<ChangeEvent<T>> get changes => _controller.stream;
bool _isDirty = false; TimeSequence? _isDirty;
/// allow recursive calls to [modify] /// allow recursive calls to [modify]
bool _isInModify = false; bool _isInModify = false;
@ -54,31 +55,49 @@ mixin Changeable<T> {
@mustCallSuper @mustCallSuper
void onAfterModify() {} void onAfterModify() {}
/// Called after the all modifications
@protected
@mustCallSuper
void onAfterAnyModify() {}
RET modify<RET>(RET Function() modify) { RET modify<RET>(RET Function() modify) {
if (_isDirty || _isInModify) { if (isDirty || _isInModify) {
try {
return modify(); return modify();
} finally {
_isDirty = TimeSequence.now();
onAfterAnyModify();
}
} }
_isInModify = true; _isInModify = true;
onBeforeModify(); onBeforeModify();
try { try {
return modify(); return modify();
} finally { } finally {
_isDirty = true; _isDirty = TimeSequence.now();
_isInModify = false; _isInModify = false;
onAfterModify(); onAfterModify();
_controller.add(ChangeEvent(object: this as T, isDirty: _isDirty)); onAfterAnyModify();
_controller.add(ChangeEvent(object: this as T, isDirty: isDirty));
} }
} }
void clean() { bool clean(TimeSequence savedAt) {
if (!_isDirty) { final dirty = _isDirty;
return; if (dirty == null) {
_logger.warning('clean() called, even though we are not even dirty.');
return false;
} }
_isDirty = false; if (savedAt.isBefore(dirty)) {
_controller.add(ChangeEvent(object: this as T, isDirty: _isDirty)); _logger.fine('We got dirty after save was invoked. so we are not clean.');
return false;
}
_isDirty = null;
_controller.add(ChangeEvent(object: this as T, isDirty: isDirty));
return true;
} }
bool get isDirty => _isDirty; bool get isDirty => _isDirty != null;
} }
abstract class KdbxNodeContext implements KdbxNode { abstract class KdbxNodeContext implements KdbxNode {
@ -87,7 +106,7 @@ abstract class KdbxNodeContext implements KdbxNode {
abstract class KdbxNode with Changeable<KdbxNode> { abstract class KdbxNode with Changeable<KdbxNode> {
KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)) { KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)) {
_isDirty = true; _isDirty = TimeSequence.now();
} }
KdbxNode.read(this.node); KdbxNode.read(this.node);
@ -101,10 +120,8 @@ abstract class KdbxNode with Changeable<KdbxNode> {
// String text(String nodeName) => _opt(nodeName)?.text; // String text(String nodeName) => _opt(nodeName)?.text;
/// must only be called to save this object. /// must only be called to save this object.
/// will mark this object as not dirty.
@mustCallSuper @mustCallSuper
XmlElement toXml() { XmlElement toXml() {
clean();
return node.copy(); return node.copy();
} }
} }
@ -148,7 +165,7 @@ extension KdbxObjectInternal on KdbxObject {
abstract class KdbxObject extends KdbxNode { abstract class KdbxObject extends KdbxNode {
KdbxObject.create( KdbxObject.create(
this.ctx, this.ctx,
this.file, this._file,
String nodeName, String nodeName,
KdbxGroup? parent, KdbxGroup? parent,
) : times = KdbxTimes.create(ctx), ) : times = KdbxTimes.create(ctx),
@ -163,8 +180,11 @@ abstract class KdbxObject extends KdbxNode {
super.read(node); super.read(node);
/// the file this object is part of. will be set AFTER loading, etc. /// the file this object is part of. will be set AFTER loading, etc.
KdbxFile get file => _file!;
set file(KdbxFile file) => _file = file;
/// TODO: We should probably get rid of this `file` reference. /// TODO: We should probably get rid of this `file` reference.
KdbxFile? file; KdbxFile? _file;
final KdbxReadWriteContext ctx; final KdbxReadWriteContext ctx;
@ -182,24 +202,35 @@ abstract class KdbxObject extends KdbxNode {
KdbxGroup? _parent; KdbxGroup? _parent;
late final UuidNode previousParentGroup =
UuidNode(this, 'PreviousParentGroup');
KdbxCustomIcon? get customIcon => KdbxCustomIcon? get customIcon =>
customIconUuid.get()?.let((uuid) => file!.body.meta.customIcons[uuid]); customIconUuid.get()?.let((uuid) => file.body.meta.customIcons[uuid]);
set customIcon(KdbxCustomIcon? icon) { set customIcon(KdbxCustomIcon? icon) {
if (icon != null) { if (icon != null) {
file!.body.meta.addCustomIcon(icon); file.body.meta.addCustomIcon(icon);
customIconUuid.set(icon.uuid); customIconUuid.set(icon.uuid);
} else { } else {
customIconUuid.set(null); customIconUuid.set(null);
} }
} }
// @override
// void onAfterModify() {
// super.onAfterModify();
// times.modifiedNow();
// // during initial `create` the file will be null.
// file?.dirtyObject(this);
// }
@override @override
void onAfterModify() { void onAfterAnyModify() {
super.onAfterModify(); super.onAfterAnyModify();
times.modifiedNow(); times.modifiedNow();
// during initial `create` the file will be null. // during initial `create` the file will be null.
file?.dirtyObject(this); _file?.dirtyObject(this);
} }
bool wasModifiedAfter(KdbxObject other) => times.lastModificationTime bool wasModifiedAfter(KdbxObject other) => times.lastModificationTime
@ -217,11 +248,28 @@ abstract class KdbxObject extends KdbxNode {
return el; return el;
} }
void internalChangeParent(KdbxGroup parent) { @internal
modify(() => _parent = parent); void internalChangeParent(KdbxGroup? parent) {
modify(() {
previousParentGroup.set(_parent?.uuid);
_parent = parent;
});
} }
void merge(MergeContext mergeContext, covariant KdbxObject other); void merge(MergeContext mergeContext, covariant KdbxObject other);
bool isInRecycleBin() {
final targetGroup = file.recycleBin;
if (targetGroup == null) {
return false;
}
return isInGroup(targetGroup);
}
bool isInGroup(KdbxGroup group) {
final parent = this.parent;
return parent != null && (parent == group || parent.isInGroup(group));
}
} }
class KdbxUuid { class KdbxUuid {

14
lib/src/kdbx_var_dictionary.dart

@ -20,37 +20,37 @@ class ValueType<T> {
final Decoder<T> decoder; final Decoder<T> decoder;
final Encoder<T>? encoder; final Encoder<T>? encoder;
static final typeUInt32 = ValueType( static final typeUInt32 = ValueType<int>(
0x04, 0x04,
(reader, _) => reader.readUint32(), (reader, _) => reader.readUint32(),
(writer, value) => writer.writeUint32(value, writer._lengthWriter()), (writer, value) => writer.writeUint32(value, writer._lengthWriter()),
); );
static final typeUInt64 = ValueType( static final typeUInt64 = ValueType<int>(
0x05, 0x05,
(reader, _) => reader.readUint64(), (reader, _) => reader.readUint64(),
(writer, value) => writer.writeUint64(value, writer._lengthWriter()), (writer, value) => writer.writeUint64(value, writer._lengthWriter()),
); );
static final typeBool = ValueType( static final typeBool = ValueType<bool>(
0x08, 0x08,
(reader, _) => reader.readUint8() != 0, (reader, _) => reader.readUint8() != 0,
(writer, value) => writer.writeUint8(value ? 1 : 0, writer._lengthWriter()), (writer, value) => writer.writeUint8(value ? 1 : 0, writer._lengthWriter()),
); );
static final typeInt32 = ValueType( static final typeInt32 = ValueType<int>(
0x0C, 0x0C,
(reader, _) => reader.readInt32(), (reader, _) => reader.readInt32(),
(writer, value) => writer.writeInt32(value, writer._lengthWriter()), (writer, value) => writer.writeInt32(value, writer._lengthWriter()),
); );
static final typeInt64 = ValueType( static final typeInt64 = ValueType<int>(
0x0D, 0x0D,
(reader, _) => reader.readInt64(), (reader, _) => reader.readInt64(),
(writer, value) => writer.writeInt64(value, writer._lengthWriter()), (writer, value) => writer.writeInt64(value, writer._lengthWriter()),
); );
static final typeString = ValueType( static final typeString = ValueType<String>(
0x18, 0x18,
(reader, length) => reader.readString(length), (reader, length) => reader.readString(length),
(writer, value) => writer.writeString(value, writer._lengthWriter()), (writer, value) => writer.writeString(value, writer._lengthWriter()),
); );
static final typeBytes = ValueType( static final typeBytes = ValueType<Uint8List>(
0x42, 0x42,
(reader, length) => reader.readBytes(length), (reader, length) => reader.readBytes(length),
(writer, value) => writer.writeBytes(value, writer._lengthWriter()), (writer, value) => writer.writeBytes(value, writer._lengthWriter()),

4
lib/src/kdbx_xml.dart

@ -4,8 +4,8 @@ import 'dart:typed_data';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_consts.dart';
import 'package:kdbx/src/kdbx_exceptions.dart';
import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/utils/byte_utils.dart'; import 'package:kdbx/src/utils/byte_utils.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -28,7 +28,9 @@ class KdbxXml {
static const ATTR_ID = 'ID'; static const ATTR_ID = 'ID';
static const NODE_BINARY = 'Binary'; static const NODE_BINARY = 'Binary';
static const ATTR_REF = 'Ref'; static const ATTR_REF = 'Ref';
static const NODE_PREVIOUS_PARENT_GROUP = 'PreviousParentGroup';
static const NODE_CUSTOM_ICONS = 'CustomIcons'; static const NODE_CUSTOM_ICONS = 'CustomIcons';
static const NODE_CUSTOM_DATA = 'CustomData';
/// CustomIcons >> Icon /// CustomIcons >> Icon
static const NODE_ICON = 'Icon'; static const NODE_ICON = 'Icon';

12
lib/src/utils/byte_utils.dart

@ -190,16 +190,16 @@ class WriterHelperDartWeb extends WriterHelper {
void writeUint64(int value, [LengthWriter? lengthWriter]) { void writeUint64(int value, [LengthWriter? lengthWriter]) {
lengthWriter?.call(8); lengthWriter?.call(8);
final _endian = Endian.little; const endian = Endian.little;
final highBits = value >> 32; final highBits = value >> 32;
final lowBits = value & mask32; final lowBits = value & mask32;
final byteData = ByteData(8); final byteData = ByteData(8);
if (_endian == Endian.big) { if (endian == Endian.big) {
byteData.setUint32(0, highBits, _endian); byteData.setUint32(0, highBits, endian);
byteData.setUint32(0 + bytesPerWord, lowBits, _endian); byteData.setUint32(0 + bytesPerWord, lowBits, endian);
} else { } else {
byteData.setUint32(0, lowBits, _endian); byteData.setUint32(0, lowBits, endian);
byteData.setUint32(0 + bytesPerWord, highBits, _endian); byteData.setUint32(0 + bytesPerWord, highBits, endian);
} }
_write(byteData); _write(byteData);
} }

7
lib/src/utils/print_utils.dart

@ -20,8 +20,9 @@ class KdbxPrintUtils {
for (final group in group.groups) { for (final group in group.groups) {
catGroup(buf, group, depth: depth + 1); catGroup(buf, group, depth: depth + 1);
} }
final valueToSting = (StringValue? value) => String? valueToSting(StringValue? value) {
forceDecrypt! ? value?.getText() : value?.toString(); return forceDecrypt! ? value?.getText() : value?.toString();
}
for (final entry in group.entries) { for (final entry in group.entries) {
final value = entry.getString(KdbxKeyCommon.PASSWORD); final value = entry.getString(KdbxKeyCommon.PASSWORD);
@ -34,7 +35,7 @@ class KdbxPrintUtils {
.join('\n')); .join('\n'));
} }
buf.writeln(entry.binaryEntries buf.writeln(entry.binaryEntries
.map((b) => '$indent `- file: ${b.key} - ${b.value.value!.length}') .map((b) => '$indent `- file: ${b.key} - ${b.value.value.length}')
.join('\n')); .join('\n'));
} }
} }

28
lib/src/utils/sequence.dart

@ -0,0 +1,28 @@
import 'package:clock/clock.dart';
/// Simple class to assign a unique integer for any point in time.
/// This is basically to ensure that even if two events happen at the
/// same millisecond we know which came first.
/// (realistically this will only make a difference in tests).
class TimeSequence {
TimeSequence._(this._sequenceIndex);
factory TimeSequence.now() => TimeSequence._(_sequenceCounter++);
static int _sequenceCounter = 0;
final int _sequenceIndex;
final DateTime _date = clock.now();
bool isAfter(TimeSequence other) {
return _sequenceIndex > other._sequenceIndex;
}
bool isBefore(TimeSequence other) {
return _sequenceIndex < other._sequenceIndex;
}
@override
String toString() {
return '{Sequence: $_sequenceIndex time: $_date}';
}
}

23
pubspec.yaml

@ -1,25 +1,21 @@
name: kdbx name: kdbx
description: KeepassX format implementation in pure dart. (kdbx 3.x and 4.x support). description: KeepassX format implementation in pure dart. (kdbx 3.x and 4.x support).
version: 2.0.0 version: 2.4.0
homepage: https://github.com/authpass/kdbx.dart homepage: https://github.com/authpass/kdbx.dart
publish_to: none
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <4.0.0'
dependencies: dependencies:
# flutter:
# sdk: flutter
# path: ^1.6.0
logging: '>=0.11.3+2 <2.0.0' logging: '>=0.11.3+2 <2.0.0'
crypto: '>=2.0.0 <4.0.0' crypto: '>=2.0.0 <4.0.0'
pointycastle: '>=3.0.0 <4.0.0' pointycastle: '>=3.4.0 <4.0.0'
xml: '>=4.4.0 <6.0.0' xml: '>=4.4.0 <7.0.0'
uuid: ">=3.0.0 <5.0.0" uuid: ">=3.0.0 <5.0.0"
meta: '>=1.0.0 <2.0.0' meta: '>=1.0.0 <2.0.0'
clock: '>=1.0.0 <2.0.0' clock: '>=1.0.0 <2.0.0'
convert: '>=2.0.0 <4.0.0' convert: '>=2.0.0 <4.0.0'
#isolate: '>=2.0.3 <3.0.0'
# using forked null safety release until it is merged https://github.com/dart-lang/isolate/pull/45
isolates: '>=3.0.0 <4.0.0' isolates: '>=3.0.0 <4.0.0'
path: '>=1.6.0 <2.0.0' path: '>=1.6.0 <2.0.0'
quiver: '>=2.1.0 <4.0.0' quiver: '>=2.1.0 <4.0.0'
@ -27,14 +23,15 @@ dependencies:
supercharged_dart: '>=1.2.0 <4.0.0' supercharged_dart: '>=1.2.0 <4.0.0'
synchronized: '>=2.2.0 <4.0.0' synchronized: '>=2.2.0 <4.0.0'
collection: ^1.15.0-nullsafety.4 collection: '>=1.15.0 <2.0.0'
# required for bin/ # required for bin/
args: '>1.5.0 <3.0.0' args: '>1.5.0 <3.0.0'
logging_appenders: '>=0.1.0 <2.0.0' logging_appenders: '>=0.1.0 <2.0.0'
argon2_ffi_base: ^1.0.0 argon2_ffi_base:
path: ../argon2_ffi_base
dev_dependencies: dev_dependencies:
pedantic: '>=1.7.0 <2.0.0' flutter_lints: '>=2.0.0 <3.0.0'
test: '>=1.6.0 <2.0.0' test: '>=1.6.0 <2.0.0'
fake_async: ^1.1.0 fake_async: ^1.2.0

61
test/deleted_objects_test.dart

@ -1,24 +1,79 @@
@Tags(['kdbx4']) @Tags(['kdbx4'])
import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:xml/xml.dart';
import 'internal/test_utils.dart'; import 'internal/test_utils.dart';
import 'kdbx_test.dart';
final _logger = Logger('deleted_objects_test'); final _logger = Logger('deleted_objects_test');
void main() { void main() {
TestUtil.setupLogging(); final testUtil = TestUtil();
_logger.finest('Running deleted objects tests.'); _logger.finest('Running deleted objects tests.');
group('read tombstones', () { group('read tombstones', () {
test('load/save keeps deleted objects.', () async { test('load/save keeps deleted objects.', () async {
final orig = final orig =
await TestUtil.readKdbxFile('test/test_files/tombstonetest.kdbx'); await testUtil.readKdbxFile('test/test_files/tombstonetest.kdbx');
expect(orig.body.deletedObjects, hasLength(1)); expect(orig.body.deletedObjects, hasLength(1));
final dt = orig.body.deletedObjects.first.deletionTime.get()!; final dt = orig.body.deletedObjects.first.deletionTime.get()!;
expect([dt.year, dt.month, dt.day], [2020, 8, 30]); expect([dt.year, dt.month, dt.day], [2020, 8, 30]);
final reload = await TestUtil.saveAndRead(orig); final reload = await testUtil.saveAndRead(orig);
expect(reload.body.deletedObjects, hasLength(1)); expect(reload.body.deletedObjects, hasLength(1));
}); });
}); });
group('delete to trash', () {
test('move to trash, read previous parent', () {
final file = testUtil.createEmptyFile();
final g = file.body.rootGroup;
final entry = testUtil.createEntry(file, g, 'foo', 'bar');
expect(g.getAllGroupsAndEntries(), hasLength(2));
file.deleteEntry(entry);
// root group, entry and trash group.
expect(g.getAllGroupsAndEntries(), hasLength(3));
expect(entry.previousParentGroup.get(), g.uuid);
});
});
group('delete permanently', () {
test('delete entry', () async {
final file = testUtil.createEmptyFile();
final g = file.body.rootGroup;
final entry = testUtil.createEntry(file, g, 'foo', 'bar');
expect(g.getAllGroupsAndEntries().length, 2);
file.deleteEntry(entry);
// moved into trash bin
expect(g.getAllGroupsAndEntries().length, 3);
// now delete from trash
file.deletePermanently(entry);
expect(g.getAllGroupsAndEntries().length, 2);
final xml = file.body.generateXml(FakeProtectedSaltGenerator());
final objects = xml.findAllElements(KdbxXml.NODE_DELETED_OBJECT);
expect(objects.length, 1);
expect(objects.first.findElements(KdbxXml.NODE_UUID).first.text,
entry.uuid.uuid);
});
test('delete group', () async {
final file = testUtil.createEmptyFile();
final rootGroup = file.body.rootGroup;
final g = file.createGroup(parent: rootGroup, name: 'group');
final objs = [
g,
testUtil.createEntry(file, g, 'foo', 'bar'),
testUtil.createEntry(file, g, 'foo2', 'bar2'),
testUtil.createEntry(file, g, 'foo3', 'bar3'),
];
expect(rootGroup.getAllGroupsAndEntries().length, 5);
file.deletePermanently(g);
expect(rootGroup.getAllGroupsAndEntries().length, 1);
final xml = file.body.generateXml(FakeProtectedSaltGenerator());
final objects = xml.findAllElements(KdbxXml.NODE_DELETED_OBJECT);
expect(objects.length, 4);
expect(objects.map((e) => e.findElements(KdbxXml.NODE_UUID).first.text),
objs.map((o) => o.uuid.uuid));
});
});
} }

4
test/icon/kdbx_customicon_test.dart

@ -3,9 +3,9 @@ import 'package:test/test.dart';
import '../internal/test_utils.dart'; import '../internal/test_utils.dart';
void main() { void main() {
TestUtil.setupLogging(); final testUtil = TestUtil();
test('load custom icons from file', () async { test('load custom icons from file', () async {
final file = await TestUtil.readKdbxFile('test/icon/icontest.kdbx'); final file = await testUtil.readKdbxFile('test/icon/icontest.kdbx');
final entry = file.body.rootGroup.entries.first; final entry = file.body.rootGroup.entries.first;
expect(entry.customIcon!.data, isNotNull); expect(entry.customIcon!.data, isNotNull);
}); });

2
test/internal/byte_utils_test.dart

@ -9,7 +9,7 @@ void main() {
final bytesBuilder = BytesBuilder(); final bytesBuilder = BytesBuilder();
final writer = WriterHelper(bytesBuilder); final writer = WriterHelper(bytesBuilder);
writer.writeUint32(1); writer.writeUint32(1);
print('result: ' + ByteUtils.toHexList(writer.output.toBytes())); print('result: ${ByteUtils.toHexList(writer.output.toBytes())}');
expect(writer.output.toBytes(), hasLength(4)); expect(writer.output.toBytes(), hasLength(4));
}); });
test('uint64', () { test('uint64', () {

39
test/internal/test_utils.dart

@ -10,12 +10,21 @@ import 'package:logging_appenders/logging_appenders.dart';
final _logger = Logger('test_utils'); final _logger = Logger('test_utils');
class TestUtil { class TestUtil {
factory TestUtil() => instance;
TestUtil._() {
setupLogging();
}
static final instance = TestUtil._();
static final keyTitle = KdbxKey('Title'); static final keyTitle = KdbxKey('Title');
static void setupLogging() => static void setupLogging() =>
PrintAppender.setupLogging(stderrLevel: Level.WARNING); PrintAppender.setupLogging(stderrLevel: Level.WARNING);
static KdbxFormat kdbxFormat() { late final kdbxFormat = _kdbxFormat();
static KdbxFormat _kdbxFormat() {
Argon2.resolveLibraryForceDynamic = true; Argon2.resolveLibraryForceDynamic = true;
return KdbxFormat(Argon2FfiFlutter(resolveLibrary: (path) { return KdbxFormat(Argon2FfiFlutter(resolveLibrary: (path) {
final cwd = Directory('.').absolute.uri; final cwd = Directory('.').absolute.uri;
@ -26,42 +35,54 @@ class TestUtil {
})); }));
} }
static Future<KdbxFile> readKdbxFile( Future<KdbxFile> readKdbxFile(
String filePath, { String filePath, {
String password = 'asdf', String password = 'asdf',
}) async { }) async {
final kdbxFormat = TestUtil.kdbxFormat();
final data = await File(filePath).readAsBytes(); final data = await File(filePath).readAsBytes();
final file = await kdbxFormat.read( final file = await kdbxFormat.read(
data, Credentials(ProtectedValue.fromString(password))); data, Credentials(ProtectedValue.fromString(password)));
return file; return file;
} }
static Future<KdbxFile> readKdbxFileBytes(Uint8List data, Future<KdbxFile> readKdbxFileBytes(Uint8List data,
{String password = 'asdf', Credentials? credentials}) async { {String password = 'asdf', Credentials? credentials}) async {
final kdbxFormat = TestUtil.kdbxFormat();
final file = await kdbxFormat.read( final file = await kdbxFormat.read(
data, credentials ?? Credentials(ProtectedValue.fromString(password))); data, credentials ?? Credentials(ProtectedValue.fromString(password)));
return file; return file;
} }
static Future<KdbxFile> saveAndRead(KdbxFile file) async { Future<KdbxFile> saveAndRead(KdbxFile file) async {
return await readKdbxFileBytes(await file.save(), return await readKdbxFileBytes(await file.save(),
credentials: file.credentials); credentials: file.credentials);
} }
static Future<void> saveTestOutput(String name, KdbxFile file) async { Future<void> saveTestOutput(String name, KdbxFile file) async {
final bytes = await file.save(); final bytes = await file.save();
final outFile = File('test_output_$name.kdbx'); final outFile = File('test_output_$name.kdbx');
await outFile.writeAsBytes(bytes); await outFile.writeAsBytes(bytes);
_logger.info('Written to $outFile'); _logger.info('Written to $outFile');
} }
static KdbxFile createEmptyFile() { KdbxFile createEmptyFile() {
final file = kdbxFormat().create( final file = kdbxFormat.create(
Credentials.composite(ProtectedValue.fromString('asdf'), null), Credentials.composite(ProtectedValue.fromString('asdf'), null),
'example'); 'example');
return file; return file;
} }
KdbxEntry createEntry(
KdbxFile file,
KdbxGroup group,
String username,
String password,
) {
final entry = KdbxEntry.create(file, group);
group.addEntry(entry);
entry.setString(KdbxKeyCommon.USER_NAME, PlainValue(username));
entry.setString(
KdbxKeyCommon.PASSWORD, ProtectedValue.fromString(password));
return entry;
}
} }

20
test/kdbx4_test.dart

@ -1,11 +1,9 @@
@Tags(['kdbx4']) @Tags(['kdbx4'])
import 'dart:io'; import 'dart:io';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'internal/test_utils.dart'; import 'internal/test_utils.dart';
@ -15,9 +13,11 @@ final _logger = Logger('kdbx4_test');
// ignore_for_file: non_constant_identifier_names // ignore_for_file: non_constant_identifier_names
void main() { void main() {
Logger.root.level = Level.ALL; final testUtil = TestUtil();
PrintAppender().attachToLogger(Logger.root); final kdbxFormat = testUtil.kdbxFormat;
final kdbxFormat = TestUtil.kdbxFormat(); if (!kdbxFormat.argon2.isFfi) {
throw StateError('Expected ffi!');
}
group('Reading', () { group('Reading', () {
test('bubb', () async { test('bubb', () async {
final data = await File('test/keepassxcpasswords.kdbx').readAsBytes(); final data = await File('test/keepassxcpasswords.kdbx').readAsBytes();
@ -36,7 +36,7 @@ void main() {
expect(pwd, 'def'); expect(pwd, 'def');
}); });
test('Reading kdbx4_keeweb modification time', () async { test('Reading kdbx4_keeweb modification time', () async {
final file = await TestUtil.readKdbxFile('test/kdbx4_keeweb.kdbx'); final file = await testUtil.readKdbxFile('test/kdbx4_keeweb.kdbx');
final firstEntry = file.body.rootGroup.entries.first; final firstEntry = file.body.rootGroup.entries.first;
final createTime = firstEntry.times.creationTime.get(); final createTime = firstEntry.times.creationTime.get();
expect(createTime, DateTime.utc(2020, 2, 26, 13, 40, 48)); expect(createTime, DateTime.utc(2020, 2, 26, 13, 40, 48));
@ -44,13 +44,13 @@ void main() {
expect(modTime, DateTime.utc(2020, 2, 26, 13, 40, 54)); expect(modTime, DateTime.utc(2020, 2, 26, 13, 40, 54));
}); });
test('Change kdbx4 modification time', () async { test('Change kdbx4 modification time', () async {
final file = await TestUtil.readKdbxFile('test/kdbx4_keeweb.kdbx'); final file = await testUtil.readKdbxFile('test/kdbx4_keeweb.kdbx');
final firstEntry = file.body.rootGroup.entries.first; final firstEntry = file.body.rootGroup.entries.first;
final d = DateTime.utc(2020, 4, 5, 10, 0); final d = DateTime.utc(2020, 4, 5, 10, 0);
firstEntry.times.lastModificationTime.set(d); firstEntry.times.lastModificationTime.set(d);
final saved = await file.save(); final saved = await file.save();
{ {
final file2 = await TestUtil.readKdbxFileBytes(saved); final file2 = await testUtil.readKdbxFileBytes(saved);
final firstEntry = file2.body.rootGroup.entries.first; final firstEntry = file2.body.rootGroup.entries.first;
expect(firstEntry.times.lastModificationTime.get(), d); expect(firstEntry.times.lastModificationTime.get(), d);
} }
@ -116,12 +116,12 @@ void main() {
}); });
group('recycle bin test', () { group('recycle bin test', () {
test('empty recycle bin with "zero" uuid', () async { test('empty recycle bin with "zero" uuid', () async {
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx'); final file = await testUtil.readKdbxFile('test/keepass2test.kdbx');
final recycleBin = file.recycleBin; final recycleBin = file.recycleBin;
expect(recycleBin, isNull); expect(recycleBin, isNull);
}); });
test('check deleting item', () async { test('check deleting item', () async {
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx'); final file = await testUtil.readKdbxFile('test/keepass2test.kdbx');
expect(file.recycleBin, isNull); expect(file.recycleBin, isNull);
final entry = file.body.rootGroup.getAllEntries().first; final entry = file.body.rootGroup.getAllEntries().first;
file.deleteEntry(entry); file.deleteEntry(entry);

59
test/kdbx4_test_pointycastle.dart

@ -0,0 +1,59 @@
import 'dart:io';
import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/kdbx_header.dart';
import 'package:logging/logging.dart';
import 'package:test/test.dart';
import 'internal/test_utils.dart';
final _logger = Logger('kdbx4_test_pointycastle');
void main() {
// ignore: unused_local_variable
final testUtil = TestUtil();
final kdbxFormat = KdbxFormat();
if (kdbxFormat.argon2.isFfi) {
throw StateError('Expected non-ffi implementation.');
}
_logger.fine('argon2 implementation: ${kdbxFormat.argon2}');
group('Reading pointycastle argon2', () {
test('pc: Reading kdbx4_keeweb', () async {
final data = await File('test/kdbx4_keeweb.kdbx').readAsBytes();
final file = await kdbxFormat.read(
data, Credentials(ProtectedValue.fromString('asdf')));
final firstEntry = file.body.rootGroup.entries.first;
final pwd = firstEntry.getString(KdbxKeyCommon.PASSWORD)!.getText();
expect(pwd, 'def');
});
});
group('Writing pointycastle argon2', () {
test('Create and save', () async {
final credentials = Credentials(ProtectedValue.fromString('asdf'));
final kdbx = kdbxFormat.create(
credentials,
'Test Keystore',
header: KdbxHeader.createV4(),
);
final rootGroup = kdbx.body.rootGroup;
_createEntry(kdbx, rootGroup, 'user1', 'LoremIpsum');
_createEntry(kdbx, rootGroup, 'user2', 'Second Password');
final saved = await kdbx.save();
final loadedKdbx = await kdbxFormat.read(
saved, Credentials(ProtectedValue.fromString('asdf')));
_logger.fine('Successfully loaded kdbx $loadedKdbx');
File('test_v4x.kdbx').writeAsBytesSync(saved);
});
});
}
KdbxEntry _createEntry(
KdbxFile file, KdbxGroup group, String username, String password) {
final entry = KdbxEntry.create(file, group);
group.addEntry(entry);
entry.setString(KdbxKeyCommon.USER_NAME, PlainValue(username));
entry.setString(KdbxKeyCommon.PASSWORD, ProtectedValue.fromString(password));
return entry;
}

48
test/kdbx_binaries_test.dart

@ -2,8 +2,6 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'internal/test_utils.dart'; import 'internal/test_utils.dart';
@ -18,7 +16,7 @@ void expectBinary(KdbxEntry entry, String key, dynamic matcher) {
Future<void> _testAddNewAttachment(String filePath) async { Future<void> _testAddNewAttachment(String filePath) async {
final saved = await (() async { final saved = await (() async {
final f = await TestUtil.readKdbxFile(filePath); final f = await TestUtil().readKdbxFile(filePath);
final entry = KdbxEntry.create(f, f.body.rootGroup); final entry = KdbxEntry.create(f, f.body.rootGroup);
entry.label = 'addattachment'; entry.label = 'addattachment';
f.body.rootGroup.addEntry(entry); f.body.rootGroup.addEntry(entry);
@ -34,7 +32,7 @@ Future<void> _testAddNewAttachment(String filePath) async {
return await f.save(); return await f.save();
})(); })();
{ {
final file = await TestUtil.readKdbxFileBytes(saved); final file = await TestUtil().readKdbxFileBytes(saved);
final entry = file.body.rootGroup.entries final entry = file.body.rootGroup.entries
.firstWhere((e) => e.label == 'addattachment'); .firstWhere((e) => e.label == 'addattachment');
final binaries = entry.binaryEntries.toList(); final binaries = entry.binaryEntries.toList();
@ -48,8 +46,7 @@ Future<void> _testAddNewAttachment(String filePath) async {
} }
void main() { void main() {
Logger.root.level = Level.ALL; final testUtil = TestUtil();
PrintAppender().attachToLogger(Logger.root);
group('kdbx3 attachment', () { group('kdbx3 attachment', () {
void expectKeepass2binariesContents(KdbxEntry entry) { void expectKeepass2binariesContents(KdbxEntry entry) {
@ -58,10 +55,10 @@ void main() {
for (final binary in binaries) { for (final binary in binaries) {
switch (binary.key.key) { switch (binary.key.key) {
case 'example1.txt': case 'example1.txt':
expect(utf8.decode(binary.value.value!), 'content1 example\n\n'); expect(utf8.decode(binary.value.value), 'content1 example\n\n');
break; break;
case 'example2.txt': case 'example2.txt':
expect(utf8.decode(binary.value.value!), 'content2 example\n\n'); expect(utf8.decode(binary.value.value), 'content2 example\n\n');
break; break;
case 'keepasslogo.jpeg': case 'keepasslogo.jpeg':
expect(binary.value.value, hasLength(7092)); expect(binary.value.value, hasLength(7092));
@ -73,28 +70,29 @@ void main() {
} }
test('read binary', () async { test('read binary', () async {
final file = await TestUtil.readKdbxFile('test/keepass2binaries.kdbx'); final file = await testUtil.readKdbxFile('test/keepass2binaries.kdbx');
final entry = file.body.rootGroup.entries.first; final entry = file.body.rootGroup.entries.first;
expectKeepass2binariesContents(entry); expectKeepass2binariesContents(entry);
}); });
test('read write read', () async { test('read write read', () async {
final fileRead = final fileRead =
await TestUtil.readKdbxFile('test/keepass2binaries.kdbx'); await testUtil.readKdbxFile('test/keepass2binaries.kdbx');
final saved = await fileRead.save(); final saved = await fileRead.save();
final file = await TestUtil.readKdbxFileBytes(saved); final file = await testUtil.readKdbxFileBytes(saved);
final entry = file.body.rootGroup.entries.first; final entry = file.body.rootGroup.entries.first;
expectKeepass2binariesContents(entry); expectKeepass2binariesContents(entry);
}); });
test('modify file with binary in history', () async { test('modify file with binary in history', () async {
final fileRead = final fileRead =
await TestUtil.readKdbxFile('test/keepass2binaries.kdbx'); await testUtil.readKdbxFile('test/keepass2binaries.kdbx');
final updateEntry = (KdbxFile file) { void updateEntry(KdbxFile file) {
final entry = fileRead.body.rootGroup.entries.first; final entry = fileRead.body.rootGroup.entries.first;
entry.setString(KdbxKeyCommon.TITLE, PlainValue('example')); entry.setString(KdbxKeyCommon.TITLE, PlainValue('example'));
}; }
updateEntry(fileRead); updateEntry(fileRead);
final saved = await fileRead.save(); final saved = await fileRead.save();
final file = await TestUtil.readKdbxFileBytes(saved); final file = await testUtil.readKdbxFileBytes(saved);
await file.save(); await file.save();
}); });
test('Add new attachment', () async { test('Add new attachment', () async {
@ -102,7 +100,7 @@ void main() {
}); });
test('Remove attachment', () async { test('Remove attachment', () async {
final saved = await (() async { final saved = await (() async {
final file = await TestUtil.readKdbxFile('test/keepass2binaries.kdbx'); final file = await testUtil.readKdbxFile('test/keepass2binaries.kdbx');
final entry = file.body.rootGroup.entries.first; final entry = file.body.rootGroup.entries.first;
expectKeepass2binariesContents(entry); expectKeepass2binariesContents(entry);
expect(file.ctx.binariesIterable, hasLength(3)); expect(file.ctx.binariesIterable, hasLength(3));
@ -110,7 +108,7 @@ void main() {
expect(file.ctx.binariesIterable, hasLength(3)); expect(file.ctx.binariesIterable, hasLength(3));
return await file.save(); return await file.save();
})(); })();
final file = await TestUtil.readKdbxFileBytes(saved); final file = await testUtil.readKdbxFileBytes(saved);
final entry = file.body.rootGroup.entries.first; final entry = file.body.rootGroup.entries.first;
expect(entry.binaryEntries, hasLength(2)); expect(entry.binaryEntries, hasLength(2));
expect(entry.binaryEntries.map((e) => (e.key.key)), expect(entry.binaryEntries.map((e) => (e.key.key)),
@ -124,12 +122,12 @@ void main() {
}); });
test('keepassxc compatibility', () async { test('keepassxc compatibility', () async {
// keepass has files in arbitrary sort order. // keepass has files in arbitrary sort order.
final file = await TestUtil.readKdbxFile( final file = await testUtil
'test/test_files/binarytest-keepassxc.kdbx'); .readKdbxFile('test/test_files/binarytest-keepassxc.kdbx');
final entry = file.body.rootGroup.entries.first; final entry = file.body.rootGroup.entries.first;
for (final name in ['a', 'b', 'c', 'd', 'e']) { for (final name in ['a', 'b', 'c', 'd', 'e']) {
expect( expect(
utf8.decode(entry.getBinary(KdbxKey('$name.txt'))!.value!).trim(), utf8.decode(entry.getBinary(KdbxKey('$name.txt'))!.value).trim(),
name, name,
); );
} }
@ -138,7 +136,7 @@ void main() {
group('kdbx4 attachment', () { group('kdbx4 attachment', () {
test('read binary', () async { test('read binary', () async {
final file = final file =
await TestUtil.readKdbxFile('test/keepass2kdbx4binaries.kdbx'); await testUtil.readKdbxFile('test/keepass2kdbx4binaries.kdbx');
expect(file.body.rootGroup.entries, hasLength(2)); expect(file.body.rootGroup.entries, hasLength(2));
expectBinary(file.body.rootGroup.entries.first, 'example2.txt', expectBinary(file.body.rootGroup.entries.first, 'example2.txt',
@ -148,9 +146,9 @@ void main() {
}); });
test('read, write, read kdbx4', () async { test('read, write, read kdbx4', () async {
final fileRead = final fileRead =
await TestUtil.readKdbxFile('test/keepass2kdbx4binaries.kdbx'); await testUtil.readKdbxFile('test/keepass2kdbx4binaries.kdbx');
final saved = await fileRead.save(); final saved = await fileRead.save();
final file = await TestUtil.readKdbxFileBytes(saved); final file = await testUtil.readKdbxFileBytes(saved);
expect(file.body.rootGroup.entries, hasLength(2)); expect(file.body.rootGroup.entries, hasLength(2));
expectBinary(file.body.rootGroup.entries.first, 'example2.txt', expectBinary(file.body.rootGroup.entries.first, 'example2.txt',
IsUtf8String('content2 example\n\n')); IsUtf8String('content2 example\n\n'));
@ -160,7 +158,7 @@ void main() {
test('remove attachment kdbx4', () async { test('remove attachment kdbx4', () async {
final saved = await (() async { final saved = await (() async {
final file = final file =
await TestUtil.readKdbxFile('test/keepass2kdbx4binaries.kdbx'); await testUtil.readKdbxFile('test/keepass2kdbx4binaries.kdbx');
final entry = file.body.rootGroup.entries.first; final entry = file.body.rootGroup.entries.first;
expectBinary(file.body.rootGroup.entries.first, 'example2.txt', expectBinary(file.body.rootGroup.entries.first, 'example2.txt',
IsUtf8String('content2 example\n\n')); IsUtf8String('content2 example\n\n'));
@ -173,7 +171,7 @@ void main() {
expect(file.dirtyObjects, [entry]); expect(file.dirtyObjects, [entry]);
return await file.save(); return await file.save();
})(); })();
final file = await TestUtil.readKdbxFileBytes(saved); final file = await testUtil.readKdbxFileBytes(saved);
final entry = file.body.rootGroup.entries.first; final entry = file.body.rootGroup.entries.first;
expect(entry.binaryEntries, hasLength(0)); expect(entry.binaryEntries, hasLength(0));
expectBinary(file.body.rootGroup.entries.last, 'keepasslogo.jpeg', expectBinary(file.body.rootGroup.entries.last, 'keepasslogo.jpeg',

57
test/kdbx_dirty_save_test.dart

@ -0,0 +1,57 @@
import 'package:kdbx/kdbx.dart';
import 'package:test/test.dart';
import 'internal/test_utils.dart';
void main() {
final testUtil = TestUtil();
group('test save with dirty objects', () {
test('modify object after save', () async {
final file = testUtil.createEmptyFile();
final group = file.body.rootGroup;
final entry = testUtil.createEntry(file, group, 'user', 'pass');
final entry2 = testUtil.createEntry(file, group, 'user', 'pass');
await file.save();
const value1 = 'new';
entry.setString(TestUtil.keyTitle, PlainValue(value1));
entry2.setString(TestUtil.keyTitle, PlainValue(value1));
expect(file.isDirty, isTrue);
await file.saveTo((bytes) async {
// must still be dirty as long as we are not finished saving.
expect(file.isDirty, isTrue);
expect(entry.isDirty, isTrue);
expect(entry2.isDirty, isTrue);
return 1;
});
expect(file.isDirty, isFalse);
expect(entry.isDirty, isFalse);
expect(entry2.isDirty, isFalse);
});
test('parallel modify', () async {
final file = testUtil.createEmptyFile();
final group = file.body.rootGroup;
final entry = testUtil.createEntry(file, group, 'user', 'pass');
final entry2 = testUtil.createEntry(file, group, 'user', 'pass');
await file.save();
const value1 = 'new';
const value2 = 'new2';
entry.setString(TestUtil.keyTitle, PlainValue(value2));
entry2.setString(TestUtil.keyTitle, PlainValue(value2));
await file.saveTo((bytes) async {
// must still be dirty as long as we are not finished saving.
expect(file.isDirty, isTrue);
expect(entry.isDirty, isTrue);
expect(entry2.isDirty, isTrue);
entry2.setString(TestUtil.keyTitle, PlainValue(value1));
return 1;
});
expect(file.isDirty, isTrue);
expect(entry.isDirty, isFalse);
expect(entry2.isDirty, isTrue);
});
});
}

8
test/kdbx_history_test.dart

@ -46,10 +46,10 @@ class StreamExpect<T> {
} }
void main() { void main() {
TestUtil.setupLogging(); final testUtil = TestUtil();
group('test history for values', () { group('test history for values', () {
test('check history creation', () async { test('check history creation', () async {
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx'); final file = await testUtil.readKdbxFile('test/keepass2test.kdbx');
const valueOrig = 'Sample Entry'; const valueOrig = 'Sample Entry';
const value1 = 'new'; const value1 = 'new';
const value2 = 'new2'; const value2 = 'new2';
@ -64,7 +64,7 @@ void main() {
} }
expect(file.dirtyObjects, hasLength(1)); expect(file.dirtyObjects, hasLength(1));
final f2 = await dirtyExpect final f2 = await dirtyExpect
.expectNext({}, () async => TestUtil.saveAndRead(file)); .expectNext({}, () async => testUtil.saveAndRead(file));
expect(file.dirtyObjects, isEmpty); expect(file.dirtyObjects, isEmpty);
{ {
final first = f2.body.rootGroup.entries.first; final first = f2.body.rootGroup.entries.first;
@ -81,7 +81,7 @@ void main() {
() async => first.setString(TestUtil.keyTitle, PlainValue(value2))); () async => first.setString(TestUtil.keyTitle, PlainValue(value2)));
} }
final f3 = await dirtyExpect final f3 = await dirtyExpect
.expectNext({}, () async => TestUtil.saveAndRead(file)); .expectNext({}, () async => testUtil.saveAndRead(file));
expect(file.dirtyObjects, isEmpty); expect(file.dirtyObjects, isEmpty);
{ {
final first = f3.body.rootGroup.entries.first; final first = f3.body.rootGroup.entries.first;

16
test/kdbx_test.dart

@ -5,7 +5,6 @@ import 'dart:typed_data';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/crypto/protected_salt_generator.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -22,9 +21,8 @@ class FakeProtectedSaltGenerator implements ProtectedSaltGenerator {
} }
void main() { void main() {
Logger.root.level = Level.ALL; final testUtil = TestUtil();
PrintAppender().attachToLogger(Logger.root); final kdbxFormat = testUtil.kdbxFormat;
final kdbxFormat = KdbxFormat();
group('Reading', () { group('Reading', () {
setUp(() {}); setUp(() {});
@ -94,7 +92,7 @@ void main() {
group('times', () { group('times', () {
test('read mod date time', () async { test('read mod date time', () async {
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx'); final file = await testUtil.readKdbxFile('test/keepass2test.kdbx');
final first = file.body.rootGroup.entries.first; final first = file.body.rootGroup.entries.first;
expect(file.header.version.major, 3); expect(file.header.version.major, 3);
expect(first.getString(KdbxKeyCommon.TITLE)!.getText(), 'Sample Entry'); expect(first.getString(KdbxKeyCommon.TITLE)!.getText(), 'Sample Entry');
@ -103,7 +101,7 @@ void main() {
}); });
test('update mod date time', () async { test('update mod date time', () async {
final newModDate = DateTime.utc(2020, 1, 2, 3, 4, 5); final newModDate = DateTime.utc(2020, 1, 2, 3, 4, 5);
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx'); final file = await testUtil.readKdbxFile('test/keepass2test.kdbx');
{ {
final first = file.body.rootGroup.entries.first; final first = file.body.rootGroup.entries.first;
expect(file.header.version.major, 3); expect(file.header.version.major, 3);
@ -112,7 +110,7 @@ void main() {
} }
final saved = await file.save(); final saved = await file.save();
{ {
final file = await TestUtil.readKdbxFileBytes(saved); final file = await testUtil.readKdbxFileBytes(saved);
final first = file.body.rootGroup.entries.first; final first = file.body.rootGroup.entries.first;
final modTime = first.times.lastModificationTime.get(); final modTime = first.times.lastModificationTime.get();
expect(modTime, newModDate); expect(modTime, newModDate);
@ -144,7 +142,7 @@ void main() {
File('test.kdbx').writeAsBytesSync(saved); File('test.kdbx').writeAsBytesSync(saved);
}); });
test('concurrent save test', () async { test('concurrent save test', () async {
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx'); final file = await testUtil.readKdbxFile('test/keepass2test.kdbx');
final readLock = Lock(); final readLock = Lock();
Future<KdbxFile> doSave( Future<KdbxFile> doSave(
Future<Uint8List> byteFuture, String debug) async { Future<Uint8List> byteFuture, String debug) async {
@ -152,7 +150,7 @@ void main() {
final bytes = await byteFuture; final bytes = await byteFuture;
return await readLock.synchronized(() { return await readLock.synchronized(() {
try { try {
final ret = TestUtil.readKdbxFileBytes(bytes); final ret = testUtil.readKdbxFileBytes(bytes);
_logger.fine('$debug FINISHED: success'); _logger.fine('$debug FINISHED: success');
return ret; return ret;
} catch (e, stackTrace) { } catch (e, stackTrace) {

10
test/kdbx_upgrade_test.dart

@ -6,17 +6,17 @@ import 'package:test/test.dart';
import 'internal/test_utils.dart'; import 'internal/test_utils.dart';
void main() { void main() {
TestUtil.setupLogging(); final testUtil = TestUtil();
group('Test upgrade from v3 to v4', () { group('Test upgrade from v3 to v4', () {
final format = TestUtil.kdbxFormat(); final format = testUtil.kdbxFormat;
test('Read v3, write v4', () async { test('Read v3, write v4', () async {
final file = final file =
await TestUtil.readKdbxFile('test/FooBar.kdbx', password: 'FooBar'); await testUtil.readKdbxFile('test/FooBar.kdbx', password: 'FooBar');
expect(file.header.version, KdbxVersion.V3_1); expect(file.header.version, KdbxVersion.V3_1);
file.upgrade(KdbxVersion.V4.major); file.upgrade(KdbxVersion.V4.major);
final v4 = await TestUtil.saveAndRead(file); final v4 = await testUtil.saveAndRead(file);
expect(v4.header.version, KdbxVersion.V4); expect(v4.header.version, KdbxVersion.V4);
await TestUtil.saveTestOutput('kdbx4upgrade', v4); await testUtil.saveTestOutput('kdbx4upgrade', v4);
}, tags: 'kdbx3'); }, tags: 'kdbx3');
test('kdbx4 is the new default', () async { test('kdbx4 is the new default', () async {
final file = final file =

45
test/keyfile/keyfile_create_test.dart

@ -0,0 +1,45 @@
import 'dart:typed_data';
import 'package:kdbx/kdbx.dart';
import 'package:logging/logging.dart';
import 'package:quiver/iterables.dart';
import 'package:test/expect.dart';
import 'package:test/scaffolding.dart';
import '../internal/test_utils.dart';
final _logger = Logger('keyfile_create_test');
void main() {
// ignore: unused_local_variable
final testUtils = TestUtil.instance;
final exampleBytes = Uint8List.fromList(
range(0, 16).expand((element) => [0xca, 0xfe]).toList());
group('creating keyfile', () {
test('Create keyfile', () {
final keyFile = KeyFileCredentials.fromBytes(exampleBytes);
final output = keyFile.toXmlV2String();
_logger.info(output);
expect(output, contains('Hash="4CA06E29"'));
expect(output, contains('CAFECAFE CAFECAFE'));
});
test('hex format', () {
final toTest = {
'abcd': 'ABCD',
'abcdefgh': 'ABCDEFGH',
'abcdef': 'ABCDEF',
'1234567812345678': '12345678 12345678',
'12345678123456': '12345678 123456',
};
for (final e in toTest.entries) {
expect(KeyFileCredentials.hexFormatLikeKeepass(e.key), e.value);
}
});
test('create and load', () {
final keyFile = KeyFileCredentials.fromBytes(exampleBytes);
final output = keyFile.toXmlV2();
final read = KeyFileCredentials(output);
expect(read.getBinary(), equals(exampleBytes));
});
});
}

58
test/merge/kdbx_merge_test.dart

@ -1,15 +1,18 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:kdbx/src/utils/print_utils.dart'; import 'package:kdbx/src/utils/print_utils.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:xml/xml.dart';
import '../internal/test_utils.dart'; import '../internal/test_utils.dart';
import '../kdbx_test.dart';
final _logger = Logger('kdbx_merge_test'); final _logger = Logger('kdbx_merge_test');
void main() { void main() {
TestUtil.setupLogging(); final testUtil = TestUtil();
var now = DateTime.fromMillisecondsSinceEpoch(0); var now = DateTime.fromMillisecondsSinceEpoch(0);
final fakeClock = Clock(() => now); final fakeClock = Clock(() => now);
@ -22,18 +25,18 @@ void main() {
}); });
group('Simple merges', () { group('Simple merges', () {
Future<KdbxFile> createSimpleFile() async { Future<KdbxFile> createSimpleFile() async {
final file = TestUtil.createEmptyFile(); final file = testUtil.createEmptyFile();
_createEntry(file, file.body.rootGroup, 'test1', 'test1'); _createEntry(file, file.body.rootGroup, 'test1', 'test1');
final subGroup = final subGroup =
file.createGroup(parent: file.body.rootGroup, name: 'Sub Group'); file.createGroup(parent: file.body.rootGroup, name: 'Sub Group');
_createEntry(file, subGroup, 'test2', 'test2'); _createEntry(file, subGroup, 'test2', 'test2');
proceedSeconds(10); proceedSeconds(10);
return await TestUtil.saveAndRead(file); return await testUtil.saveAndRead(file);
} }
test('Noop merge', () async { test('Noop merge', () async {
final file = await createSimpleFile(); final file = await createSimpleFile();
final file2 = await TestUtil.saveAndRead(file); final file2 = await testUtil.saveAndRead(file);
final merge = file.merge(file2); final merge = file.merge(file2);
final set = Set<KdbxUuid>.from(merge.merged.keys); final set = Set<KdbxUuid>.from(merge.merged.keys);
expect(set, hasLength(4)); expect(set, hasLength(4));
@ -43,15 +46,13 @@ void main() {
await withClock(fakeClock, () async { await withClock(fakeClock, () async {
final file = await createSimpleFile(); final file = await createSimpleFile();
final fileMod = await TestUtil.saveAndRead(file); final fileMod = await testUtil.saveAndRead(file);
fileMod.body.rootGroup.entries.first fileMod.body.rootGroup.entries.first
.setString(KdbxKeyCommon.USER_NAME, PlainValue('changed.')); .setString(KdbxKeyCommon.USER_NAME, PlainValue('changed.'));
_logger.info('mod date: ' + _logger.info('mod date: ${fileMod.body.rootGroup.entries.first.times.lastModificationTime
fileMod.body.rootGroup.entries.first.times.lastModificationTime .get()}');
.get() final file2 = await testUtil.saveAndRead(fileMod);
.toString());
final file2 = await TestUtil.saveAndRead(fileMod);
_logger.info('\n\n\nstarting merge.\n'); _logger.info('\n\n\nstarting merge.\n');
final merge = file.merge(file2); final merge = file.merge(file2);
@ -66,10 +67,10 @@ void main() {
() async => await withClock(fakeClock, () async { () async => await withClock(fakeClock, () async {
final file = await createSimpleFile(); final file = await createSimpleFile();
final fileMod = await TestUtil.saveAndRead(file); final fileMod = await testUtil.saveAndRead(file);
fileMod.body.rootGroup.groups.first.name.set('Sub Group New Name.'); fileMod.body.rootGroup.groups.first.name.set('Sub Group New Name.');
final file2 = await TestUtil.saveAndRead(fileMod); final file2 = await testUtil.saveAndRead(fileMod);
final merge = file.merge(file2); final merge = file.merge(file2);
final set = Set<KdbxUuid>.from(merge.merged.keys); final set = Set<KdbxUuid>.from(merge.merged.keys);
expect(set, hasLength(4)); expect(set, hasLength(4));
@ -81,21 +82,48 @@ void main() {
() async => await withClock(fakeClock, () async { () async => await withClock(fakeClock, () async {
final file = await createSimpleFile(); final file = await createSimpleFile();
final fileMod = await TestUtil.saveAndRead(file); final fileMod = await testUtil.saveAndRead(file);
expect(fileMod.recycleBin, isNull); expect(fileMod.recycleBin, isNull);
fileMod.deleteEntry(fileMod.body.rootGroup.entries.first); fileMod.deleteEntry(fileMod.body.rootGroup.entries.first);
expect(fileMod.recycleBin, isNotNull); expect(fileMod.recycleBin, isNotNull);
final file2 = await TestUtil.saveAndRead(fileMod); final file2 = await testUtil.saveAndRead(fileMod);
final merge = file.merge(file2); final merge = file.merge(file2);
_logger.info('Merged file:\n' _logger.info('Merged file:\n'
'${KdbxPrintUtils().catGroupToString(file.body.rootGroup)}'); '${KdbxPrintUtils().catGroupToString(file.body.rootGroup)}');
final set = Set<KdbxUuid>.from(merge.merged.keys); final set = Set<KdbxUuid>.from(merge.merged.keys);
expect(set, hasLength(5)); expect(set, hasLength(5));
expect(Set<KdbxNode>.from(merge.changes.map<KdbxNode?>((e) => e.object)), expect(
Set<KdbxNode>.from(merge.changes.map<KdbxNode?>((e) => e.object)),
hasLength(2)); hasLength(2));
}), }),
); );
test(
'permanently delete an entry',
() async => await withClock(fakeClock, () async {
final file = await createSimpleFile();
final objCount = file.body.rootGroup.getAllGroupsAndEntries().length;
final fileMod = await testUtil.saveAndRead(file);
final entryDelete = fileMod.body.rootGroup.entries.first;
fileMod.deletePermanently(entryDelete);
expect(fileMod.body.rootGroup.getAllGroupsAndEntries(),
hasLength(objCount - 1));
final file2 = await testUtil.saveAndRead(fileMod);
final merge = file.merge(file2);
_logger.info('Merged file:\n'
'${KdbxPrintUtils().catGroupToString(file.body.rootGroup)}');
expect(merge.deletedObjects, hasLength(1));
expect(
file.body.rootGroup.getAllGroupsAndEntries().length, objCount - 1);
final xml = file.body.generateXml(FakeProtectedSaltGenerator());
final deleted = xml.findAllElements(KdbxXml.NODE_DELETED_OBJECT);
expect(deleted, hasLength(1));
expect(
deleted.first.findAllElements(KdbxXml.NODE_UUID).map((e) => e.text),
[entryDelete.uuid.uuid]);
}),
);
}); });
} }

Loading…
Cancel
Save