diff --git a/lib/kdbx.dart b/lib/kdbx.dart index 31b55e8..553522f 100644 --- a/lib/kdbx.dart +++ b/lib/kdbx.dart @@ -5,6 +5,7 @@ export 'src/crypto/protected_value.dart' show ProtectedValue, StringValue, PlainValue; export 'src/kdbx_consts.dart'; export 'src/kdbx_custom_data.dart'; +export 'src/kdbx_dao.dart' show KdbxDao; export 'src/kdbx_entry.dart'; export 'src/kdbx_format.dart'; export 'src/kdbx_group.dart'; diff --git a/lib/src/kdbx_dao.dart b/lib/src/kdbx_dao.dart new file mode 100644 index 0000000..dc16362 --- /dev/null +++ b/lib/src/kdbx_dao.dart @@ -0,0 +1,67 @@ +import 'package:kdbx/kdbx.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; + +final _logger = Logger('kdbx_dao'); + +/// Helper object for accessing and modifing data inside +/// a kdbx file. +extension KdbxDao on KdbxFile { + KdbxGroup createGroup({ + @required KdbxGroup parent, + @required String name, + }) { + assert(parent != null, name != null); + final newGroup = KdbxGroup.create(parent: parent, name: name); + parent.addGroup(newGroup); + return newGroup; + } + + KdbxGroup findGroupByUuid(KdbxUuid uuid) => + body.rootGroup.getAllGroups().firstWhere((group) => group.uuid == uuid, + orElse: () => + throw StateError('Unable to find group with uuid $uuid')); + + KdbxGroup _createRecycleBin() { + body.meta.recycleBinEnabled.set(true); + final group = createGroup(parent: body.rootGroup, name: 'Trash'); + group.icon.set(KdbxIcon.TrashBin); + group.enableAutoType.set(false); + group.enableSearching.set(false); + body.meta.recycleBinUUID.set(group.uuid); + return group; + } + + KdbxGroup get recycleBin { + final uuid = body.meta.recycleBinUUID.get(); + if (uuid == null) { + return _createRecycleBin(); + } + _logger.finer(() { + final groupDebug = body.rootGroup + .getAllGroups() + .map((g) => '${g.uuid}: ${g.name}') + .join('\n'); + return 'All Groups: $groupDebug'; + }); + return findGroupByUuid(uuid); + } + + void deleteGroup(KdbxGroup group) { + move(group, recycleBin); + } + + void move(KdbxObject kdbxObject, KdbxGroup toGroup) { + assert(toGroup != null); + kdbxObject.times.locationChanged.setToNow(); + if (kdbxObject is KdbxGroup) { + kdbxObject.parent.internalRemoveGroup(kdbxObject); + kdbxObject.internalChangeParent(toGroup); + toGroup.addGroup(kdbxObject); + } else if (kdbxObject is KdbxEntry) { + kdbxObject.parent.internalRemoveEntry(kdbxObject); + kdbxObject.internalChangeParent(toGroup); + toGroup.addEntry(kdbxObject); + } + } +} diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index 7a430f6..59f773c 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -30,14 +30,15 @@ class KdbxKey { } class KdbxEntry extends KdbxObject { - KdbxEntry.create(KdbxFile file, this.parent) + KdbxEntry.create(KdbxFile file, KdbxGroup parent) : isHistoryEntry = false, - super.create(file, 'Entry') { + super.create(file, 'Entry', parent) { icon.set(KdbxIcon.Key); } - KdbxEntry.read(this.parent, XmlElement node, {this.isHistoryEntry = false}) - : super.read(node) { + KdbxEntry.read(KdbxGroup parent, XmlElement node, + {this.isHistoryEntry = false}) + : super.read(parent, node) { _strings.addEntries(node.findElements(KdbxXml.NODE_STRING).map((el) { final key = KdbxKey(el.findElements(KdbxXml.NODE_KEY).single.text); final valueNode = el.findElements(KdbxXml.NODE_VALUE).single; @@ -111,7 +112,6 @@ class KdbxEntry extends KdbxObject { return el; } - KdbxGroup parent; final Map _strings = {}; // Map get strings => UnmodifiableMapView(_strings); @@ -152,4 +152,9 @@ class KdbxEntry extends KdbxObject { } String get label => _plainValue(KdbxKey('Title')); + + @override + String toString() { + return 'KdbxGroup{uuid=$uuid,name=$label}'; + } } diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 811c785..ccc9a20 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:convert/convert.dart' as convert; import 'package:crypto/crypto.dart' as crypto; +import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/crypto/protected_value.dart'; import 'package:kdbx/src/internal/byte_utils.dart'; @@ -18,7 +19,6 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:pointycastle/export.dart'; import 'package:xml/xml.dart' as xml; -import 'package:kdbx/src/internal/extension_utils.dart'; final _logger = Logger('kdbx.format'); diff --git a/lib/src/kdbx_group.dart b/lib/src/kdbx_group.dart index 341a8e5..7768e24 100644 --- a/lib/src/kdbx_group.dart +++ b/lib/src/kdbx_group.dart @@ -8,18 +8,22 @@ import 'package:xml/xml.dart'; import 'kdbx_object.dart'; class KdbxGroup extends KdbxObject { - KdbxGroup.create({@required this.parent, @required String name}) - : super.create(parent?.file, 'Group') { + KdbxGroup.create({@required KdbxGroup parent, @required String name}) + : super.create( + parent?.file, + 'Group', + parent, + ) { this.name.set(name); icon.set(KdbxIcon.Folder); expanded.set(true); } - KdbxGroup.read(this.parent, XmlElement node) : super.read(node) { + KdbxGroup.read(KdbxGroup parent, XmlElement node) : super.read(parent, node) { node .findElements('Group') .map((el) => KdbxGroup.read(this, el)) - .forEach(groups.add); + .forEach(_groups.add); node .findElements('Entry') .map((el) => KdbxEntry.read(this, el)) @@ -45,9 +49,8 @@ class KdbxGroup extends KdbxObject { List getAllEntries() => getAllGroups().expand((g) => g.entries).toList(growable: false); - /// null if this is the root group. - final KdbxGroup parent; - final List groups = []; + List get groups => List.unmodifiable(_groups); + final List _groups = []; List get entries => List.unmodifiable(_entries); final List _entries = []; @@ -58,7 +61,29 @@ class KdbxGroup extends KdbxObject { 'Invalid operation. Trying to add entry which is already in another group.'); } _entries.add(entry); - node.children.add(entry.node); + isDirty = true; + } + + void addGroup(KdbxGroup group) { + if (group.parent != this) { + throw StateError( + 'Invalid operation. Trying to add group which is already in another group.'); + } + _groups.add(group); + isDirty = true; + } + + void internalRemoveGroup(KdbxGroup group) { + if (!_groups.remove(group)) { + throw StateError('Unable to remove $group from $this (Not found)'); + } + isDirty = true; + } + + void internalRemoveEntry(KdbxEntry entry) { + if (!_entries.remove(entry)) { + throw StateError('Unable to remove $entry from $this (Not found)'); + } isDirty = true; } @@ -66,4 +91,13 @@ class KdbxGroup extends KdbxObject { // String get name => text('Name') ?? ''; BooleanNode get expanded => BooleanNode(this, 'IsExpanded'); + + BooleanNode get enableAutoType => BooleanNode(this, 'EnableAutoType'); + + BooleanNode get enableSearching => BooleanNode(this, 'EnableSearching'); + + @override + String toString() { + return 'KdbxGroup{uuid=$uuid,name=${name.get()}}'; + } } diff --git a/lib/src/kdbx_object.dart b/lib/src/kdbx_object.dart index 070d6ef..860aade 100644 --- a/lib/src/kdbx_object.dart +++ b/lib/src/kdbx_object.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:kdbx/src/kdbx_format.dart'; +import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_times.dart'; import 'package:kdbx/src/kdbx_xml.dart'; import 'package:logging/logging.dart'; @@ -19,6 +20,11 @@ class ChangeEvent { final T object; final bool isDirty; + + @override + String toString() { + return 'ChangeEvent{object: $object, isDirty: $isDirty}'; + } } mixin Changeable { @@ -60,14 +66,16 @@ abstract class KdbxNode with Changeable { } abstract class KdbxObject extends KdbxNode { - KdbxObject.create(this.file, String nodeName) + KdbxObject.create(this.file, String nodeName, KdbxGroup parent) : times = KdbxTimes.create(), + _parent = parent, super.create(nodeName) { _uuid.set(KdbxUuid.random()); } - KdbxObject.read(XmlElement node) + KdbxObject.read(KdbxGroup parent, XmlElement node) : times = KdbxTimes.read(node.findElements('Times').single), + _parent = parent, super.read(node); /// the file this object is part of. will be set AFTER loading, etc. @@ -81,6 +89,10 @@ abstract class KdbxObject extends KdbxNode { IconNode get icon => IconNode(this, 'IconID'); + KdbxGroup get parent => _parent; + + KdbxGroup _parent; + @override set isDirty(bool dirty) { if (dirty) { @@ -99,6 +111,8 @@ abstract class KdbxObject extends KdbxNode { el.children.add(times.toXml()); return el; } + + void internalChangeParent(KdbxGroup parent) => _parent = parent; } class KdbxUuid { @@ -117,4 +131,11 @@ class KdbxUuid { @override String toString() => uuid; + + @override + bool operator ==(Object other) => + identical(this, other) || other is KdbxUuid && uuid == other.uuid; + + @override + int get hashCode => uuid.hashCode; } diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index 7745b40..38c2435 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:clock/clock.dart'; import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/kdbx_consts.dart'; import 'package:meta/meta.dart'; @@ -130,7 +131,7 @@ class BooleanNode extends KdbxSubTextNode { @override bool decode(String value) { - switch (value) { + switch (value?.toLowerCase()) { case 'null': return null; case 'true': @@ -148,6 +149,10 @@ class BooleanNode extends KdbxSubTextNode { class DateTimeUtcNode extends KdbxSubTextNode { DateTimeUtcNode(KdbxNode node, String name) : super(node, name); + void setToNow() { + set(clock.now().toUtc()); + } + @override DateTime decode(String value) => DateTime.parse(value);