diff --git a/lib/src/crypto/protected_value.dart b/lib/src/crypto/protected_value.dart index ad35f32..8df4ea7 100644 --- a/lib/src/crypto/protected_value.dart +++ b/lib/src/crypto/protected_value.dart @@ -24,6 +24,11 @@ class PlainValue implements StringValue { return 'PlainValue{text: $text}'; } + @override + bool operator ==(dynamic other) => other is PlainValue && other.text == text; + + @override + int get hashCode => text.hashCode; } class ProtectedValue implements StringValue { @@ -64,6 +69,10 @@ class ProtectedValue implements StringValue { return utf8.decode(binaryValue); } + @override + bool operator ==(dynamic other) => + other is ProtectedValue && other.getText() == getText(); + @override String toString() { return 'ProtectedValue{${base64.encode(hash)}}'; diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index a8394e6..153e320 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -4,8 +4,11 @@ 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:logging/logging.dart'; import 'package:xml/xml.dart'; +final _logger = Logger('kdbx.kdbx_entry'); + /// Represents a case insensitive (but case preserving) key. class KdbxKey { KdbxKey(this.key) : _canonicalKey = key.toLowerCase(); @@ -22,11 +25,14 @@ class KdbxKey { } class KdbxEntry extends KdbxObject { - KdbxEntry.create(KdbxFile file, this.parent) : super.create(file, 'Entry') { + KdbxEntry.create(KdbxFile file, this.parent) + : isHistoryEntry = false, + super.create(file, 'Entry') { icon.set(KdbxIcon.Key); } - KdbxEntry.read(this.parent, XmlElement node) : super.read(node) { + KdbxEntry.read(this.parent, XmlElement node, {this.isHistoryEntry = false}) + : super.read(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; @@ -39,14 +45,14 @@ class KdbxEntry extends KdbxObject { })); } + final bool isHistoryEntry; + List _history; - List get history => - _history ?? - (() { + List get history => _history ??= (() { return _historyElement .findElements('Entry') - .map((entry) => KdbxEntry.read(parent, entry)) + .map((entry) => KdbxEntry.read(parent, entry, isHistoryEntry: true)) .toList(); })(); @@ -70,26 +76,33 @@ class KdbxEntry extends KdbxObject { @override XmlElement toXml() { final el = super.toXml(); + XmlUtils.removeChildrenByName(el, KdbxXml.NODE_STRING); + XmlUtils.removeChildrenByName(el, KdbxXml.NODE_HISTORY); el.children.removeWhere( (e) => e is XmlElement && e.name.local == KdbxXml.NODE_STRING); el.children.addAll(stringEntries.map((stringEntry) { final value = XmlElement(XmlName(KdbxXml.NODE_VALUE)); if (stringEntry.value is ProtectedValue) { value.attributes - .add(XmlAttribute(XmlName(KdbxXml.ATTR_PROTECTED), 'true')); + .add(XmlAttribute(XmlName(KdbxXml.ATTR_PROTECTED), 'True')); KdbxFile.setProtectedValueForNode( value, stringEntry.value as ProtectedValue); - } else { + } else if (stringEntry.value is StringValue) { value.children.add(XmlText(stringEntry.value.getText())); } return XmlElement(XmlName(KdbxXml.NODE_STRING)) ..children.addAll([ - XmlElement(XmlName(KdbxXml.ATTR_PROTECTED)), XmlElement(XmlName(KdbxXml.NODE_KEY)) ..children.add(XmlText(stringEntry.key.key)), value, ]); })); + if (!isHistoryEntry) { + el.children.add( + XmlElement(XmlName(KdbxXml.NODE_HISTORY)) + ..children.addAll(history.map((e) => e.toXml())), + ); + } return el; } @@ -104,6 +117,10 @@ class KdbxEntry extends KdbxObject { StringValue getString(KdbxKey key) => _strings[key]; void setString(KdbxKey key, StringValue value) { + if (_strings[key] == value) { + _logger.finest('Value did not change for $key'); + return; + } isDirty = true; _strings[key] = value; } diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index a86f83e..ce53234 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -150,16 +150,21 @@ class KdbxBody extends KdbxNode { xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) { final rootGroupNode = rootGroup.toXml(); // update protected values... - for (final el in rootGroupNode - .findAllElements('Value') - .where((el) => el.getAttribute('Protected')?.toLowerCase() == 'true')) { + for (final el in rootGroupNode.findAllElements(KdbxXml.NODE_VALUE).where( + (el) => + el.getAttribute(KdbxXml.ATTR_PROTECTED)?.toLowerCase() == 'true')) { final pv = KdbxFile.protectedValues[el]; if (pv != null) { final newValue = saltGenerator.encryptToBase64(pv.getText()); el.children.clear(); el.children.add(xml.XmlText(newValue)); } else { - _logger.warning('Unable to find protected value for $el ${el.parent}'); +// assert((() { +// _logger.severe('Unable to find protected value for $el ${el.parent.parent} (children: ${el.children})'); +// return false; +// })()); + // this is always an error, not just during debug. + throw StateError('Unable to find protected value for $el ${el.parent}'); } } diff --git a/lib/src/kdbx_object.dart b/lib/src/kdbx_object.dart index a7a3499..7bed415 100644 --- a/lib/src/kdbx_object.dart +++ b/lib/src/kdbx_object.dart @@ -19,13 +19,16 @@ class ChangeEvent { mixin Changeable { final _controller = StreamController>.broadcast(); + Stream> get changes => _controller.stream; bool _isDirty = false; + set isDirty(bool dirty) { _isDirty = dirty; _controller.add(ChangeEvent(object: this as T, isDirty: dirty)); } + bool get isDirty => _isDirty; } @@ -50,11 +53,14 @@ abstract class KdbxNode with Changeable { abstract class KdbxObject extends KdbxNode { KdbxObject.create(this.file, String nodeName) - : times = KdbxTimes.create(), super.create(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); + 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; @@ -62,6 +68,7 @@ abstract class KdbxObject extends KdbxNode { final KdbxTimes times; KdbxUuid get uuid => _uuid.get(); + UuidNode get _uuid => UuidNode(this, 'UUID'); IconNode get icon => IconNode(this, 'IconID'); @@ -88,12 +95,12 @@ abstract class KdbxObject extends KdbxNode { class KdbxUuid { const KdbxUuid(this.uuid); - KdbxUuid.random() : this(base64.encode(uuidGenerator.parse(uuidGenerator.v4()))); - static final Uuid uuidGenerator = Uuid(options: { - 'grng': UuidUtil.cryptoRNG - }); + KdbxUuid.random() + : this(base64.encode(uuidGenerator.parse(uuidGenerator.v4()))); + static final Uuid uuidGenerator = + Uuid(options: {'grng': UuidUtil.cryptoRNG}); /// base64 representation of uuid. final String uuid;