Browse Source

support modifying of entries, storing entry history, ..

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
8643a7b30b
  1. 2
      lib/kdbx.dart
  2. 56
      lib/src/kdbx_entry.dart
  3. 13
      lib/src/kdbx_format.dart
  4. 3
      lib/src/kdbx_group.dart
  5. 13
      lib/src/kdbx_object.dart
  6. 8
      lib/src/kdbx_xml.dart
  7. 4
      test/kdbx_test.dart

2
lib/kdbx.dart

@ -1,7 +1,7 @@
/// dart library for reading keepass file format (kdbx). /// dart library for reading keepass file format (kdbx).
library 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_entry.dart';
export 'src/kdbx_format.dart'; export 'src/kdbx_format.dart';
export 'src/kdbx_header.dart' export 'src/kdbx_header.dart'

56
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_format.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
/// Represents a case insensitive (but case preserving) key. /// Represents a case insensitive (but case preserving) key.
@ -21,15 +22,16 @@ class KdbxKey {
} }
class KdbxEntry extends KdbxObject { 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); icon.set(KdbxIcon.Key);
} }
KdbxEntry.read(this.parent, XmlElement node) : super.read(node) { KdbxEntry.read(this.parent, XmlElement node) : super.read(node) {
_strings.addEntries(node.findElements('String').map((el) { _strings.addEntries(node.findElements(KdbxXml.NODE_STRING).map((el) {
final key = KdbxKey(el.findElements('Key').single.text); final key = KdbxKey(el.findElements(KdbxXml.NODE_KEY).single.text);
final valueNode = el.findElements('Value').single; final valueNode = el.findElements(KdbxXml.NODE_VALUE).single;
if (valueNode.getAttribute('Protected')?.toLowerCase() == 'true') { if (valueNode.getAttribute(KdbxXml.ATTR_PROTECTED)?.toLowerCase() ==
'true') {
return MapEntry(key, KdbxFile.protectedValueForNode(valueNode)); return MapEntry(key, KdbxFile.protectedValueForNode(valueNode));
} else { } else {
return MapEntry(key, PlainValue(valueNode.text)); return MapEntry(key, PlainValue(valueNode.text));
@ -37,22 +39,52 @@ class KdbxEntry extends KdbxObject {
})); }));
} }
List<KdbxEntry> _history;
List<KdbxEntry> 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 @override
XmlElement toXml() { XmlElement toXml() {
final el = super.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) { el.children.addAll(stringEntries.map((stringEntry) {
final value = XmlElement(XmlName('Value')); final value = XmlElement(XmlName(KdbxXml.NODE_VALUE));
if (stringEntry.value is ProtectedValue) { if (stringEntry.value is ProtectedValue) {
value.attributes.add(XmlAttribute(XmlName('Protected'), 'true')); value.attributes
.add(XmlAttribute(XmlName(KdbxXml.ATTR_PROTECTED), 'true'));
KdbxFile.setProtectedValueForNode( KdbxFile.setProtectedValueForNode(
value, stringEntry.value as ProtectedValue); value, stringEntry.value as ProtectedValue);
} else { } else {
value.children.add(XmlText(stringEntry.value.getText())); value.children.add(XmlText(stringEntry.value.getText()));
} }
return XmlElement(XmlName('String')) return XmlElement(XmlName(KdbxXml.NODE_STRING))
..children.addAll([ ..children.addAll([
XmlElement(XmlName('Key')) XmlElement(XmlName(KdbxXml.ATTR_PROTECTED))
..children.add(XmlText(stringEntry.key.key)), ..children.add(XmlText(stringEntry.key.key)),
value, value,
]); ]);
@ -65,11 +97,13 @@ class KdbxEntry extends KdbxObject {
// Map<KdbxKey, StringValue> get strings => UnmodifiableMapView(_strings); // Map<KdbxKey, StringValue> get strings => UnmodifiableMapView(_strings);
Iterable<MapEntry<KdbxKey, StringValue>> get stringEntries => _strings.entries; Iterable<MapEntry<KdbxKey, StringValue>> get stringEntries =>
_strings.entries;
StringValue getString(KdbxKey key) => _strings[key]; StringValue getString(KdbxKey key) => _strings[key];
void setString(KdbxKey key, StringValue value) { void setString(KdbxKey key, StringValue value) {
isDirty = true;
_strings[key] = value; _strings[key] = value;
} }

13
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:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/crypto/protected_salt_generator.dart';
import 'package:kdbx/src/crypto/protected_value.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/byte_utils.dart';
import 'package:kdbx/src/internal/crypto_utils.dart'; import 'package:kdbx/src/internal/crypto_utils.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
@ -36,7 +35,11 @@ class Credentials {
} }
class KdbxFile { 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<ProtectedValue>(); static final protectedValues = Expando<ProtectedValue>();
@ -52,6 +55,7 @@ class KdbxFile {
final Credentials credentials; final Credentials credentials;
final KdbxHeader header; final KdbxHeader header;
final KdbxBody body; final KdbxBody body;
final Set<KdbxObject> dirtyObjects = {};
Uint8List save() { Uint8List save() {
assert(header.versionMajor == 3); assert(header.versionMajor == 3);
@ -68,6 +72,7 @@ class KdbxFile {
(crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List) (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List)
.buffer); .buffer);
body.writeV3(writer, this, gen); body.writeV3(writer, this, gen);
dirtyObjects.clear();
return output.toBytes(); return output.toBytes();
} }
@ -76,6 +81,10 @@ class KdbxFile {
.cast<KdbxObject>() .cast<KdbxObject>()
.followedBy(body.rootGroup.getAllEntries()); .followedBy(body.rootGroup.getAllEntries());
void dirtyObject(KdbxObject kdbxObject) {
dirtyObjects.add(kdbxObject);
}
// void _subscribeToChildren() { // void _subscribeToChildren() {
// final allObjects = _allObjects; // final allObjects = _allObjects;
// for (final obj in allObjects) { // for (final obj in allObjects) {

3
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/internal/async_utils.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';
@ -9,7 +10,7 @@ import 'kdbx_object.dart';
class KdbxGroup extends KdbxObject { class KdbxGroup extends KdbxObject {
KdbxGroup.create({@required this.parent, @required String name}) KdbxGroup.create({@required this.parent, @required String name})
: super.create('Group') { : super.create(parent?.file, 'Group') {
this.name.set(name); this.name.set(name);
icon.set(KdbxIcon.Folder); icon.set(KdbxIcon.Folder);
expanded.set(true); expanded.set(true);

13
lib/src/kdbx_object.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:kdbx/src/kdbx_format.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:meta/meta.dart'; import 'package:meta/meta.dart';
@ -30,7 +31,7 @@ mixin Changeable<T> {
abstract class KdbxNode with Changeable<KdbxNode> { abstract class KdbxNode with Changeable<KdbxNode> {
KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)) { KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)) {
isDirty = true; _isDirty = true;
} }
KdbxNode.read(this.node); KdbxNode.read(this.node);
@ -48,13 +49,16 @@ abstract class KdbxNode with Changeable<KdbxNode> {
} }
abstract class KdbxObject extends KdbxNode { abstract class KdbxObject extends KdbxNode {
KdbxObject.create(String nodeName) KdbxObject.create(this.file, String nodeName)
: times = KdbxTimes.create(), super.create(nodeName) { : times = KdbxTimes.create(), super.create(nodeName) {
_uuid.set(KdbxUuid.random()); _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;
final KdbxTimes times; final KdbxTimes times;
KdbxUuid get uuid => _uuid.get(); KdbxUuid get uuid => _uuid.get();
@ -65,7 +69,10 @@ abstract class KdbxObject extends KdbxNode {
@override @override
set isDirty(bool dirty) { set isDirty(bool dirty) {
super.isDirty = dirty; super.isDirty = dirty;
times.modifiedNow(); if (dirty) {
times.modifiedNow();
file.dirtyObject(this);
}
} }
@override @override

8
lib/src/kdbx_xml.dart

@ -6,6 +6,14 @@ import 'package:kdbx/src/kdbx_consts.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:xml/xml.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<T> { abstract class KdbxSubNode<T> {
KdbxSubNode(this.node, this.name); KdbxSubNode(this.node, this.name);

4
test/kdbx_test.dart

@ -43,7 +43,7 @@ void main() {
test('Create Entry', () { test('Create Entry', () {
final kdbx = KdbxFormat.create(Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest'); final kdbx = KdbxFormat.create(Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest');
final rootGroup = kdbx.body.rootGroup; final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(rootGroup); final entry = KdbxEntry.create(kdbx, rootGroup);
rootGroup.addEntry(entry); rootGroup.addEntry(entry);
entry.setString(KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum')); entry.setString(KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum'));
print(kdbx.body.generateXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true)); print(kdbx.body.generateXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true));
@ -56,7 +56,7 @@ void main() {
final Uint8List saved = (() { final Uint8List saved = (() {
final kdbx = KdbxFormat.create(credentials, 'CreateTest'); final kdbx = KdbxFormat.create(credentials, 'CreateTest');
final rootGroup = kdbx.body.rootGroup; final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(rootGroup); final entry = KdbxEntry.create(kdbx, rootGroup);
rootGroup.addEntry(entry); rootGroup.addEntry(entry);
entry.setString( entry.setString(
KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum')); KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum'));

Loading…
Cancel
Save