Browse Source

basic support for DeletedObjects tombstones in the root group.

pull/3/head
Herbert Poul 4 years ago
parent
commit
7111cc3711
  1. 7
      example/pubspec.lock
  2. 3
      example/pubspec.yaml
  3. 4
      lib/src/internal/extension_utils.dart
  4. 5
      lib/src/kdbx_deleted_object.dart
  5. 5
      lib/src/kdbx_entry.dart
  6. 33
      lib/src/kdbx_format.dart
  7. 4
      lib/src/kdbx_group.dart
  8. 3
      lib/src/kdbx_xml.dart
  9. 24
      test/deleted_objects_test.dart
  10. BIN
      test/test_files/tombstonetest.kdbx

7
example/pubspec.lock

@ -148,6 +148,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.0" version: "1.7.0"
pedantic:
dependency: "direct dev"
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.2"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:

3
example/pubspec.yaml

@ -10,3 +10,6 @@ dependencies:
kdbx: kdbx:
path: ../ path: ../
dev_dependencies:
pedantic: ^1.9.2

4
lib/src/internal/extension_utils.dart

@ -33,3 +33,7 @@ extension XmlElementExt on xml.XmlElement {
extension ObjectExt<T> on T { extension ObjectExt<T> on T {
R let<R>(R Function(T that) op) => op(this); R let<R>(R Function(T that) op) => op(this);
} }
extension IterableExt<T> on Iterable<T> {
T get singleOrNull => singleWhere((element) => true, orElse: () => null);
}

5
lib/src/kdbx_deleted_object.dart

@ -3,14 +3,15 @@ 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) KdbxDeletedObject.create(this.ctx, KdbxUuid uuid) : super.create(NODE_NAME) {
: super.create('DeletedObject') {
_uuid.set(uuid); _uuid.set(uuid);
deletionTime.setToNow(); deletionTime.setToNow();
} }
KdbxDeletedObject.read(XmlElement node, this.ctx) : super.read(node); KdbxDeletedObject.read(XmlElement node, this.ctx) : super.read(node);
static const NODE_NAME = KdbxXml.NODE_DELETED_OBJECT;
@override @override
final KdbxReadWriteContext ctx; final KdbxReadWriteContext ctx;

5
lib/src/kdbx_entry.dart

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/crypto/protected_value.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_binary.dart';
import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_consts.dart';
import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_file.dart';
@ -255,7 +256,3 @@ class KdbxEntry extends KdbxObject {
return 'KdbxGroup{uuid=$uuid,name=$label}'; return 'KdbxGroup{uuid=$uuid,name=$label}';
} }
} }
extension<T> on Iterable<T> {
T get singleOrNull => singleWhere((element) => true, orElse: () => null);
}

33
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/key_encrypter_kdf.dart';
import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/crypto/protected_salt_generator.dart';
import 'package:kdbx/src/crypto/protected_value.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/utils/byte_utils.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';
@ -191,7 +193,9 @@ class HashCredentials implements Credentials {
} }
class KdbxBody extends KdbxNode { 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); node.children.add(meta.node);
final rootNode = xml.XmlElement(xml.XmlName('Root')); final rootNode = xml.XmlElement(xml.XmlName('Root'));
node.children.add(rootNode); node.children.add(rootNode);
@ -202,11 +206,17 @@ class KdbxBody extends KdbxNode {
xml.XmlElement node, xml.XmlElement node,
this.meta, this.meta,
this.rootGroup, this.rootGroup,
) : super.read(node); Iterable<KdbxDeletedObject> deletedObjects,
) : _deletedObjects = List.of(deletedObjects),
super.read(node);
// final xml.XmlDocument xmlDocument; // final xml.XmlDocument xmlDocument;
final KdbxMeta meta; final KdbxMeta meta;
final KdbxGroup rootGroup; final KdbxGroup rootGroup;
final List<KdbxDeletedObject> _deletedObjects;
@visibleForTesting
List<KdbxDeletedObject> get deletedObjects => _deletedObjects;
Future<void> writeV3(WriterHelper writer, KdbxFile kdbxFile, Future<void> writeV3(WriterHelper writer, KdbxFile kdbxFile,
ProtectedSaltGenerator saltGenerator) async { ProtectedSaltGenerator saltGenerator) async {
@ -302,7 +312,13 @@ class KdbxBody extends KdbxNode {
'KeePassFile', 'KeePassFile',
nest: [ nest: [
meta.toXml(), 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(); // final doc = xml.XmlDocument();
@ -637,9 +653,16 @@ class KdbxFormat {
} }
final rootGroup = 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.'); _logger.fine('successfully read Meta.');
return KdbxBody.read(keePassFile, kdbxMeta, rootGroup); return KdbxBody.read(keePassFile, kdbxMeta, rootGroup, deletedObjects);
} }
Uint8List _decryptContent( Uint8List _decryptContent(

4
lib/src/kdbx_group.dart

@ -15,7 +15,7 @@ class KdbxGroup extends KdbxObject {
: super.create( : super.create(
ctx, ctx,
parent?.file, parent?.file,
'Group', KdbxXml.NODE_GROUP,
parent, parent,
) { ) {
this.name.set(name); this.name.set(name);
@ -26,7 +26,7 @@ class KdbxGroup extends KdbxObject {
KdbxGroup.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node) KdbxGroup.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node)
: super.read(ctx, parent, node) { : super.read(ctx, parent, node) {
node node
.findElements('Group') .findElements(KdbxXml.NODE_GROUP)
.map((el) => KdbxGroup.read(ctx, this, el)) .map((el) => KdbxGroup.read(ctx, this, el))
.forEach(_groups.add); .forEach(_groups.add);
node node

3
lib/src/kdbx_xml.dart

@ -14,6 +14,9 @@ class KdbxXml {
static const NODE_VALUE = 'Value'; static const NODE_VALUE = 'Value';
static const ATTR_PROTECTED = 'Protected'; static const ATTR_PROTECTED = 'Protected';
static const ATTR_COMPRESSED = 'Compressed'; 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_HISTORY = 'History';
static const NODE_BINARIES = 'Binaries'; static const NODE_BINARIES = 'Binaries';
static const ATTR_ID = 'ID'; static const ATTR_ID = 'ID';

24
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));
});
});
}

BIN
test/test_files/tombstonetest.kdbx

Binary file not shown.
Loading…
Cancel
Save