diff --git a/example/pubspec.lock b/example/pubspec.lock index 16973cc..dd5c757 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -148,6 +148,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + pedantic: + dependency: "direct dev" + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.2" petitparser: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 287a631..21fa7af 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -10,3 +10,6 @@ dependencies: kdbx: path: ../ + +dev_dependencies: + pedantic: ^1.9.2 diff --git a/lib/src/internal/extension_utils.dart b/lib/src/internal/extension_utils.dart index 0094721..0558dac 100644 --- a/lib/src/internal/extension_utils.dart +++ b/lib/src/internal/extension_utils.dart @@ -33,3 +33,7 @@ extension XmlElementExt on xml.XmlElement { extension ObjectExt on T { R let(R Function(T that) op) => op(this); } + +extension IterableExt on Iterable { + T get singleOrNull => singleWhere((element) => true, orElse: () => null); +} diff --git a/lib/src/kdbx_deleted_object.dart b/lib/src/kdbx_deleted_object.dart index 35593b7..4f95c9d 100644 --- a/lib/src/kdbx_deleted_object.dart +++ b/lib/src/kdbx_deleted_object.dart @@ -3,14 +3,15 @@ import 'package:kdbx/src/kdbx_xml.dart'; import 'package:xml/xml.dart'; class KdbxDeletedObject extends KdbxNode implements KdbxNodeContext { - KdbxDeletedObject.create(this.ctx, KdbxUuid uuid) - : super.create('DeletedObject') { + KdbxDeletedObject.create(this.ctx, KdbxUuid uuid) : super.create(NODE_NAME) { _uuid.set(uuid); deletionTime.setToNow(); } KdbxDeletedObject.read(XmlElement node, this.ctx) : super.read(node); + static const NODE_NAME = KdbxXml.NODE_DELETED_OBJECT; + @override final KdbxReadWriteContext ctx; diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index 8556906..18df046 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/crypto/protected_value.dart'; +import 'package:kdbx/src/internal/extension_utils.dart'; import 'package:kdbx/src/kdbx_binary.dart'; import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_file.dart'; @@ -255,7 +256,3 @@ class KdbxEntry extends KdbxObject { return 'KdbxGroup{uuid=$uuid,name=$label}'; } } - -extension on Iterable { - T get singleOrNull => singleWhere((element) => true, orElse: () => null); -} diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 9a5b10d..0e64114 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -12,6 +12,8 @@ import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/crypto/key_encrypter_kdf.dart'; import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/crypto/protected_value.dart'; +import 'package:kdbx/src/internal/extension_utils.dart'; +import 'package:kdbx/src/kdbx_deleted_object.dart'; import 'package:kdbx/src/utils/byte_utils.dart'; import 'package:kdbx/src/internal/consts.dart'; import 'package:kdbx/src/internal/crypto_utils.dart'; @@ -191,7 +193,9 @@ class HashCredentials implements Credentials { } class KdbxBody extends KdbxNode { - KdbxBody.create(this.meta, this.rootGroup) : super.create('KeePassFile') { + KdbxBody.create(this.meta, this.rootGroup) + : _deletedObjects = [], + super.create('KeePassFile') { node.children.add(meta.node); final rootNode = xml.XmlElement(xml.XmlName('Root')); node.children.add(rootNode); @@ -202,11 +206,17 @@ class KdbxBody extends KdbxNode { xml.XmlElement node, this.meta, this.rootGroup, - ) : super.read(node); + Iterable deletedObjects, + ) : _deletedObjects = List.of(deletedObjects), + super.read(node); // final xml.XmlDocument xmlDocument; final KdbxMeta meta; final KdbxGroup rootGroup; + final List _deletedObjects; + + @visibleForTesting + List get deletedObjects => _deletedObjects; Future writeV3(WriterHelper writer, KdbxFile kdbxFile, ProtectedSaltGenerator saltGenerator) async { @@ -302,7 +312,13 @@ class KdbxBody extends KdbxNode { 'KeePassFile', nest: [ meta.toXml(), - () => builder.element('Root', nest: rootGroupNode), + () => builder.element('Root', nest: [ + rootGroupNode, + XmlUtils.createNode( + KdbxXml.NODE_DELETED_OBJECTS, + _deletedObjects.map((e) => e.toXml()).toList(), + ), + ]), ], ); // final doc = xml.XmlDocument(); @@ -637,9 +653,16 @@ class KdbxFormat { } final rootGroup = - KdbxGroup.read(ctx, null, root.findElements('Group').single); + KdbxGroup.read(ctx, null, root.findElements(KdbxXml.NODE_GROUP).single); + final deletedObjects = root + .findElements(KdbxXml.NODE_DELETED_OBJECTS) + .singleOrNull + ?.let((el) => el + .findElements(KdbxDeletedObject.NODE_NAME) + .map((node) => KdbxDeletedObject.read(node, ctx))) ?? + []; _logger.fine('successfully read Meta.'); - return KdbxBody.read(keePassFile, kdbxMeta, rootGroup); + return KdbxBody.read(keePassFile, kdbxMeta, rootGroup, deletedObjects); } Uint8List _decryptContent( diff --git a/lib/src/kdbx_group.dart b/lib/src/kdbx_group.dart index 9cc7517..acebe51 100644 --- a/lib/src/kdbx_group.dart +++ b/lib/src/kdbx_group.dart @@ -15,7 +15,7 @@ class KdbxGroup extends KdbxObject { : super.create( ctx, parent?.file, - 'Group', + KdbxXml.NODE_GROUP, parent, ) { this.name.set(name); @@ -26,7 +26,7 @@ class KdbxGroup extends KdbxObject { KdbxGroup.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node) : super.read(ctx, parent, node) { node - .findElements('Group') + .findElements(KdbxXml.NODE_GROUP) .map((el) => KdbxGroup.read(ctx, this, el)) .forEach(_groups.add); node diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index aef289b..65aca64 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -14,6 +14,9 @@ class KdbxXml { static const NODE_VALUE = 'Value'; static const ATTR_PROTECTED = 'Protected'; static const ATTR_COMPRESSED = 'Compressed'; + static const NODE_GROUP = 'Group'; + static const NODE_DELETED_OBJECT = 'DeletedObject'; + static const NODE_DELETED_OBJECTS = 'DeletedObjects'; static const NODE_HISTORY = 'History'; static const NODE_BINARIES = 'Binaries'; static const ATTR_ID = 'ID'; diff --git a/test/deleted_objects_test.dart b/test/deleted_objects_test.dart new file mode 100644 index 0000000..35a223e --- /dev/null +++ b/test/deleted_objects_test.dart @@ -0,0 +1,24 @@ +@Tags(['kdbx4']) + +import 'package:logging/logging.dart'; +import 'package:test/test.dart'; + +import 'internal/test_utils.dart'; + +final _logger = Logger('deleted_objects_test'); + +void main() { + TestUtil.setupLogging(); + _logger.finest('Running deleted objects tests.'); + group('read tombstones', () { + test('load/save keeps deleted objects.', () async { + final orig = + await TestUtil.readKdbxFile('test/test_files/tombstonetest.kdbx'); + expect(orig.body.deletedObjects, hasLength(1)); + final dt = orig.body.deletedObjects.first.deletionTime.get(); + expect([dt.year, dt.month, dt.day], [2020, 8, 30]); + final reload = await TestUtil.saveAndRead(orig); + expect(reload.body.deletedObjects, hasLength(1)); + }); + }); +} diff --git a/test/test_files/tombstonetest.kdbx b/test/test_files/tombstonetest.kdbx new file mode 100644 index 0000000..74541dc Binary files /dev/null and b/test/test_files/tombstonetest.kdbx differ