Browse Source

WIP: merging incoming changes #80

pull/3/head
Herbert Poul 4 years ago
parent
commit
64bec43473
  1. 24
      lib/kdbx.dart
  2. 7
      lib/src/kdbx_binary.dart
  3. 2
      lib/src/kdbx_custom_data.dart
  4. 4
      lib/src/kdbx_dao.dart
  5. 3
      lib/src/kdbx_deleted_object.dart
  6. 124
      lib/src/kdbx_entry.dart
  7. 11
      lib/src/kdbx_file.dart
  8. 167
      lib/src/kdbx_format.dart
  9. 137
      lib/src/kdbx_group.dart
  10. 42
      lib/src/kdbx_meta.dart
  11. 64
      lib/src/kdbx_object.dart
  12. 21
      lib/src/kdbx_times.dart
  13. 37
      lib/src/kdbx_xml.dart
  14. 1
      pubspec.yaml

24
lib/kdbx.dart

@ -9,10 +9,19 @@ export 'src/kdbx_binary.dart' show KdbxBinary;
export 'src/kdbx_consts.dart'; export 'src/kdbx_consts.dart';
export 'src/kdbx_custom_data.dart'; export 'src/kdbx_custom_data.dart';
export 'src/kdbx_dao.dart' show KdbxDao; export 'src/kdbx_dao.dart' show KdbxDao;
export 'src/kdbx_entry.dart'; export 'src/kdbx_entry.dart' show KdbxEntry, KdbxKey;
export 'src/kdbx_file.dart'; export 'src/kdbx_file.dart';
export 'src/kdbx_format.dart'; export 'src/kdbx_format.dart'
export 'src/kdbx_group.dart'; show
KdbxBody,
Credentials,
CredentialsPart,
HashCredentials,
KdbxFormat,
KeyFileComposite,
KeyFileCredentials,
PasswordCredentials;
export 'src/kdbx_group.dart' show KdbxGroup;
export 'src/kdbx_header.dart' export 'src/kdbx_header.dart'
show show
KdbxException, KdbxException,
@ -21,5 +30,12 @@ export 'src/kdbx_header.dart'
KdbxUnsupportedException, KdbxUnsupportedException,
KdbxVersion; KdbxVersion;
export 'src/kdbx_meta.dart'; export 'src/kdbx_meta.dart';
export 'src/kdbx_object.dart'; export 'src/kdbx_object.dart'
show
KdbxUuid,
KdbxObject,
KdbxNode,
Changeable,
ChangeEvent,
KdbxNodeContext;
export 'src/utils/byte_utils.dart' show ByteUtils; export 'src/utils/byte_utils.dart' show ByteUtils;

7
lib/src/kdbx_binary.dart

