From 7111cc3711b16b3dddff231b4ae0a1d99fb5df99 Mon Sep 17 00:00:00 2001 From: Herbert Poul Date: Sun, 30 Aug 2020 13:48:28 +0200 Subject: [PATCH] basic support for DeletedObjects tombstones in the root group. --- example/pubspec.lock | 7 ++++++ example/pubspec.yaml | 3 +++ lib/src/internal/extension_utils.dart | 4 ++++ lib/src/kdbx_deleted_object.dart | 5 ++-- lib/src/kdbx_entry.dart | 5 +--- lib/src/kdbx_format.dart | 33 ++++++++++++++++++++++---- lib/src/kdbx_group.dart | 4 ++-- lib/src/kdbx_xml.dart | 3 +++ test/deleted_objects_test.dart | 24 +++++++++++++++++++ test/test_files/tombstonetest.kdbx | Bin 0 -> 1541 bytes 10 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 test/deleted_objects_test.dart create mode 100644 test/test_files/tombstonetest.kdbx 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 0000000000000000000000000000000000000000..74541dc115ac8e76ed516c12f28cbb8407c02fa0 GIT binary patch literal 1541 zcmV+g2KxB}*`k_f`%AR|00aO65C8xGF~RcYzi~rQzE}kzYW!ON0|Wp70096100bZa z004oRl3_2q!45+pn4HR|?`ag68ku85$^8e0iVX=qS@Q=F0002md_cq9&uzT(J?V(l ziC+&3ivR!s00BY;0000aRaHqu5C8xG?_+J>j44D*k@u;j1LFz|1pxp607(b{00093 z00000000F60000@2mk;800004000001OWg508j(~00062002S(0000}AOHXWnXh4N zG#0664CYt8At{FRQUUgCCZr#!KxvOq>oi341OWg509FJ5000vJ000001ONa44GIkk z%ZTJ5b*cf(m6vp=9hzpZ)xI976#Ps6Fmt54w? z?qvl+qS96w>@~NtdlA5>)^Wj>S)&~L!>i}48$iUQRr3{2Ba+X!YxV>OfCK;l!xZ6o z1z9Xs`m`QmDu#zfnw6Y^g!&PsQX-aO+rrrrO1=$crssr5eGw@shp@w5-9MzbXE#nr zndZNkVlsn2Zj^A`+2oTfqmZEan8oVm?0KBOgS1*-HCg7fJOrfcqQ%=B%zLxk1vz?s z+J3=EDP;@pc5fJ^3>UUmnm`DVd$~HjD6{>fF>?2xw4eTh!@^Q*wicxfub37F!J!m3)1z-|DgYXOO7HylvNW+a=PFeU>G0G;$CL zm`$xf7Y)3mPvr}mXbgd*L-o*GQ*h9w2AQKSTX)oKq5+;5 z^0%dlLxSfXiw536JChuv$TQRebPZrxiz+11ALMc#NkPHHZDfqg@2MSp=S{5u?Z#mU zK9mX7w)66dV}j$c*&{I+qK`c~ouvFB+&TBw0SP6o#^Qs@5zu4yKx#wYh-Vpe?g6M~ z8F(}xm7cuV!8)+c?qcmDyxn!GfW-fHL&rvUoKEq3-(&K)BMC--If*j4QqyA(zIKt+ z@^DAt4d7W3AU7iF)CfZtudhRZc>=K7f7jkq=AxD` zVvA$_BedCb28Nm|PQ4|Eq)%z9EX?QA>O<<8lW$AuF(lQ(nE^%ukw~R80&!5sh1w_T z2?E*fXr0dUPmWU*#5JY;L-UHm!pT;akAfQY7!y+jI7UYmiGpF8B8&IR0b}Va+IOdD zxfiWUC(wS40vMRpAIoGJ&)?56;z13C_twXe>~6MFO*tO0Fc*QMz3F;|rljK+iuEvB zMqUdfxiM<>ECHK9;NGp0Rr8fd;XAji72>FKqAlE;pk_D?1O$g%KTYZK7Y45&!YTFV zThi?NKaSBbddM7`iI`}3_IPPIVE zVXbggAU@{B>rdA3d)pA(LZeegmbR{Kv4@RwpA5RVe|t~*y4zB9hS-aQkAgGvgNI3o z1#t|&FxQ$UZif~xgwJt4*P~|1ey)LMI$c1oq5SQoXZ}AfSCNW(e9r}+$$Vc&q;FlJ za3)b{TSB?9^a)l?pgMkVA*(Ha93DMHgb}8)kE7VGCd9Ik< zkQVF88?sQBK7}_r6vUzNZ>^CZgPL6$9dO31S%4mnt-;wws3b+X#lV90$+sCk># r=i;sj?LJ9z1AzE*Ig6!Uqxl3OB7bj)Sf|B6DA>$Q