diff --git a/example/pubspec.lock b/example/pubspec.lock index 75ae5b8..b51bd78 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -229,4 +229,4 @@ packages: source: hosted version: "3.7.0" sdks: - dart: ">=2.7.0 <3.0.0" + dart: ">=2.8.0 <3.0.0" diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index 5fa56e2..b1368a6 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -43,7 +43,7 @@ class KdbxEntry extends KdbxObject { KdbxEntry.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node, {this.isHistoryEntry = false}) : history = [], - super.read(parent, node) { + super.read(ctx, parent, 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; diff --git a/lib/src/kdbx_file.dart b/lib/src/kdbx_file.dart index eb70523..20a7ac0 100644 --- a/lib/src/kdbx_file.dart +++ b/lib/src/kdbx_file.dart @@ -13,7 +13,8 @@ import 'package:xml/xml.dart' as xml; final _logger = Logger('kdbx_file'); 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) { obj.file = this; } @@ -31,6 +32,7 @@ class KdbxFile { } final KdbxFormat kdbxFormat; + final KdbxReadWriteContext ctx; final Credentials credentials; final KdbxHeader header; final KdbxBody body; diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index a5b7011..a399706 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -64,11 +64,31 @@ class KeyFileComposite implements Credentials { /// Context used during reading and writing. 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(); + 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 final List binaries; + final KdbxHeader header; + + int get versionMajor => header.versionMajor; + KdbxBinary binaryById(int id) { if (id >= binaries.length) { return null; @@ -278,16 +298,20 @@ class KdbxFormat { String generator, KdbxHeader header, }) { + header ??= KdbxHeader.create(); + final ctx = KdbxReadWriteContext(binaries: [], header: header); final meta = KdbxMeta.create( databaseName: name, + ctx: ctx, generator: generator, ); final rootGroup = KdbxGroup.create(parent: null, name: name); final body = KdbxBody.create(meta, rootGroup); return KdbxFile( + ctx, this, credentials, - header ?? KdbxHeader.create(), + header, body, ); } @@ -349,13 +373,14 @@ class KdbxFormat { final blocks = HashedBlockReader.readBlocks(ReaderHelper(content)); _logger.finer('compression: ${header.compression}'); - final ctx = KdbxReadWriteContext(binaries: []); + final ctx = KdbxReadWriteContext(binaries: [], header: header); if (header.compression == Compression.gzip) { final xml = GZipCodec().decode(blocks); 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 { - return KdbxFile(this, credentials, header, + return KdbxFile(ctx, this, credentials, header, _loadXml(ctx, header, utf8.decode(blocks))); } } @@ -396,9 +421,9 @@ class KdbxFormat { // header.innerFields.addAll(headerFields); header.innerHeader.updateFrom(innerHeader); final xml = utf8.decode(contentReader.readRemaining()); - final context = KdbxReadWriteContext(binaries: []); + final context = KdbxReadWriteContext(binaries: [], header: header); 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.'); } @@ -547,6 +572,7 @@ class KdbxFormat { final gen = _createProtectedSaltGenerator(header); final document = xml.parse(xmlString); + KdbxReadWriteContext.setKdbxContextForNode(document, ctx); for (final el in document .findAllElements(KdbxXml.NODE_VALUE) @@ -562,7 +588,7 @@ class KdbxFormat { final meta = keePassFile.findElements('Meta').single; final root = keePassFile.findElements('Root').single; - final kdbxMeta = KdbxMeta.read(meta); + final kdbxMeta = KdbxMeta.read(meta, ctx); if (kdbxMeta.binaries?.isNotEmpty == true) { ctx.binaries.addAll(kdbxMeta.binaries); } else if (header.innerHeader.binaries.isNotEmpty) { diff --git a/lib/src/kdbx_group.dart b/lib/src/kdbx_group.dart index 5da2bb8..ac68d77 100644 --- a/lib/src/kdbx_group.dart +++ b/lib/src/kdbx_group.dart @@ -20,7 +20,7 @@ class KdbxGroup extends KdbxObject { } KdbxGroup.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node) - : super.read(parent, node) { + : super.read(ctx, parent, node) { node .findElements('Group') .map((el) => KdbxGroup.read(ctx, this, el)) diff --git a/lib/src/kdbx_meta.dart b/lib/src/kdbx_meta.dart index 08e8db1..cfbc859 100644 --- a/lib/src/kdbx_meta.dart +++ b/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/kdbx_binary.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:xml/xml.dart' as xml; -class KdbxMeta extends KdbxNode { +class KdbxMeta extends KdbxNode implements KdbxNodeContext { KdbxMeta.create({ @required String databaseName, + @required this.ctx, String generator, }) : customData = KdbxCustomData.create(), binaries = [], @@ -18,7 +20,7 @@ class KdbxMeta extends KdbxNode { this.generator.set(generator ?? 'kdbx.dart'); } - KdbxMeta.read(xml.XmlElement node) + KdbxMeta.read(xml.XmlElement node, this.ctx) : customData = node .singleElement('CustomData') ?.let((e) => KdbxCustomData.read(e)) ?? @@ -37,6 +39,9 @@ class KdbxMeta extends KdbxNode { })?.toList(), super.read(node); + @override + final KdbxReadWriteContext ctx; + final KdbxCustomData customData; /// only used in Kdbx 3 diff --git a/lib/src/kdbx_object.dart b/lib/src/kdbx_object.dart index 65c5f97..dcdc3fb 100644 --- a/lib/src/kdbx_object.dart +++ b/lib/src/kdbx_object.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; +import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_times.dart'; @@ -43,6 +44,10 @@ mixin Changeable { bool get isDirty => _isDirty; } +abstract class KdbxNodeContext implements KdbxNode { + KdbxReadWriteContext get ctx; +} + abstract class KdbxNode with Changeable { KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)) { _isDirty = true; @@ -67,14 +72,14 @@ abstract class KdbxNode with Changeable { abstract class KdbxObject extends KdbxNode { KdbxObject.create(this.file, String nodeName, KdbxGroup parent) - : times = KdbxTimes.create(), + : times = KdbxTimes.create(file.ctx), _parent = parent, super.create(nodeName) { _uuid.set(KdbxUuid.random()); } - KdbxObject.read(KdbxGroup parent, XmlElement node) - : times = KdbxTimes.read(node.findElements('Times').single), + KdbxObject.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node) + : times = KdbxTimes.read(node.findElements('Times').single, ctx), _parent = parent, super.read(node); diff --git a/lib/src/kdbx_times.dart b/lib/src/kdbx_times.dart index 80f8d8a..f5ce711 100644 --- a/lib/src/kdbx_times.dart +++ b/lib/src/kdbx_times.dart @@ -1,10 +1,11 @@ import 'package:clock/clock.dart'; +import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_xml.dart'; import 'package:xml/xml.dart'; -class KdbxTimes extends KdbxNode { - KdbxTimes.create() : super.create('Times') { +class KdbxTimes extends KdbxNode implements KdbxNodeContext { + KdbxTimes.create(this.ctx) : super.create('Times') { final now = clock.now().toUtc(); creationTime.set(now); lastModificationTime.set(now); @@ -14,7 +15,9 @@ class KdbxTimes extends KdbxNode { usageCount.set(0); 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 lastModificationTime => diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index 08f0f85..e3b823c 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -158,7 +158,11 @@ class BooleanNode extends KdbxSubTextNode { } class DateTimeUtcNode extends KdbxSubTextNode { - 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() { set(clock.now().toUtc()); @@ -174,7 +178,6 @@ class DateTimeUtcNode extends KdbxSubTextNode { } // kdbx 4.x uses base64 encoded date. final decoded = base64.decode(value); - const EpochSeconds = 62135596800; final secondsFrom00 = ReaderHelper(decoded).readUint64(); @@ -186,8 +189,14 @@ class DateTimeUtcNode extends KdbxSubTextNode { @override String encode(DateTime value) { assert(value.isUtc); - - // TODO for kdbx v4 we need to support binary/base64 + if (_ctx.versionMajor >= 4) { + // 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); } } @@ -212,7 +221,7 @@ class DateTimeUtils { static String toIso8601StringSeconds(DateTime dateTime) { final y = _fourDigits(dateTime.year); final m = _twoDigits(dateTime.month); - final d = _twoDigits(dateTime.hour); + final d = _twoDigits(dateTime.day); final h = _twoDigits(dateTime.hour); final min = _twoDigits(dateTime.minute); final sec = _twoDigits(dateTime.second); diff --git a/test/kdbx4_test.dart b/test/kdbx4_test.dart index 6c8cd72..52f0a1e 100644 --- a/test/kdbx4_test.dart +++ b/test/kdbx4_test.dart @@ -33,6 +33,24 @@ void main() { final pwd = firstEntry.getString(KdbxKey('Password')).getText(); 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 { final data = await File('test/keyfile/BinaryKeyFilePasswords.kdbx').readAsBytes(); diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart index bb2f8d5..2aebd1f 100644 --- a/test/kdbx_test.dart +++ b/test/kdbx_test.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:kdbx/kdbx.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:test/test.dart'; +import 'internal/test_utils.dart'; + class FakeProtectedSaltGenerator implements ProtectedSaltGenerator { @override 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', () { test('Simple save and load', () async { final credentials = Credentials(ProtectedValue.fromString('FooBar'));