diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index c923676..d210aef 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -38,8 +38,9 @@ class KdbxEntry extends KdbxObject { })); } + @override XmlElement toXml() { - final el = node.copy() as XmlElement; + final el = super.toXml(); el.children.removeWhere((e) => e is XmlElement && e.name.local == 'String'); el.children.addAll(strings.entries.map((stringEntry) { final value = XmlElement(XmlName('Value')); diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 8cd98cc..4bc77ee 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -92,7 +92,7 @@ class KdbxBody extends KdbxNode { meta.headerHash.set( (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List) .buffer); - final xml = toXml(saltGenerator); + final xml = generateXml(saltGenerator); final xmlBytes = utf8.encode(xml.toXmlString()); final Uint8List compressedBytes = (kdbxFile.header.compression == Compression.gzip ? GZipCodec().encode(xmlBytes) : xmlBytes) as Uint8List; @@ -108,7 +108,7 @@ class KdbxBody extends KdbxNode { writer.writeBytes(encrypted); } - xml.XmlDocument toXml(ProtectedSaltGenerator saltGenerator) { + xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) { final rootGroupNode = rootGroup.toXml(); // update protected values... for (final el in rootGroupNode @@ -150,8 +150,10 @@ class KdbxMeta extends KdbxNode { Base64Node get headerHash => Base64Node(this, 'HeaderHash'); + @override + // ignore: unnecessary_overrides xml.XmlElement toXml() { - return node; + return super.toXml(); } } @@ -223,7 +225,7 @@ class KdbxFormat { decryptCipher.init(false, ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List())); final paddedDecrypted = AesHelper.processBlocks(decryptCipher, encryptedPayload); - final decrypted = paddedDecrypted;//AesHelper.unpad(paddedDecrypted); + final decrypted = AesHelper.unpad(paddedDecrypted); final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes; @@ -236,6 +238,7 @@ class KdbxFormat { decrypted.sublist(0, streamStart.lengthInBytes))) { throw KdbxInvalidKeyException(); } + // ignore: unnecessary_cast final content = decrypted.sublist(streamStart.lengthInBytes) as Uint8List; return content; } diff --git a/lib/src/kdbx_group.dart b/lib/src/kdbx_group.dart index 28890b2..180ea49 100644 --- a/lib/src/kdbx_group.dart +++ b/lib/src/kdbx_group.dart @@ -24,8 +24,9 @@ class KdbxGroup extends KdbxObject { .forEach(_entries.add); } + @override XmlElement toXml() { - final el = node.copy() as XmlElement; + final el = super.toXml(); XmlUtils.removeChildrenByName(el, 'Group'); XmlUtils.removeChildrenByName(el, 'Entry'); el.children.addAll(groups.map((g) => g.toXml())); diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart index d492f84..d7e722c 100644 --- a/lib/src/kdbx_header.dart +++ b/lib/src/kdbx_header.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart' as crypto; @@ -6,7 +5,6 @@ import 'package:kdbx/src/internal/byte_utils.dart'; import 'package:kdbx/src/internal/consts.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; -import 'package:pointycastle/api.dart'; final _logger = Logger('kdbx.header'); @@ -266,6 +264,7 @@ class HashedBlockReader { static Iterable readNextBlock(ReaderHelper reader) sync* { while (true) { + // ignore: unused_local_variable final blockIndex = reader.readUint32(); final blockHash = reader.readBytes(HASH_SIZE); final blockSize = reader.readUint32(); diff --git a/lib/src/kdbx_object.dart b/lib/src/kdbx_object.dart index b7720b9..37b009f 100644 --- a/lib/src/kdbx_object.dart +++ b/lib/src/kdbx_object.dart @@ -1,22 +1,13 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:kdbx/src/kdbx_times.dart'; import 'package:kdbx/src/kdbx_xml.dart'; +import 'package:meta/meta.dart'; import 'package:uuid/uuid.dart'; import 'package:uuid/uuid_util.dart'; import 'package:xml/xml.dart'; -class KdbxTimes { - KdbxTimes.read(this.node); - - XmlElement node; - - DateTime get creationTime => _readTime('CreationTime'); - - DateTime _readTime(String nodeName) => - DateTime.parse(node.findElements(nodeName).single.text); -} - abstract class KdbxNode { KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)); @@ -29,21 +20,35 @@ abstract class KdbxNode { KdbxSubTextNode textNode(String nodeName) => StringNode(this, nodeName); + @mustCallSuper + XmlElement toXml() { + final el = node.copy() as XmlElement; + return el; + } } - abstract class KdbxObject extends KdbxNode { KdbxObject.create(String nodeName) - : super.create(nodeName) { + : times = KdbxTimes.create(), super.create(nodeName) { _uuid.set(KdbxUuid.random()); } - KdbxObject.read(XmlElement node) : super.read(node); + KdbxObject.read(XmlElement node) : times = KdbxTimes.read(node.findElements('Times').single),super.read(node); + + final KdbxTimes times; KdbxUuid get uuid => _uuid.get(); UuidNode get _uuid => UuidNode(this, 'UUID'); IconNode get icon => IconNode(this, 'IconID'); + + @override + XmlElement toXml() { + final el = super.toXml(); + XmlUtils.removeChildrenByName(el, 'Times'); + el.children.add(times.toXml()); + return el; + } } class KdbxUuid { diff --git a/lib/src/kdbx_times.dart b/lib/src/kdbx_times.dart new file mode 100644 index 0000000..888a665 --- /dev/null +++ b/lib/src/kdbx_times.dart @@ -0,0 +1,32 @@ + + +import 'package:clock/clock.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') { + final now = clock.now().toUtc(); + creationTime.set(now); + lastModificationTime.set(now); + lastAccessTime.set(now); + expiryTime.set(now); + expires.set(false); + usageCount.set(0); + locationChanged.set(now); + } + KdbxTimes.read(XmlElement node) : super.read(node); + + DateTimeUtcNode get creationTime => DateTimeUtcNode(this, 'CreationTime'); + DateTimeUtcNode get lastModificationTime => DateTimeUtcNode(this, 'CreationTime'); + DateTimeUtcNode get lastAccessTime => DateTimeUtcNode(this, 'CreationTime'); + DateTimeUtcNode get expiryTime => DateTimeUtcNode(this, 'CreationTime'); + BooleanNode get expires => BooleanNode(this, 'Expires'); + IntNode get usageCount => IntNode(this, 'Usagecount'); + DateTimeUtcNode get locationChanged => DateTimeUtcNode(this, 'LocationChanged'); + + void accessedNow() { + lastAccessTime.set(clock.now()); + } +} diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index 1455d0e..90231e7 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -26,8 +26,9 @@ abstract class KdbxSubTextNode extends KdbxSubNode { @protected T decode(String value); - XmlElement _opt(String nodeName) => - node.node.findElements(nodeName).singleWhere((x) => true, orElse: () => null); + XmlElement _opt(String nodeName) => node.node + .findElements(nodeName) + .singleWhere((x) => true, orElse: () => null); @override T get() { @@ -41,7 +42,7 @@ abstract class KdbxSubTextNode extends KdbxSubNode { @override void set(T value) { final el = - node.node.findElements(name).singleWhere((x) => true, orElse: () { + node.node.findElements(name).singleWhere((x) => true, orElse: () { final el = XmlElement(XmlName(name)); node.node.children.add(el); return el; @@ -58,6 +59,16 @@ abstract class KdbxSubTextNode extends KdbxSubNode { } } +class IntNode extends KdbxSubTextNode { + IntNode(KdbxNode node, String name) : super(node, name); + + @override + int decode(String value) => int.tryParse(value); + + @override + String encode(int value) => value.toString(); +} + class StringNode extends KdbxSubTextNode { StringNode(KdbxNode node, String name) : super(node, name); @@ -104,20 +115,73 @@ class BooleanNode extends KdbxSubTextNode { @override bool decode(String value) { switch (value) { - case 'null': return null; - case 'true': return true; - case 'false': return false; + case 'null': + return null; + case 'true': + return true; + case 'false': + return false; } throw KdbxCorruptedFileException('Invalid boolean value $value for $name'); } @override String encode(bool value) => value ? 'true' : 'false'; +} +class DateTimeUtcNode extends KdbxSubTextNode { + DateTimeUtcNode(KdbxNode node, String name) : super(node, name); + + @override + DateTime decode(String value) => DateTime.parse(value); + + @override + String encode(DateTime value) { + assert(value.isUtc); + + // TODO for kdbx v4 we need to support binary/base64 + return DateTimeUtils.toIso8601StringSeconds(value); + } } class XmlUtils { static void removeChildrenByName(XmlNode node, String name) { - node.children.removeWhere((node) => node is XmlElement && node.name.local == name); + node.children + .removeWhere((node) => node is XmlElement && node.name.local == name); } -} \ No newline at end of file +} + +class DateTimeUtils { + static String toIso8601StringSeconds(DateTime dateTime) { + final String y = _fourDigits(dateTime.year); + final String m = _twoDigits(dateTime.month); + final String d = _twoDigits(dateTime.hour); + final String h = _twoDigits(dateTime.hour); + final String min = _twoDigits(dateTime.minute); + final String sec = _twoDigits(dateTime.second); + return '$y-$m-${d}T$h:$min:${sec}Z'; + } + + static String _fourDigits(int n) { + final absN = n.abs(); + final sign = n < 0 ? '-' : ''; + // ignore: prefer_single_quotes + if (absN >= 1000) { + return '$n'; + } + if (absN >= 100) { + return '${sign}0$absN'; + } + if (absN >= 10) { + return '${sign}00$absN'; + } + return '${sign}000$absN'; + } + + static String _twoDigits(int n) { + if (n >= 10) { + return '$n'; + } + return '0$n'; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 1e27f9d..3c69876 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: xml: '>=3.5.0 <4.0.0' uuid: '>=2.0.0 <3.0.0' meta: '>=1.0.0 <2.0.0' + clock: '>=1.0.0 <2.0.0' collection: '>=1.14.0 <2.0.0' diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart index fda9415..5ea3481 100644 --- a/test/kdbx_test.dart +++ b/test/kdbx_test.dart @@ -38,7 +38,7 @@ void main() { expect(kdbx.body.rootGroup, isNotNull); expect(kdbx.body.rootGroup.name.get(), 'CreateTest'); expect(kdbx.body.meta.databaseName.get(), 'CreateTest'); - print(kdbx.body.toXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true)); + print(kdbx.body.generateXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true)); }); test('Create Entry', () { final kdbx = KdbxFormat.create(Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest'); @@ -46,14 +46,14 @@ void main() { final entry = KdbxEntry.create(rootGroup); rootGroup.addEntry(entry); entry.setString(KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum')); - print(kdbx.body.toXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true)); + print(kdbx.body.generateXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true)); }); }); group('Integration', () { test('Simple save and load', () { final credentials = Credentials(ProtectedValue.fromString('FooBar')); - Uint8List saved = (() { + final Uint8List saved = (() { final kdbx = KdbxFormat.create(credentials, 'CreateTest'); final rootGroup = kdbx.body.rootGroup; final entry = KdbxEntry.create(rootGroup); @@ -67,6 +67,7 @@ void main() { final kdbx = KdbxFormat.read(saved, credentials); expect(kdbx.body.rootGroup.entries.first.strings[KdbxKey('Password')].getText(), 'LoremIpsum'); + File('test.kdbx').writeAsBytesSync(saved); }); }); }