KeepassX format implementation in pure dart.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

260 lines
6.7 KiB

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
4 years ago
import 'package:collection/collection.dart' show IterableExtension;
import 'package:kdbx/src/internal/extension_utils.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_meta.dart';
import 'package:kdbx/src/kdbx_times.dart';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:quiver/iterables.dart';
import 'package:uuid/uuid.dart';
import 'package:uuid/uuid_util.dart';
import 'package:xml/xml.dart';
5 years ago
// ignore: unused_element
final _logger = Logger('kdbx.kdbx_object');
class ChangeEvent<T> {
4 years ago
ChangeEvent({required this.object, required this.isDirty});
final T object;
final bool isDirty;
@override
String toString() {
return 'ChangeEvent{object: $object, isDirty: $isDirty}';
}
}
mixin Changeable<T> {
final _controller = StreamController<ChangeEvent<T>>.broadcast();
Stream<ChangeEvent<T>> get changes => _controller.stream;
bool _isDirty = false;
/// allow recursive calls to [modify]
bool _isInModify = false;
/// Called before the *first* modification (ie. before `isDirty` changes
/// from false to true)
@protected
@mustCallSuper
void onBeforeModify() {}
/// Called after the *first* modification (ie. after `isDirty` changed
/// from false to true)
@protected
@mustCallSuper
void onAfterModify() {}
RET modify<RET>(RET Function() modify) {
if (_isDirty || _isInModify) {
return modify();
}
_isInModify = true;
onBeforeModify();
try {
return modify();
} finally {
_isDirty = true;
_isInModify = false;
onAfterModify();
_controller.add(ChangeEvent(object: this as T, isDirty: _isDirty));
}
}
void clean() {
if (!_isDirty) {
return;
}
_isDirty = false;
_controller.add(ChangeEvent(object: this as T, isDirty: _isDirty));
}
bool get isDirty => _isDirty;
}
abstract class KdbxNodeContext implements KdbxNode {
KdbxReadWriteContext get ctx;
}
abstract class KdbxNode with Changeable<KdbxNode> {
KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)) {
_isDirty = true;
}
KdbxNode.read(this.node);
/// XML Node used while reading this KdbxNode.
/// Must NOT be modified. Only copies which are obtained through [toXml].
/// this node should always represent the original loaded state.
final XmlElement node;
// @protected
// String text(String nodeName) => _opt(nodeName)?.text;
/// must only be called to save this object.
/// will mark this object as not dirty.
@mustCallSuper
XmlElement toXml() {
clean();
return node.copy();
}
}
extension IterableKdbxObject<T extends KdbxObject> on Iterable<T> {
4 years ago
T? findByUuid(KdbxUuid uuid) =>
firstWhereOrNull((element) => element.uuid == uuid);
}
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 {
KdbxObject.create(
this.ctx,
this.file,
String nodeName,
4 years ago
KdbxGroup? parent,
) : times = KdbxTimes.create(ctx),
_parent = parent,
super.create(nodeName) {
_uuid.set(KdbxUuid.random());
}
4 years ago
KdbxObject.read(this.ctx, KdbxGroup? parent, XmlElement node)
: times = KdbxTimes.read(node.findElements('Times').single, ctx),
_parent = parent,
super.read(node);
/// the file this object is part of. will be set AFTER loading, etc.
/// TODO: We should probably get rid of this `file` reference.
4 years ago
KdbxFile? file;
final KdbxReadWriteContext ctx;
final KdbxTimes times;
4 years ago
KdbxUuid get uuid => _uuid.get()!;
UuidNode get _uuid => UuidNode(this, KdbxXml.NODE_UUID);
IconNode get icon => IconNode(this, 'IconID');
UuidNode get customIconUuid => UuidNode(this, 'CustomIconUUID');
4 years ago
KdbxGroup? get parent => _parent;
4 years ago
KdbxGroup? _parent;
4 years ago
KdbxCustomIcon? get customIcon =>
customIconUuid.get()?.let((uuid) => file!.body.meta.customIcons[uuid]);
4 years ago
set customIcon(KdbxCustomIcon? icon) {
if (icon != null) {
4 years ago
file!.body.meta.addCustomIcon(icon);
customIconUuid.set(icon.uuid);
} else {
customIconUuid.set(null);
}
}
@override
void onAfterModify() {
super.onAfterModify();
times.modifiedNow();
// during initial `create` the file will be null.
file?.dirtyObject(this);
}
bool wasModifiedAfter(KdbxObject other) => times.lastModificationTime
4 years ago
.get()!
.isAfter(other.times.lastModificationTime.get()!);
bool wasMovedAfter(KdbxObject other) =>
4 years ago
times.locationChanged.get()!.isAfter(other.times.locationChanged.get()!);
@override
XmlElement toXml() {
final el = super.toXml();
XmlUtils.removeChildrenByName(el, 'Times');
el.children.add(times.toXml());
return el;
}
void internalChangeParent(KdbxGroup parent) {
modify(() => _parent = parent);
}
void merge(MergeContext mergeContext, covariant KdbxObject other);
}
class KdbxUuid {
const KdbxUuid(this.uuid);
KdbxUuid.random() : this(base64.encode(Uuid.parse(uuidGenerator.v4())));
KdbxUuid.fromBytes(Uint8List bytes) : this(base64.encode(bytes));
/// https://tools.ietf.org/html/rfc4122.html#section-4.1.7
/// > The nil UUID is special form of UUID that is specified to have all
/// 128 bits set to zero.
static const NIL = KdbxUuid('AAAAAAAAAAAAAAAAAAAAAA==');
static const Uuid uuidGenerator =
Uuid(options: <String, dynamic>{'grng': UuidUtil.cryptoRNG});
/// base64 representation of uuid.
final String uuid;
Uint8List toBytes() => base64.decode(uuid);
@override
String toString() => uuid;
@override
bool operator ==(Object other) =>
identical(this, other) || other is KdbxUuid && uuid == other.uuid;
@override
int get hashCode => uuid.hashCode;
/// Whether this is the [NIL] uuid.
bool get isNil => this == NIL;
}