diff --git a/lib/kdbx.dart b/lib/kdbx.dart index 5e82379..8b7b849 100644 --- a/lib/kdbx.dart +++ b/lib/kdbx.dart @@ -1,7 +1,7 @@ /// dart library for reading keepass file format (kdbx). library kdbx; -export 'src/crypto/protected_value.dart' show ProtectedValue, StringValue; +export 'src/crypto/protected_value.dart' show ProtectedValue, StringValue, PlainValue; export 'src/kdbx_entry.dart'; export 'src/kdbx_format.dart'; export 'src/kdbx_header.dart' diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index 3635440..8ac97a5 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -3,6 +3,7 @@ import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_object.dart'; +import 'package:kdbx/src/kdbx_xml.dart'; import 'package:xml/xml.dart'; /// Represents a case insensitive (but case preserving) key. @@ -21,15 +22,16 @@ class KdbxKey { } class KdbxEntry extends KdbxObject { - KdbxEntry.create(this.parent) : super.create('Entry') { + KdbxEntry.create(KdbxFile file, this.parent) : super.create(file, 'Entry') { icon.set(KdbxIcon.Key); } KdbxEntry.read(this.parent, XmlElement node) : super.read(node) { - _strings.addEntries(node.findElements('String').map((el) { - final key = KdbxKey(el.findElements('Key').single.text); - final valueNode = el.findElements('Value').single; - if (valueNode.getAttribute('Protected')?.toLowerCase() == 'true') { + _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; + if (valueNode.getAttribute(KdbxXml.ATTR_PROTECTED)?.toLowerCase() == + 'true') { return MapEntry(key, KdbxFile.protectedValueForNode(valueNode)); } else { return MapEntry(key, PlainValue(valueNode.text)); @@ -37,22 +39,52 @@ class KdbxEntry extends KdbxObject { })); } + List _history; + + List get history => + _history ?? + (() { + return _historyElement + .findElements('Entry') + .map((entry) => KdbxEntry.read(parent, entry)) + .toList(); + })(); + + XmlElement get _historyElement => node + .findElements(KdbxXml.NODE_HISTORY) + .singleWhere((_) => true, orElse: () { + final el = XmlElement(XmlName(KdbxXml.NODE_HISTORY)); + node.children.add(el); + return el; + }); + + @override + set isDirty(bool newDirty) { + if (!isDirty && newDirty) { + final history = _historyElement; + history.children.add(toXml()); + } + super.isDirty = newDirty; + } + @override XmlElement toXml() { final el = super.toXml(); - el.children.removeWhere((e) => e is XmlElement && e.name.local == 'String'); + el.children.removeWhere( + (e) => e is XmlElement && e.name.local == KdbxXml.NODE_STRING); el.children.addAll(stringEntries.map((stringEntry) { - final value = XmlElement(XmlName('Value')); + final value = XmlElement(XmlName(KdbxXml.NODE_VALUE)); if (stringEntry.value is ProtectedValue) { - value.attributes.add(XmlAttribute(XmlName('Protected'), 'true')); + value.attributes + .add(XmlAttribute(XmlName(KdbxXml.ATTR_PROTECTED), 'true')); KdbxFile.setProtectedValueForNode( value, stringEntry.value as ProtectedValue); } else { value.children.add(XmlText(stringEntry.value.getText())); } - return XmlElement(XmlName('String')) + return XmlElement(XmlName(KdbxXml.NODE_STRING)) ..children.addAll([ - XmlElement(XmlName('Key')) + XmlElement(XmlName(KdbxXml.ATTR_PROTECTED)) ..children.add(XmlText(stringEntry.key.key)), value, ]); @@ -65,11 +97,13 @@ class KdbxEntry extends KdbxObject { // Map get strings => UnmodifiableMapView(_strings); - Iterable> get stringEntries => _strings.entries; + Iterable> get stringEntries => + _strings.entries; StringValue getString(KdbxKey key) => _strings[key]; void setString(KdbxKey key, StringValue value) { + isDirty = true; _strings[key] = value; } diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 20b2c29..a86f83e 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -6,7 +6,6 @@ import 'package:convert/convert.dart' as convert; import 'package:crypto/crypto.dart' as crypto; import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/crypto/protected_value.dart'; -import 'package:kdbx/src/internal/async_utils.dart'; import 'package:kdbx/src/internal/byte_utils.dart'; import 'package:kdbx/src/internal/crypto_utils.dart'; import 'package:kdbx/src/kdbx_group.dart'; @@ -36,7 +35,11 @@ class Credentials { } class KdbxFile { - KdbxFile(this.credentials, this.header, this.body); + KdbxFile(this.credentials, this.header, this.body) { + for (final obj in _allObjects) { + obj.file = this; + } + } static final protectedValues = Expando(); @@ -52,6 +55,7 @@ class KdbxFile { final Credentials credentials; final KdbxHeader header; final KdbxBody body; + final Set dirtyObjects = {}; Uint8List save() { assert(header.versionMajor == 3); @@ -68,6 +72,7 @@ class KdbxFile { (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List) .buffer); body.writeV3(writer, this, gen); + dirtyObjects.clear(); return output.toBytes(); } @@ -76,6 +81,10 @@ class KdbxFile { .cast() .followedBy(body.rootGroup.getAllEntries()); + void dirtyObject(KdbxObject kdbxObject) { + dirtyObjects.add(kdbxObject); + } + // void _subscribeToChildren() { // final allObjects = _allObjects; // for (final obj in allObjects) { diff --git a/lib/src/kdbx_group.dart b/lib/src/kdbx_group.dart index 3a4d7f1..4500a0e 100644 --- a/lib/src/kdbx_group.dart +++ b/lib/src/kdbx_group.dart @@ -1,3 +1,4 @@ +import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/internal/async_utils.dart'; import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_entry.dart'; @@ -9,7 +10,7 @@ import 'kdbx_object.dart'; class KdbxGroup extends KdbxObject { KdbxGroup.create({@required this.parent, @required String name}) - : super.create('Group') { + : super.create(parent?.file, 'Group') { this.name.set(name); icon.set(KdbxIcon.Folder); expanded.set(true); diff --git a/lib/src/kdbx_object.dart b/lib/src/kdbx_object.dart index 78ea8af..8baa369 100644 --- a/lib/src/kdbx_object.dart +++ b/lib/src/kdbx_object.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; +import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_times.dart'; import 'package:kdbx/src/kdbx_xml.dart'; import 'package:meta/meta.dart'; @@ -30,7 +31,7 @@ mixin Changeable { abstract class KdbxNode with Changeable { KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)) { - isDirty = true; + _isDirty = true; } KdbxNode.read(this.node); @@ -48,13 +49,16 @@ abstract class KdbxNode with Changeable { } abstract class KdbxObject extends KdbxNode { - KdbxObject.create(String nodeName) + KdbxObject.create(this.file, String nodeName) : times = KdbxTimes.create(), super.create(nodeName) { _uuid.set(KdbxUuid.random()); } KdbxObject.read(XmlElement node) : times = KdbxTimes.read(node.findElements('Times').single),super.read(node); + /// the file this object is part of. will be set AFTER loading, etc. + KdbxFile file; + final KdbxTimes times; KdbxUuid get uuid => _uuid.get(); @@ -65,7 +69,10 @@ abstract class KdbxObject extends KdbxNode { @override set isDirty(bool dirty) { super.isDirty = dirty; - times.modifiedNow(); + if (dirty) { + times.modifiedNow(); + file.dirtyObject(this); + } } @override diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index 5144044..f312e96 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -6,6 +6,14 @@ import 'package:kdbx/src/kdbx_consts.dart'; import 'package:meta/meta.dart'; import 'package:xml/xml.dart'; +class KdbxXml { + static const NODE_STRING = 'String'; + static const NODE_KEY = 'Key'; + static const NODE_VALUE = 'Value'; + static const ATTR_PROTECTED = 'Protected'; + static const NODE_HISTORY = 'History'; +} + abstract class KdbxSubNode { KdbxSubNode(this.node, this.name); diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart index 42b83a2..70f5cc6 100644 --- a/test/kdbx_test.dart +++ b/test/kdbx_test.dart @@ -43,7 +43,7 @@ void main() { test('Create Entry', () { final kdbx = KdbxFormat.create(Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest'); final rootGroup = kdbx.body.rootGroup; - final entry = KdbxEntry.create(rootGroup); + final entry = KdbxEntry.create(kdbx, rootGroup); rootGroup.addEntry(entry); entry.setString(KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum')); print(kdbx.body.generateXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true)); @@ -56,7 +56,7 @@ void main() { final Uint8List saved = (() { final kdbx = KdbxFormat.create(credentials, 'CreateTest'); final rootGroup = kdbx.body.rootGroup; - final entry = KdbxEntry.create(rootGroup); + final entry = KdbxEntry.create(kdbx, rootGroup); rootGroup.addEntry(entry); entry.setString( KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum'));