Browse Source

fix DateTime serializing (1. implement kdbx 4 serializing and 2. fix kdbx 3 hour/day mixup)

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
aa115f16a4
  1. 2
      example/pubspec.lock
  2. 2
      lib/src/kdbx_entry.dart
  3. 4
      lib/src/kdbx_file.dart
  4. 42
      lib/src/kdbx_format.dart
  5. 2
      lib/src/kdbx_group.dart
  6. 9
      lib/src/kdbx_meta.dart
  7. 11
      lib/src/kdbx_object.dart
  8. 9
      lib/src/kdbx_times.dart
  9. 19
      lib/src/kdbx_xml.dart
  10. 18
      test/kdbx4_test.dart
  11. 31
      test/kdbx_test.dart

2
example/pubspec.lock

@ -229,4 +229,4 @@ packages:
source: hosted source: hosted
version: "3.7.0" version: "3.7.0"
sdks: sdks:
dart: ">=2.7.0 <3.0.0" dart: ">=2.8.0 <3.0.0"

2
lib/src/kdbx_entry.dart

@ -43,7 +43,7 @@ class KdbxEntry extends KdbxObject {
KdbxEntry.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node, KdbxEntry.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node,
{this.isHistoryEntry = false}) {this.isHistoryEntry = false})
: history = [], : history = [],
super.read(parent, node) { super.read(ctx, parent, node) {
_strings.addEntries(node.findElements(KdbxXml.NODE_STRING).map((el) { _strings.addEntries(node.findElements(KdbxXml.NODE_STRING).map((el) {
final key = KdbxKey(el.findElements(KdbxXml.NODE_KEY).single.text); final key = KdbxKey(el.findElements(KdbxXml.NODE_KEY).single.text);
final valueNode = el.findElements(KdbxXml.NODE_VALUE).single; final valueNode = el.findElements(KdbxXml.NODE_VALUE).single;

4
lib/src/kdbx_file.dart

@ -13,7 +13,8 @@ import 'package:xml/xml.dart' as xml;
final _logger = Logger('kdbx_file'); final _logger = Logger('kdbx_file');
class KdbxFile { class KdbxFile {
KdbxFile(this.kdbxFormat, this.credentials, this.header, this.body) { KdbxFile(
this.ctx, this.kdbxFormat, this.credentials, this.header, this.body) {
for (final obj in _allObjects) { for (final obj in _allObjects) {
obj.file = this; obj.file = this;
} }
@ -31,6 +32,7 @@ class KdbxFile {
} }
final KdbxFormat kdbxFormat; final KdbxFormat kdbxFormat;
final KdbxReadWriteContext ctx;
final Credentials credentials; final Credentials credentials;
final KdbxHeader header; final KdbxHeader header;
final KdbxBody body; final KdbxBody body;

42
lib/src/kdbx_format.dart

@ -64,11 +64,31 @@ class KeyFileComposite implements Credentials {
/// Context used during reading and writing. /// Context used during reading and writing.
class KdbxReadWriteContext { class KdbxReadWriteContext {
KdbxReadWriteContext({@required this.binaries}) : assert(binaries != null); KdbxReadWriteContext({@required this.binaries, @required this.header})
: assert(binaries != null),
assert(header != null);
static final kdbxContext = Expando<KdbxReadWriteContext>();
static KdbxReadWriteContext kdbxContextForNode(xml.XmlParent node) {
final ret = kdbxContext[node.document];
if (ret == null) {
throw StateError('Unable to locate kdbx context for document.');
}
return ret;
}
static void setKdbxContextForNode(
xml.XmlParent node, KdbxReadWriteContext ctx) {
kdbxContext[node.document] = ctx;
}
@protected @protected
final List<KdbxBinary> binaries; final List<KdbxBinary> binaries;
final KdbxHeader header;
int get versionMajor => header.versionMajor;
KdbxBinary binaryById(int id) { KdbxBinary binaryById(int id) {
if (id >= binaries.length) { if (id >= binaries.length) {
return null; return null;
@ -278,16 +298,20 @@ class KdbxFormat {
String generator, String generator,
KdbxHeader header, KdbxHeader header,
}) { }) {
header ??= KdbxHeader.create();
final ctx = KdbxReadWriteContext(binaries: [], header: header);
final meta = KdbxMeta.create( final meta = KdbxMeta.create(
databaseName: name, databaseName: name,
ctx: ctx,
generator: generator, generator: generator,
); );
final rootGroup = KdbxGroup.create(parent: null, name: name); final rootGroup = KdbxGroup.create(parent: null, name: name);
final body = KdbxBody.create(meta, rootGroup); final body = KdbxBody.create(meta, rootGroup);
return KdbxFile( return KdbxFile(
ctx,
this, this,
credentials, credentials,
header ?? KdbxHeader.create(), header,
body, body,
); );
} }
@ -349,13 +373,14 @@ class KdbxFormat {
final blocks = HashedBlockReader.readBlocks(ReaderHelper(content)); final blocks = HashedBlockReader.readBlocks(ReaderHelper(content));
_logger.finer('compression: ${header.compression}'); _logger.finer('compression: ${header.compression}');
final ctx = KdbxReadWriteContext(binaries: []); final ctx = KdbxReadWriteContext(binaries: [], header: header);
if (header.compression == Compression.gzip) { if (header.compression == Compression.gzip) {
final xml = GZipCodec().decode(blocks); final xml = GZipCodec().decode(blocks);
final string = utf8.decode(xml); final string = utf8.decode(xml);
return KdbxFile(this, credentials, header, _loadXml(ctx, header, string)); return KdbxFile(
ctx, this, credentials, header, _loadXml(ctx, header, string));
} else { } else {
return KdbxFile(this, credentials, header, return KdbxFile(ctx, this, credentials, header,
_loadXml(ctx, header, utf8.decode(blocks))); _loadXml(ctx, header, utf8.decode(blocks)));
} }
} }
@ -396,9 +421,9 @@ class KdbxFormat {
// header.innerFields.addAll(headerFields); // header.innerFields.addAll(headerFields);
header.innerHeader.updateFrom(innerHeader); header.innerHeader.updateFrom(innerHeader);
final xml = utf8.decode(contentReader.readRemaining()); final xml = utf8.decode(contentReader.readRemaining());
final context = KdbxReadWriteContext(binaries: []); final context = KdbxReadWriteContext(binaries: [], header: header);
return KdbxFile( return KdbxFile(
this, credentials, header, _loadXml(context, header, xml)); context, this, credentials, header, _loadXml(context, header, xml));
} }
throw StateError('Kdbx4 without compression is not yet supported.'); throw StateError('Kdbx4 without compression is not yet supported.');
} }
@ -547,6 +572,7 @@ class KdbxFormat {
final gen = _createProtectedSaltGenerator(header); final gen = _createProtectedSaltGenerator(header);
final document = xml.parse(xmlString); final document = xml.parse(xmlString);
KdbxReadWriteContext.setKdbxContextForNode(document, ctx);
for (final el in document for (final el in document
.findAllElements(KdbxXml.NODE_VALUE) .findAllElements(KdbxXml.NODE_VALUE)
@ -562,7 +588,7 @@ class KdbxFormat {
final meta = keePassFile.findElements('Meta').single; final meta = keePassFile.findElements('Meta').single;
final root = keePassFile.findElements('Root').single; final root = keePassFile.findElements('Root').single;
final kdbxMeta = KdbxMeta.read(meta); final kdbxMeta = KdbxMeta.read(meta, ctx);
if (kdbxMeta.binaries?.isNotEmpty == true) { if (kdbxMeta.binaries?.isNotEmpty == true) {
ctx.binaries.addAll(kdbxMeta.binaries); ctx.binaries.addAll(kdbxMeta.binaries);
} else if (header.innerHeader.binaries.isNotEmpty) { } else if (header.innerHeader.binaries.isNotEmpty) {

2
lib/src/kdbx_group.dart

@ -20,7 +20,7 @@ class KdbxGroup extends KdbxObject {
} }
KdbxGroup.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node) KdbxGroup.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node)
: super.read(parent, node) { : super.read(ctx, parent, node) {
node node
.findElements('Group') .findElements('Group')
.map((el) => KdbxGroup.read(ctx, this, el)) .map((el) => KdbxGroup.read(ctx, this, el))

9
lib/src/kdbx_meta.dart

@ -1,3 +1,4 @@
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';
@ -7,9 +8,10 @@ import 'package:kdbx/src/kdbx_xml.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
class KdbxMeta extends KdbxNode { class KdbxMeta extends KdbxNode implements KdbxNodeContext {
KdbxMeta.create({ KdbxMeta.create({
@required String databaseName, @required String databaseName,
@required this.ctx,
String generator, String generator,
}) : customData = KdbxCustomData.create(), }) : customData = KdbxCustomData.create(),
binaries = [], binaries = [],
@ -18,7 +20,7 @@ class KdbxMeta extends KdbxNode {
this.generator.set(generator ?? 'kdbx.dart'); this.generator.set(generator ?? 'kdbx.dart');
} }
KdbxMeta.read(xml.XmlElement node) KdbxMeta.read(xml.XmlElement node, this.ctx)
: customData = node : customData = node
.singleElement('CustomData') .singleElement('CustomData')
?.let((e) => KdbxCustomData.read(e)) ?? ?.let((e) => KdbxCustomData.read(e)) ??
@ -37,6 +39,9 @@ class KdbxMeta extends KdbxNode {
})?.toList(), })?.toList(),
super.read(node); super.read(node);
@override
final KdbxReadWriteContext ctx;
final KdbxCustomData customData; final KdbxCustomData customData;
/// only used in Kdbx 3 /// only used in Kdbx 3

11
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/kdbx.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_group.dart';
import 'package:kdbx/src/kdbx_times.dart'; import 'package:kdbx/src/kdbx_times.dart';
@ -43,6 +44,10 @@ mixin Changeable<T> {
bool get isDirty => _isDirty; bool get isDirty => _isDirty;
} }
abstract class KdbxNodeContext implements KdbxNode {
KdbxReadWriteContext get ctx;
}
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;
@ -67,14 +72,14 @@ abstract class KdbxNode with Changeable<KdbxNode> {
abstract class KdbxObject extends KdbxNode { abstract class KdbxObject extends KdbxNode {
KdbxObject.create(this.file, String nodeName, KdbxGroup parent) KdbxObject.create(this.file, String nodeName, KdbxGroup parent)
: times = KdbxTimes.create(), : times = KdbxTimes.create(file.ctx),
_parent = parent, _parent = parent,
super.create(nodeName) { super.create(nodeName) {
_uuid.set(KdbxUuid.random()); _uuid.set(KdbxUuid.random());
} }
KdbxObject.read(KdbxGroup parent, XmlElement node) KdbxObject.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node)
: times = KdbxTimes.read(node.findElements('Times').single), : times = KdbxTimes.read(node.findElements('Times').single, ctx),
_parent = parent, _parent = parent,
super.read(node); super.read(node);

9
lib/src/kdbx_times.dart

@ -1,10 +1,11 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:kdbx/kdbx.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:xml/xml.dart'; import 'package:xml/xml.dart';
class KdbxTimes extends KdbxNode { class KdbxTimes extends KdbxNode implements KdbxNodeContext {
KdbxTimes.create() : super.create('Times') { KdbxTimes.create(this.ctx) : super.create('Times') {
final now = clock.now().toUtc(); final now = clock.now().toUtc();
creationTime.set(now); creationTime.set(now);
lastModificationTime.set(now); lastModificationTime.set(now);
@ -14,7 +15,9 @@ class KdbxTimes extends KdbxNode {
usageCount.set(0); usageCount.set(0);
locationChanged.set(now); locationChanged.set(now);
} }
KdbxTimes.read(XmlElement node) : super.read(node); KdbxTimes.read(XmlElement node, this.ctx) : super.read(node);
final KdbxReadWriteContext ctx;
DateTimeUtcNode get creationTime => DateTimeUtcNode(this, 'CreationTime'); DateTimeUtcNode get creationTime => DateTimeUtcNode(this, 'CreationTime');
DateTimeUtcNode get lastModificationTime => DateTimeUtcNode get lastModificationTime =>

19
lib/src/kdbx_xml.dart

@ -158,7 +158,11 @@ class BooleanNode extends KdbxSubTextNode<bool> {
} }
class DateTimeUtcNode extends KdbxSubTextNode<DateTime> { class DateTimeUtcNode extends KdbxSubTextNode<DateTime> {
DateTimeUtcNode(KdbxNode node, String name) : super(node, name); DateTimeUtcNode(KdbxNodeContext node, String name) : super(node, name);
static const EpochSeconds = 62135596800;
KdbxReadWriteContext get _ctx => (node as KdbxNodeContext).ctx;
void setToNow() { void setToNow() {
set(clock.now().toUtc()); set(clock.now().toUtc());
@ -174,7 +178,6 @@ class DateTimeUtcNode extends KdbxSubTextNode<DateTime> {
} }
// kdbx 4.x uses base64 encoded date. // kdbx 4.x uses base64 encoded date.
final decoded = base64.decode(value); final decoded = base64.decode(value);
const EpochSeconds = 62135596800;
final secondsFrom00 = ReaderHelper(decoded).readUint64(); final secondsFrom00 = ReaderHelper(decoded).readUint64();
@ -186,8 +189,14 @@ class DateTimeUtcNode extends KdbxSubTextNode<DateTime> {
@override @override
String encode(DateTime value) { String encode(DateTime value) {
assert(value.isUtc); assert(value.isUtc);
if (_ctx.versionMajor >= 4) {
// TODO for kdbx v4 we need to support binary/base64 // for kdbx v4 we need to support binary/base64
final secondsFrom00 =
(value.millisecondsSinceEpoch ~/ 1000) + EpochSeconds;
final encoded = base64.encode(
(WriterHelper()..writeUint64(secondsFrom00)).output.toBytes());
return encoded;
}
return DateTimeUtils.toIso8601StringSeconds(value); return DateTimeUtils.toIso8601StringSeconds(value);
} }
} }
@ -212,7 +221,7 @@ class DateTimeUtils {
static String toIso8601StringSeconds(DateTime dateTime) { static String toIso8601StringSeconds(DateTime dateTime) {
final y = _fourDigits(dateTime.year); final y = _fourDigits(dateTime.year);
final m = _twoDigits(dateTime.month); final m = _twoDigits(dateTime.month);
final d = _twoDigits(dateTime.hour); final d = _twoDigits(dateTime.day);
final h = _twoDigits(dateTime.hour); final h = _twoDigits(dateTime.hour);
final min = _twoDigits(dateTime.minute); final min = _twoDigits(dateTime.minute);
final sec = _twoDigits(dateTime.second); final sec = _twoDigits(dateTime.second);

18
test/kdbx4_test.dart

@ -33,6 +33,24 @@ void main() {
final pwd = firstEntry.getString(KdbxKey('Password')).getText(); final pwd = firstEntry.getString(KdbxKey('Password')).getText();
expect(pwd, 'def'); expect(pwd, 'def');
}); });
test('Reading kdbx4_keeweb modification time', () async {
final file = await TestUtil.readKdbxFile('test/kdbx4_keeweb.kdbx');
final firstEntry = file.body.rootGroup.entries.first;
final modTime = firstEntry.times.lastModificationTime.get();
expect(modTime, DateTime.utc(2020, 2, 26, 13, 40, 48));
});
test('Change kdbx4 modification time', () async {
final file = await TestUtil.readKdbxFile('test/kdbx4_keeweb.kdbx');
final firstEntry = file.body.rootGroup.entries.first;
final d = DateTime.utc(2020, 4, 5, 10, 0);
firstEntry.times.lastModificationTime.set(d);
final saved = await file.save();
{
final file2 = await TestUtil.readKdbxFileBytes(saved);
final firstEntry = file2.body.rootGroup.entries.first;
expect(firstEntry.times.lastModificationTime.get(), d);
}
});
test('Binary Keyfile', () async { test('Binary Keyfile', () async {
final data = final data =
await File('test/keyfile/BinaryKeyFilePasswords.kdbx').readAsBytes(); await File('test/keyfile/BinaryKeyFilePasswords.kdbx').readAsBytes();

31
test/kdbx_test.dart

@ -1,5 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/crypto/protected_salt_generator.dart';
@ -9,6 +8,8 @@ import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart'; import 'package:logging_appenders/logging_appenders.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'internal/test_utils.dart';
class FakeProtectedSaltGenerator implements ProtectedSaltGenerator { class FakeProtectedSaltGenerator implements ProtectedSaltGenerator {
@override @override
String decryptBase64(String protectedValue) => 'fake'; String decryptBase64(String protectedValue) => 'fake';
@ -78,6 +79,34 @@ void main() {
}); });
}); });
group('times', () {
test('read mod date time', () async {
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx');
final first = file.body.rootGroup.entries.first;
expect(file.header.versionMajor, 3);
expect(first.getString(KdbxKey('Title')).getText(), 'Sample Entry');
final modTime = first.times.lastModificationTime.get();
expect(modTime, DateTime.utc(2020, 5, 6, 7, 31, 48));
});
test('update mod date time', () async {
final newModDate = DateTime.utc(2020, 1, 2, 3, 4, 5);
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx');
{
final first = file.body.rootGroup.entries.first;
expect(file.header.versionMajor, 3);
expect(first.getString(KdbxKey('Title')).getText(), 'Sample Entry');
first.times.lastModificationTime.set(newModDate);
}
final saved = await file.save();
{
final file = await TestUtil.readKdbxFileBytes(saved);
final first = file.body.rootGroup.entries.first;
final modTime = first.times.lastModificationTime.get();
expect(modTime, newModDate);
}
});
});
group('Integration', () { group('Integration', () {
test('Simple save and load', () async { test('Simple save and load', () async {
final credentials = Credentials(ProtectedValue.fromString('FooBar')); final credentials = Credentials(ProtectedValue.fromString('FooBar'));

Loading…
Cancel
Save