@ -6,6 +6,7 @@ import 'package:kdbx/src/utils/byte_utils.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:quiver/core.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
class KdbxBinary { class KdbxBinary {
@ -13,6 +14,7 @@ class KdbxBinary {
final bool isInline; final bool isInline;
final bool isProtected; final bool isProtected;
final Uint8List value; final Uint8List value;
int _valueHashCode;
static KdbxBinary readBinaryInnerHeader(InnerHeaderField field) { static KdbxBinary readBinaryInnerHeader(InnerHeaderField field) {
final flags = field.bytes[0]; final flags = field.bytes[0];
@ -25,6 +27,11 @@ class KdbxBinary {
); );
} }
int get valueHashCode => _valueHashCode ??= hashObjects(value);
bool valueEqual(KdbxBinary other) =>
valueHashCode == other.valueHashCode && ByteUtils.eq(value, value);
InnerHeaderField writeToInnerHeader() { InnerHeaderField writeToInnerHeader() {
final writer = WriterHelper(); final writer = WriterHelper();
final flags = isProtected ? 0x01 : 0x00; final flags = isProtected ? 0x01 : 0x00;

2
lib/src/kdbx_custom_data.dart

@ -28,6 +28,8 @@ class KdbxCustomData extends KdbxNode {
modify(() => _data[key] = value); modify(() => _data[key] = value);
} }
bool containsKey(String key) => _data.containsKey(key);
@override @override
xml.XmlElement toXml() { xml.XmlElement toXml() {
final el = super.toXml(); final el = super.toXml();

4
lib/src/kdbx_dao.dart

@ -1,5 +1,7 @@
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/kdbx_entry.dart';
import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_file.dart';
import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_object.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
/// Helper object for accessing and modifing data inside /// Helper object for accessing and modifing data inside

3
lib/src/kdbx_deleted_object.dart

@ -1,4 +1,5 @@
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';

124
lib/src/kdbx_entry.dart

@ -1,11 +1,11 @@
import 'dart:typed_data'; import 'dart:typed_data';
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/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';
import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
@ -13,6 +13,7 @@ import 'package:kdbx/src/kdbx_xml.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:quiver/check.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
final _logger = Logger('kdbx.kdbx_entry'); final _logger = Logger('kdbx.kdbx_entry');
@ -37,10 +38,74 @@ class KdbxKey {
} }
} }
extension KdbxEntryInternal on KdbxEntry {
KdbxEntry cloneInto(KdbxGroup otherGroup, {bool toHistoryEntry = false}) =>
KdbxEntry.create(
otherGroup.file,
otherGroup,
isHistoryEntry: toHistoryEntry,
)
..forceSetUuid(uuid)
..let(toHistoryEntry ? (x) => null : otherGroup.addEntry)
.._overwriteFrom(
OverwriteContext.noop,
this,
includeHistory: !toHistoryEntry,
);
List<KdbxSubNode> get _overwriteNodes => [
...objectNodes,
foregroundColor,
backgroundColor,
overrideURL,
tags,
];
void _overwriteFrom(
OverwriteContext overwriteContext,
KdbxEntry other, {
bool includeHistory = false,
}) {
// we only support overwriting history, if it is empty.
checkArgument(!includeHistory || history.isEmpty,
message:
'We can only overwrite with history, if local history is empty.');
assertSameUuid(other, 'overwrite');
overwriteSubNodesFrom(
overwriteContext,
_overwriteNodes,
other._overwriteNodes,
);
// overwrite all strings
_strings.clear();
_strings.addAll(other._strings);
// overwrite all binaries
final newBinaries = other._binaries.map((key, value) => MapEntry(
key,
ctx.findBinaryByValue(value) ??
(value..let((that) => ctx.addBinary(that))),
));
_binaries.clear();
_binaries.addAll(newBinaries);
times.overwriteFrom(other.times);
if (includeHistory) {
for (final historyEntry in other.history) {
history.add(historyEntry.cloneInto(parent, toHistoryEntry: false));
}
}
}
}
class KdbxEntry extends KdbxObject { class KdbxEntry extends KdbxObject {
KdbxEntry.create(KdbxFile file, KdbxGroup parent) /// Creates a new entry in the given parent group.
: isHistoryEntry = false, /// callers are still responsible for calling [parent.addEntry(..)]!
history = [], ///
/// FIXME: this makes no sense, we should automatically attach this to the parent.
KdbxEntry.create(
KdbxFile file,
KdbxGroup parent, {
this.isHistoryEntry = false,
}) : history = [],
super.create(file.ctx, file, 'Entry', parent) { super.create(file.ctx, file, 'Entry', parent) {
icon.set(KdbxIcon.Key); icon.set(KdbxIcon.Key);
} }
@ -89,6 +154,11 @@ class KdbxEntry extends KdbxObject {
final List<KdbxEntry> history; final List<KdbxEntry> history;
ColorNode get foregroundColor => ColorNode(this, 'ForegroundColor');
ColorNode get backgroundColor => ColorNode(this, 'BackgroundColor');
StringNode get overrideURL => StringNode(this, 'OverrideURL');
StringNode get tags => StringNode(this, 'Tags');
@override @override
set file(KdbxFile file) { set file(KdbxFile file) {
super.file = file; super.file = file;
@ -102,7 +172,12 @@ class KdbxEntry extends KdbxObject {
@override @override
void onBeforeModify() { void onBeforeModify() {
super.onBeforeModify(); super.onBeforeModify();
history.add(KdbxEntry.read(ctx, parent, toXml())..file = file); history.add(KdbxEntry.read(
ctx,
parent,
toXml(),
isHistoryEntry: true,
)..file = file);
} }
@override @override
@ -251,6 +326,45 @@ class KdbxEntry extends KdbxObject {
throw StateError('Unable to find unique name for $fileName'); throw StateError('Unable to find unique name for $fileName');
} }
static KdbxEntry _findHistoryEntry(
List<KdbxEntry> history, DateTime lastModificationTime) =>
history.firstWhere(
(history) =>
history.times.lastModificationTime.get() == lastModificationTime,
orElse: () => null);
@override
void merge(MergeContext mergeContext, KdbxEntry other) {
assertSameUuid(other, 'merge');
if (other.wasModifiedAfter(this)) {
// other object is newer, create new history entry and copy fields.
modify(() => _overwriteFrom(mergeContext, other));
} else if (wasModifiedAfter(other)) {
// we are newer. check if the old revision lives on in our history.
final ourLastModificationTime = times.lastModificationTime.get();
final historyEntry = _findHistoryEntry(history, ourLastModificationTime);
if (historyEntry == null) {
// it seems like we don't know about that state, so we have to add
// it to history.
history.add(other.cloneInto(parent, toHistoryEntry: true));
}
}
// copy missing history entries.
for (final otherHistoryEntry in other.history) {
final meHistoryEntry = _findHistoryEntry(
history, otherHistoryEntry.times.lastModificationTime.get());
if (meHistoryEntry == null) {
mergeContext.trackChange(
this,
debug: 'merge in history '
'${otherHistoryEntry.times.lastModificationTime.get()}',
);
history.add(otherHistoryEntry.cloneInto(parent, toHistoryEntry: true));
}
}
mergeContext.markAsMerged(this);
}
@override @override
String toString() { String toString() {
return 'KdbxGroup{uuid=$uuid,name=$label}'; return 'KdbxGroup{uuid=$uuid,name=$label}';

11
lib/src/kdbx_file.dart

@ -119,6 +119,17 @@ class KdbxFile {
body.meta.headerHash.remove(); body.meta.headerHash.remove();
header.upgrade(majorVersion); header.upgrade(majorVersion);
} }
/// Merges the given file into this file.
/// Both files must have the same origin (ie. same root group UUID).
/// FIXME: THiS iS NOT YET FINISHED, DO NOT USE.
void merge(KdbxFile other) {
if (other.body.rootGroup.uuid != body.rootGroup.uuid) {
throw KdbxUnsupportedException(
'Root groups of source and dest file do not match.');
}
body.merge(other.body);
}
} }
class CachedValue<T> { class CachedValue<T> {

167
lib/src/kdbx_format.dart

@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:archive/archive.dart'; import 'package:archive/archive.dart';
import 'package:supercharged_dart/supercharged_dart.dart';
import 'package:argon2_ffi_base/argon2_ffi_base.dart'; import 'package:argon2_ffi_base/argon2_ffi_base.dart';
import 'package:convert/convert.dart' as convert; import 'package:convert/convert.dart' as convert;
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
@ -27,6 +28,7 @@ import 'package:kdbx/src/kdbx_xml.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:quiver/iterables.dart';
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
final _logger = Logger('kdbx.format'); final _logger = Logger('kdbx.format');
@ -72,7 +74,8 @@ class KdbxReadWriteContext {
@required this.header, @required this.header,
}) : assert(binaries != null), }) : assert(binaries != null),
assert(header != null), assert(header != null),
_binaries = binaries; _binaries = binaries,
_deletedObjects = [];
static final kdbxContext = Expando<KdbxReadWriteContext>(); static final kdbxContext = Expando<KdbxReadWriteContext>();
@ -89,8 +92,10 @@ class KdbxReadWriteContext {
kdbxContext[node.document] = ctx; kdbxContext[node.document] = ctx;
} }
// TODO make [_binaries] and [_deletedObjects] late init :-)
@protected @protected
final List<KdbxBinary> _binaries; final List<KdbxBinary> _binaries;
final List<KdbxDeletedObject> _deletedObjects;
Iterable<KdbxBinary> get binariesIterable => _binaries; Iterable<KdbxBinary> get binariesIterable => _binaries;
@ -98,6 +103,12 @@ class KdbxReadWriteContext {
int get versionMajor => header.version.major; int get versionMajor => header.version.major;
void initContext(Iterable<KdbxBinary> binaries,
Iterable<KdbxDeletedObject> deletedObjects) {
_binaries.addAll(binaries);
_deletedObjects.addAll(deletedObjects);
}
KdbxBinary binaryById(int id) { KdbxBinary binaryById(int id) {
if (id >= _binaries.length) { if (id >= _binaries.length) {
return null; return null;
@ -109,6 +120,12 @@ class KdbxReadWriteContext {
_binaries.add(binary); _binaries.add(binary);
} }
KdbxBinary findBinaryByValue(KdbxBinary binary) {
// TODO create a hashset or map?
return _binaries.firstWhere((element) => element.valueEqual(binary),
orElse: () => null);
}
/// finds the ID of the given binary. /// finds the ID of the given binary.
/// if it can't be found, [KdbxCorruptedFileException] is thrown. /// if it can't be found, [KdbxCorruptedFileException] is thrown.
int findBinaryId(KdbxBinary binary) { int findBinaryId(KdbxBinary binary) {
@ -193,9 +210,7 @@ class HashCredentials implements Credentials {
} }
class KdbxBody extends KdbxNode { class KdbxBody extends KdbxNode {
KdbxBody.create(this.meta, this.rootGroup) KdbxBody.create(this.meta, this.rootGroup) : super.create('KeePassFile') {
: _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);
@ -206,17 +221,14 @@ class KdbxBody extends KdbxNode {
xml.XmlElement node, xml.XmlElement node,
this.meta, this.meta,
this.rootGroup, this.rootGroup,
Iterable<KdbxDeletedObject> deletedObjects, ) : super.read(node);
) : _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 @visibleForTesting
List<KdbxDeletedObject> get deletedObjects => _deletedObjects; List<KdbxDeletedObject> get deletedObjects => ctx._deletedObjects;
Future<void> writeV3(WriterHelper writer, KdbxFile kdbxFile, Future<void> writeV3(WriterHelper writer, KdbxFile kdbxFile,
ProtectedSaltGenerator saltGenerator) async { ProtectedSaltGenerator saltGenerator) async {
@ -284,6 +296,63 @@ class KdbxBody extends KdbxNode {
} }
} }
KdbxReadWriteContext get ctx => rootGroup.ctx;
Map<KdbxUuid, KdbxObject> _createObjectIndex() => Map.fromEntries(
concat([rootGroup.getAllGroups(), rootGroup.getAllEntries()])
.map((e) => MapEntry(e.uuid, e)));
void merge(KdbxBody other) {
// sync deleted objects.
final deleted =
Map.fromEntries(ctx._deletedObjects.map((e) => MapEntry(e.uuid, e)));
final incomingDeleted = <KdbxUuid, KdbxDeletedObject>{};
for (final obj in other.ctx._deletedObjects) {
if (!deleted.containsKey(obj.uuid)) {
final del = KdbxDeletedObject.create(ctx, obj.uuid);
ctx._deletedObjects.add(del);
incomingDeleted[del.uuid] = del;
deleted[del.uuid] = del;
}
}
final mergeContext = MergeContext(
objectIndex: _createObjectIndex(),
deletedObjects: deleted,
);
// sync binaries
for (final binary in other.ctx.binariesIterable) {
if (ctx.findBinaryByValue(binary) == null) {
ctx.addBinary(binary);
mergeContext.trackChange(this,
debug: 'adding new binary ${binary.value.length}');
}
}
meta.merge(other.meta);
rootGroup.merge(mergeContext, other.rootGroup);
// remove deleted objects
for (final incomingDelete in incomingDeleted.values) {
final object = mergeContext.objectIndex[incomingDelete.uuid];
mergeContext.trackChange(object, debug: 'was deleted.');
}
// FIXME do some cleanup.
_logger.info('Finished merging. ${mergeContext.debugChanges()}');
final incomingObjects = other._createObjectIndex();
_logger.info('Merged: ${mergeContext.merged} vs. '
'(local objects: ${mergeContext.objectIndex.length}, '
'incoming objects: ${incomingObjects.length})');
// sanity checks
if (mergeContext.merged.keys.length != mergeContext.objectIndex.length) {
// TODO figure out what went wrong.
}
}
xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) { xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) {
final rootGroupNode = rootGroup.toXml(); final rootGroupNode = rootGroup.toXml();
// update protected values... // update protected values...
@ -316,7 +385,7 @@ class KdbxBody extends KdbxNode {
rootGroupNode, rootGroupNode,
XmlUtils.createNode( XmlUtils.createNode(
KdbxXml.NODE_DELETED_OBJECTS, KdbxXml.NODE_DELETED_OBJECTS,
_deletedObjects.map((e) => e.toXml()).toList(), ctx._deletedObjects.map((e) => e.toXml()).toList(),
), ),
]), ]),
], ],
@ -330,6 +399,65 @@ class KdbxBody extends KdbxNode {
} }
} }
abstract class OverwriteContext {
const OverwriteContext();
static const noop = OverwriteContextNoop();
void trackChange(KdbxObject object, {String node, String debug});
}
class OverwriteContextNoop implements OverwriteContext {
const OverwriteContextNoop();
@override
void trackChange(KdbxObject object, {String node, String debug}) {}
}
class MergeChange {
MergeChange({this.object, this.node, this.debug});
final KdbxNode object;
/// the name of the subnode of [object].
final String node;
final String debug;
}
class MergeContext implements OverwriteContext {
MergeContext({this.objectIndex, this.deletedObjects});
final Map<KdbxUuid, KdbxObject> objectIndex;
final Map<KdbxUuid, KdbxDeletedObject> deletedObjects;
final Map<KdbxUuid, KdbxObject> merged = {};
final List<MergeChange> changes = [];
void markAsMerged(KdbxObject object) {
if (merged.containsKey(object.uuid)) {
throw StateError(
'object was already market as merged! ${object.uuid}: $object');
}
merged[object.uuid] = object;
}
@override
void trackChange(KdbxNode object, {String node, String debug}) {
changes.add(MergeChange(
object: object,
node: node,
debug: debug,
));
}
String debugChanges() {
final group =
changes.groupBy((element) => element.object, valueTransform: (x) => x);
return group.entries
.map((e) => [
e.key.toString(),
': ',
...e.value.map((e) => e.toString()),
].join('\n '))
.join('\n');
}
}
class _KeysV4 { class _KeysV4 {
_KeysV4(this.hmacKey, this.cipherKey); _KeysV4(this.hmacKey, this.cipherKey);
@ -645,15 +773,12 @@ class KdbxFormat {
final root = keePassFile.findElements('Root').single; final root = keePassFile.findElements('Root').single;
final kdbxMeta = KdbxMeta.read(meta, ctx); final kdbxMeta = KdbxMeta.read(meta, ctx);
if (kdbxMeta.binaries?.isNotEmpty == true) { // kdbx < 4 has binaries in the meta section, >= 4 in the binary header.
ctx._binaries.addAll(kdbxMeta.binaries); final binaries = kdbxMeta.binaries?.isNotEmpty == true
} else if (header.innerHeader.binaries.isNotEmpty) { ? kdbxMeta.binaries
ctx._binaries.addAll(header.innerHeader.binaries : header.innerHeader.binaries
.map((e) => KdbxBinary.readBinaryInnerHeader(e))); .map((e) => KdbxBinary.readBinaryInnerHeader(e));
}
final rootGroup =
KdbxGroup.read(ctx, null, root.findElements(KdbxXml.NODE_GROUP).single);
final deletedObjects = root final deletedObjects = root
.findElements(KdbxXml.NODE_DELETED_OBJECTS) .findElements(KdbxXml.NODE_DELETED_OBJECTS)
.singleOrNull .singleOrNull
@ -661,8 +786,12 @@ class KdbxFormat {
.findElements(KdbxDeletedObject.NODE_NAME) .findElements(KdbxDeletedObject.NODE_NAME)
.map((node) => KdbxDeletedObject.read(node, ctx))) ?? .map((node) => KdbxDeletedObject.read(node, ctx))) ??
[]; [];
ctx.initContext(binaries, deletedObjects);
final rootGroup =
KdbxGroup.read(ctx, null, root.findElements(KdbxXml.NODE_GROUP).single);
_logger.fine('successfully read Meta.'); _logger.fine('successfully read Meta.');
return KdbxBody.read(keePassFile, kdbxMeta, rootGroup, deletedObjects); return KdbxBody.read(keePassFile, kdbxMeta, rootGroup);
} }
Uint8List _decryptContent( Uint8List _decryptContent(

137
lib/src/kdbx_group.dart

@ -1,18 +1,22 @@
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_consts.dart';
import 'package:kdbx/src/kdbx_entry.dart'; import 'package:kdbx/src/kdbx_entry.dart';
import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
import 'kdbx_object.dart'; import 'kdbx_object.dart';
final _logger = Logger('kdbx_group');
class KdbxGroup extends KdbxObject { class KdbxGroup extends KdbxObject {
KdbxGroup.create( KdbxGroup.create({
{@required KdbxReadWriteContext ctx, @required KdbxReadWriteContext ctx,
@required KdbxGroup parent, @required KdbxGroup parent,
@required String name}) @required String name,
: super.create( }) : super.create(
ctx, ctx,
parent?.file, parent?.file,
KdbxXml.NODE_GROUP, KdbxXml.NODE_GROUP,
@ -65,6 +69,8 @@ class KdbxGroup extends KdbxObject {
throw StateError( throw StateError(
'Invalid operation. Trying to add entry which is already in another group.'); 'Invalid operation. Trying to add entry which is already in another group.');
} }
assert(_entries.findByUuid(entry.uuid) == null,
'must not already be in this group.');
modify(() => _entries.add(entry)); modify(() => _entries.add(entry));
} }
@ -76,36 +82,127 @@ class KdbxGroup extends KdbxObject {
modify(() => _groups.add(group)); modify(() => _groups.add(group));
} }
void internalRemoveGroup(KdbxGroup group) {
modify(() {
if (!_groups.remove(group)) {
throw StateError('Unable to remove $group from $this (Not found)');
}
});
}
void internalRemoveEntry(KdbxEntry entry) {
modify(() {
if (!_entries.remove(entry)) {
throw StateError('Unable to remove $entry from $this (Not found)');
}
});
}
/// returns all parents recursively including this group. /// returns all parents recursively including this group.
List<KdbxGroup> get breadcrumbs => [...?parent?.breadcrumbs, this]; List<KdbxGroup> get breadcrumbs => [...?parent?.breadcrumbs, this];
StringNode get name => StringNode(this, 'Name'); StringNode get name => StringNode(this, 'Name');
StringNode get notes => StringNode(this, 'Notes');
// String get name => text('Name') ?? ''; // String get name => text('Name') ?? '';
BooleanNode get expanded => BooleanNode(this, 'IsExpanded'); BooleanNode get expanded => BooleanNode(this, 'IsExpanded');
StringNode get defaultAutoTypeSequence =>
StringNode(this, 'DefaultAutoTypeSequence');
BooleanNode get enableAutoType => BooleanNode(this, 'EnableAutoType'); BooleanNode get enableAutoType => BooleanNode(this, 'EnableAutoType');
BooleanNode get enableSearching => BooleanNode(this, 'EnableSearching'); BooleanNode get enableSearching => BooleanNode(this, 'EnableSearching');
UuidNode get lastTopVisibleEntry => UuidNode(this, 'LastTopVisibleEntry');
@override
void merge(MergeContext mergeContext, KdbxGroup other) {
assertSameUuid(other, 'merge');
if (other.wasModifiedAfter(this)) {
_logger.finest('merge: other group was modified $uuid');
_overwriteFrom(mergeContext, other);
}
_mergeSubObjects<KdbxGroup>(
mergeContext,
_groups,
other._groups,
importToHere: (other) =>
KdbxGroup.create(ctx: ctx, parent: this, name: other.name.get())
..forceSetUuid(other.uuid)
.._overwriteFrom(mergeContext, other),
);
_mergeSubObjects<KdbxEntry>(
mergeContext,
_entries,
other._entries,
importToHere: (other) => other.cloneInto(this),
);
}
void _mergeSubObjects<T extends KdbxObject>(
MergeContext mergeContext, List<T> me, List<T> other,
{@required T Function(T obj) importToHere}) {
// possibilities:
// 1. Not changed at all 👍
// 2. Deleted in other
// 3. Deleted in this
// 4. Modified in other
// 5. Modified in this
// 6. Moved in other
// 7. Moved in this
for (final otherObj in other) {
final meObj = me.findByUuid(otherObj.uuid);
if (meObj == null) {
// moved or deleted.
final movedObj = mergeContext.objectIndex[otherObj.uuid];
if (movedObj == null) {
// item was created in the other file. we have to import it
importToHere(otherObj);
} else {
// item was moved.
if (otherObj.wasMovedAfter(movedObj)) {
// item was moved in the other file, so we have to move it here.
file.move(movedObj, this);
} else {
// item was moved in this file, so nothing to do.
}
movedObj.merge(mergeContext, otherObj);
}
} else {
meObj.merge(mergeContext, otherObj);
}
}
mergeContext.markAsMerged(this);
}
List<KdbxSubNode> get _overwriteNodes => [
...objectNodes,
name,
notes,
expanded,
defaultAutoTypeSequence,
enableAutoType,
enableSearching,
lastTopVisibleEntry,
];
void _overwriteFrom(MergeContext mergeContext, KdbxGroup other) {
assertSameUuid(other, 'overwrite');
overwriteSubNodesFrom(mergeContext, _overwriteNodes, other._overwriteNodes);
// we should probably check that [lastTopVisibleEntry] is still a
// valid reference?
times.overwriteFrom(other.times);
}
@override @override
String toString() { String toString() {
return 'KdbxGroup{uuid=$uuid,name=${name.get()}}'; return 'KdbxGroup{uuid=$uuid,name=${name.get()}}';
} }
} }
extension KdbxGroupInternal on KdbxGroup {
void internalRemoveGroup(KdbxGroup group) {
modify(() {
if (!_groups.remove(group)) {
throw StateError('Unable to remove $group from $this (Not found)');
}
});
}
void internalRemoveEntry(KdbxEntry entry) {
modify(() {
if (!_entries.remove(entry)) {
throw StateError('Unable to remove $entry from $this (Not found)');
}
});
}
}

42
lib/src/kdbx_meta.dart

@ -2,10 +2,10 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/internal/extension_utils.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_custom_data.dart'; import 'package:kdbx/src/kdbx_custom_data.dart';
import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
@ -165,6 +165,46 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext {
); );
return ret; return ret;
} }
// Merge in changes in [other] into this meta data.
void merge(KdbxMeta other) {
// FIXME make sure this is finished
if (other.databaseNameChanged.isAfter(databaseNameChanged)) {
databaseName.set(other.databaseName.get());
databaseNameChanged.set(other.databaseNameChanged.get());
}
if (other.databaseDescriptionChanged.isAfter(databaseDescriptionChanged)) {
databaseDescription.set(other.databaseDescription.get());
databaseDescriptionChanged.set(other.databaseDescriptionChanged.get());
}
if (other.defaultUserNameChanged.isAfter(defaultUserNameChanged)) {
defaultUserName.set(other.defaultUserName.get());
defaultUserNameChanged.set(other.defaultUserNameChanged.get());
}
if (other.masterKeyChanged.isAfter(masterKeyChanged)) {
throw UnimplementedError(
'Other database changed master key. not supported.');
}
if (other.recycleBinChanged.isAfter(recycleBinChanged)) {
recycleBinEnabled.set(other.recycleBinEnabled.get());
recycleBinUUID.set(other.recycleBinUUID.get());
recycleBinChanged.set(other.recycleBinChanged.get());
}
final otherIsNewer = other.settingsChanged.isAfter(settingsChanged);
// merge custom data
for (final otherCustomDataEntry in other.customData.entries) {
if (otherIsNewer || !customData.containsKey(otherCustomDataEntry.key)) {
customData[otherCustomDataEntry.key] = otherCustomDataEntry.value;
}
}
// merge custom icons
for (final otherCustomIcon in other._customIcons.values) {
_customIcons[otherCustomIcon.uuid] ??= otherCustomIcon;
}
settingsChanged.set(other.settingsChanged.get());
}
} }
class KdbxCustomIcon { class KdbxCustomIcon {

64
lib/src/kdbx_object.dart

@ -2,14 +2,16 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/internal/extension_utils.dart'; import 'package:kdbx/src/internal/extension_utils.dart';
import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_file.dart';
import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_meta.dart';
import 'package:kdbx/src/kdbx_times.dart'; import 'package:kdbx/src/kdbx_times.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:quiver/iterables.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:uuid/uuid_util.dart'; import 'package:uuid/uuid_util.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
@ -36,6 +38,9 @@ mixin Changeable<T> {
bool _isDirty = false; bool _isDirty = false;
/// allow recursive calls to [modify]
bool _isInModify = false;
/// Called before the *first* modification (ie. before `isDirty` changes /// Called before the *first* modification (ie. before `isDirty` changes
/// from false to true) /// from false to true)
@protected @protected
@ -49,14 +54,16 @@ mixin Changeable<T> {
void onAfterModify() {} void onAfterModify() {}
RET modify<RET>(RET Function() modify) { RET modify<RET>(RET Function() modify) {
if (_isDirty) { if (_isDirty || _isInModify) {
return modify(); return modify();
} }
_isInModify = true;
onBeforeModify(); onBeforeModify();
try { try {
return modify(); return modify();
} finally { } finally {
_isDirty = true; _isDirty = true;
_isInModify = false;
onAfterModify(); onAfterModify();
_controller.add(ChangeEvent(object: this as T, isDirty: _isDirty)); _controller.add(ChangeEvent(object: this as T, isDirty: _isDirty));
} }
@ -102,9 +109,49 @@ abstract class KdbxNode with Changeable<KdbxNode> {
} }
} }
extension IterableKdbxObject<T extends KdbxObject> on Iterable<T> {
T findByUuid(KdbxUuid uuid) =>
firstWhere((element) => element.uuid == uuid, orElse: () => null);
}
extension KdbxObjectInternal on KdbxObject {
List<KdbxSubNode> get objectNodes => [
icon,
customIconUuid,
];
/// should only be used in internal code, used to clone
/// from one kdbx file into another. (like merging).
void forceSetUuid(KdbxUuid uuid) {
_uuid.set(uuid, force: true);
}
void assertSameUuid(KdbxObject other, String debugAction) {
if (uuid != other.uuid) {
throw StateError(
'Uuid of other object does not match current object for $debugAction');
}
}
void overwriteSubNodesFrom(OverwriteContext overwriteContext,
List<KdbxSubNode> myNodes, List<KdbxSubNode> otherNodes) {
for (final node in zip([myNodes, otherNodes])) {
final me = node[0];
final other = node[1];
if (me.set(other.get())) {
overwriteContext.trackChange(this, node: me.name);
}
}
}
}
abstract class KdbxObject extends KdbxNode { abstract class KdbxObject extends KdbxNode {
KdbxObject.create(this.ctx, this.file, String nodeName, KdbxGroup parent) KdbxObject.create(
: assert(ctx != null), this.ctx,
this.file,
String nodeName,
KdbxGroup parent,
) : assert(ctx != null),
times = KdbxTimes.create(ctx), times = KdbxTimes.create(ctx),
_parent = parent, _parent = parent,
super.create(nodeName) { super.create(nodeName) {
@ -156,6 +203,13 @@ abstract class KdbxObject extends KdbxNode {
file?.dirtyObject(this); file?.dirtyObject(this);
} }
bool wasModifiedAfter(KdbxObject other) => times.lastModificationTime
.get()
.isAfter(other.times.lastModificationTime.get());
bool wasMovedAfter(KdbxObject other) =>
times.locationChanged.get().isAfter(other.times.locationChanged.get());
@override @override
XmlElement toXml() { XmlElement toXml() {
final el = super.toXml(); final el = super.toXml();
@ -167,6 +221,8 @@ abstract class KdbxObject extends KdbxNode {
void internalChangeParent(KdbxGroup parent) { void internalChangeParent(KdbxGroup parent) {
modify(() => _parent = parent); modify(() => _parent = parent);
} }
void merge(MergeContext mergeContext, covariant KdbxObject other);
} }
class KdbxUuid { class KdbxUuid {

21
lib/src/kdbx_times.dart

@ -1,7 +1,8 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
import 'package:quiver/iterables.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
class KdbxTimes extends KdbxNode implements KdbxNodeContext { class KdbxTimes extends KdbxNode implements KdbxNodeContext {
@ -38,4 +39,22 @@ class KdbxTimes extends KdbxNode implements KdbxNodeContext {
accessedNow(); accessedNow();
lastModificationTime.set(clock.now().toUtc()); lastModificationTime.set(clock.now().toUtc());
} }
List<KdbxSubNode> get _nodes => [
creationTime,
lastModificationTime,
lastAccessTime,
expiryTime,
expires,
usageCount,
locationChanged,
];
void overwriteFrom(KdbxTimes other) {
for (final pair in zip([_nodes, other._nodes])) {
final me = pair[0];
final other = pair[1];
me.set(other.get());
}
}
} }

37
lib/src/kdbx_xml.dart

@ -2,7 +2,9 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/utils/byte_utils.dart'; import 'package:kdbx/src/utils/byte_utils.dart';
import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_consts.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@ -58,7 +60,7 @@ abstract class KdbxSubNode<T> {
T get(); T get();
void set(T value); bool set(T value);
void remove() { void remove() {
node.modify(() { node.modify(() {
@ -103,9 +105,9 @@ abstract class KdbxSubTextNode<T> extends KdbxSubNode<T> {
} }
@override @override
void set(T value, {bool force = false}) { bool set(T value, {bool force = false}) {
if (get() == value && force != true) { if (get() == value && force != true) {
return; return false;
} }
node.modify(() { node.modify(() {
final el = final el =
@ -125,6 +127,7 @@ abstract class KdbxSubTextNode<T> extends KdbxSubNode<T> {
el.children.add(XmlText(stringValue)); el.children.add(XmlText(stringValue));
}); });
_onModify?.call(); _onModify?.call();
return true;
} }
@override @override
@ -183,6 +186,30 @@ class IconNode extends KdbxSubTextNode<KdbxIcon> {
String encode(KdbxIcon value) => value.index.toString(); String encode(KdbxIcon value) => value.index.toString();
} }
class KdbxColor {
const KdbxColor._fromRgbCode(this._rgb) : assert(_rgb != null && _rgb != '');
const KdbxColor._nullColor() : _rgb = '';
factory KdbxColor.parse(String rgb) =>
rgb.isEmpty ? nullColor : KdbxColor._fromRgbCode(rgb);
static const nullColor = KdbxColor._nullColor();
final String _rgb;
bool get isNull => this == nullColor;
}
class ColorNode extends KdbxSubTextNode<KdbxColor> {
ColorNode(KdbxNode node, String name) : super(node, name);
@override
KdbxColor decode(String value) => KdbxColor.parse(value);
@override
String encode(KdbxColor value) => value.isNull ? '' : value._rgb;
}
class BooleanNode extends KdbxSubTextNode<bool> { class BooleanNode extends KdbxSubTextNode<bool> {
BooleanNode(KdbxNode node, String name) : super(node, name); BooleanNode(KdbxNode node, String name) : super(node, name);
@ -210,6 +237,8 @@ class DateTimeUtcNode extends KdbxSubTextNode<DateTime> {
KdbxReadWriteContext get _ctx => (node as KdbxNodeContext).ctx; KdbxReadWriteContext get _ctx => (node as KdbxNodeContext).ctx;
bool isAfter(DateTimeUtcNode other) => get().isAfter(other.get());
void setToNow() { void setToNow() {
set(clock.now().toUtc()); set(clock.now().toUtc());
} }

1
pubspec.yaml

@ -23,6 +23,7 @@ dependencies:
path: '>=1.6.0 <2.0.0' path: '>=1.6.0 <2.0.0'
quiver: '>=2.1.0 <3.0.0' quiver: '>=2.1.0 <3.0.0'
archive: '>=2.0.13 <3.0.0' archive: '>=2.0.13 <3.0.0'
supercharged_dart: '>=1.2.0 <2.0.0'
collection: '>=1.14.0 <2.0.0' collection: '>=1.14.0 <2.0.0'

Loading…
Cancel
Save