diff --git a/bin/kdbx.dart b/bin/kdbx.dart index c5a9966..96b6c61 100644 --- a/bin/kdbx.dart +++ b/bin/kdbx.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:kdbx/src/crypto/protected_value.dart'; import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_group.dart'; -import 'package:kdbx/src/kdbx_header.dart'; import 'package:logging/logging.dart'; import 'package:logging_appenders/logging_appenders.dart'; import 'package:prompts/prompts.dart' as prompts; @@ -71,7 +71,7 @@ abstract class KdbxFileCommand extends Command { if (inputFile == null) { usageException('Required argument: --input'); } - final bytes = await File(inputFile).readAsBytes(); + final bytes = await File(inputFile).readAsBytes() as Uint8List; final password = prompts.get('Password for $inputFile', conceal: true, validate: (str) => str.isNotEmpty); final file = await KdbxFormat.read( @@ -84,7 +84,8 @@ abstract class KdbxFileCommand extends Command { class CatCommand extends KdbxFileCommand { CatCommand() { - argParser.addFlag('decrypt', help: 'Force decryption of all protected strings.'); + argParser.addFlag('decrypt', + help: 'Force decryption of all protected strings.'); } @override @@ -108,7 +109,8 @@ class CatCommand extends KdbxFileCommand { } for (final entry in group.entries) { final value = entry.strings['Password']; - print('$indent `- ${entry.strings['Title']?.getText()}: ${forceDecrypt ? value?.getText() : value?.toString()}'); + print( + '$indent `- ${entry.strings['Title']?.getText()}: ${forceDecrypt ? value?.getText() : value?.toString()}'); } } } diff --git a/lib/kdbx.dart b/lib/kdbx.dart index b0b57af..5e82379 100644 --- a/lib/kdbx.dart +++ b/lib/kdbx.dart @@ -1,4 +1,13 @@ /// dart library for reading keepass file format (kdbx). library kdbx; +export 'src/crypto/protected_value.dart' show ProtectedValue, StringValue; +export 'src/kdbx_entry.dart'; export 'src/kdbx_format.dart'; +export 'src/kdbx_header.dart' + show + KdbxException, + KdbxInvalidKeyException, + KdbxCorruptedFileException, + KdbxUnsupportedException; +export 'src/kdbx_object.dart'; diff --git a/lib/src/internal/byte_utils.dart b/lib/src/internal/byte_utils.dart index d35467c..a625efd 100644 --- a/lib/src/internal/byte_utils.dart +++ b/lib/src/internal/byte_utils.dart @@ -1,7 +1,5 @@ -import 'dart:typed_data'; - class ByteUtils { - static bool eq(Uint8List a, Uint8List b) { + static bool eq(List a, List b) { if (a.length != b.length) { return false; } @@ -15,5 +13,5 @@ class ByteUtils { static String toHex(int val) => '0x${val.toRadixString(16)}'; - static String toHexList(Uint8List list) => list.map((val) => toHex(val)).join(' '); + static String toHexList(List list) => list.map((val) => toHex(val)).join(' '); } diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index 9d36e76..6ed264e 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -19,4 +19,14 @@ class KdbxEntry extends KdbxObject { KdbxGroup parent; Map strings; + + String _plainValue(String key) { + final value = strings[key]; + if (value is PlainValue) { + return value.getText(); + } + return value?.toString(); + } + + String get label => _plainValue('Title'); } diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index b4c71d6..440e545 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:convert/convert.dart' as convert; import 'package:crypto/crypto.dart' as crypto; import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/crypto/protected_value.dart'; @@ -13,8 +14,24 @@ import 'package:logging/logging.dart'; import 'package:pointycastle/export.dart'; import 'package:xml/xml.dart' as xml; +import 'kdbx_object.dart'; + final _logger = Logger('kdbx.format'); +class Credentials { + Credentials(this._password); + + final ProtectedValue _password; + + Uint8List getHash() { + final output = convert.AccumulatorSink(); + final input = crypto.sha256.startChunkedConversion(output); + input.add(_password.hash); + input.close(); + return output.events.single.bytes as Uint8List; + } +} + class KdbxFile { KdbxFile(this.credentials, this.header, this.body); @@ -37,7 +54,11 @@ class KdbxBody { final KdbxGroup rootGroup; } -class KdbxMeta {} +class KdbxMeta extends KdbxNode { + KdbxMeta.read(xml.XmlElement node) : super.read(node); + + String get databaseName => text('DatabaseName'); +} class KdbxFormat { static Future read(Uint8List input, Credentials credentials) async { @@ -89,7 +110,7 @@ class KdbxFormat { final root = keePassFile.findElements('Root').single; final rootGroup = KdbxGroup.read(null, root.findElements('Group').single); _logger.fine('got meta: ${meta.toXmlString(pretty: true)}'); - return KdbxBody(document, KdbxMeta(), rootGroup); + return KdbxBody(document, KdbxMeta.read(meta), rootGroup); } static Uint8List _decryptContent( @@ -111,7 +132,7 @@ class KdbxFormat { decrypted.sublist(0, streamStart.lengthInBytes))) { throw KdbxInvalidKeyException(); } - final content = decrypted.sublist(streamStart.lengthInBytes); + 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 dd6d2f5..93a7288 100644 --- a/lib/src/kdbx_group.dart +++ b/lib/src/kdbx_group.dart @@ -3,8 +3,6 @@ import 'package:xml/xml.dart'; import 'kdbx_object.dart'; -final _builder = XmlBuilder(); - class KdbxGroup extends KdbxObject { KdbxGroup(this.parent) : super.create('Group'); @@ -19,6 +17,15 @@ class KdbxGroup extends KdbxObject { .forEach(entries.add); } + /// Returns all groups plus this group itself. + List getAllGroups() => groups + .expand((g) => g.getAllGroups()) + .followedBy([this]).toList(growable: false); + + /// Returns all entries of this group and all sub groups. + List getAllEntries() => + getAllGroups().expand((g) => g.entries).toList(growable: false); + /// null if this is the root group. final KdbxGroup parent; final List groups = []; diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart index 8944d09..bb0dcb2 100644 --- a/lib/src/kdbx_header.dart +++ b/lib/src/kdbx_header.dart @@ -51,20 +51,14 @@ class HeaderField { } class KdbxHeader { - KdbxHeader( - {this.sig1, - this.sig2, - this.versionMinor, - this.versionMajor, - this.fields}); + KdbxHeader({this.sig1, this.sig2, this.versionMinor, this.versionMajor, this.fields}); static Future read(ReaderHelper reader) async { // reading signature final sig1 = reader.readUint32(); final sig2 = reader.readUint32(); if (!(sig1 == Consts.FileMagic && sig2 == Consts.Sig2Kdbx)) { - throw UnsupportedError( - 'Unsupported file structure. ${ByteUtils.toHex(sig1)}, ${ByteUtils.toHex(sig2)}'); + throw UnsupportedError('Unsupported file structure. ${ByteUtils.toHex(sig1)}, ${ByteUtils.toHex(sig2)}'); } // reading version @@ -72,8 +66,7 @@ class KdbxHeader { final versionMajor = reader.readUint16(); _logger.finer('Reading version: $versionMajor.$versionMinor'); - final headerFields = Map.fromEntries(readField(reader, versionMajor) - .map((field) => MapEntry(field.field, field))); + final headerFields = Map.fromEntries(readField(reader, versionMajor).map((field) => MapEntry(field.field, field))); return KdbxHeader( sig1: sig1, sig2: sig2, @@ -83,12 +76,10 @@ class KdbxHeader { ); } - static Iterable readField( - ReaderHelper reader, int versionMajor) sync* { + static Iterable readField(ReaderHelper reader, int versionMajor) sync* { while (true) { final headerId = reader.readUint8(); - final int bodySize = - versionMajor >= 4 ? reader.readUint32() : reader.readUint16(); + final int bodySize = versionMajor >= 4 ? reader.readUint32() : reader.readUint16(); _logger.finer('Read header ${HeaderFields.values[headerId]}'); final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null; if (headerId > 0) { @@ -117,22 +108,7 @@ class KdbxHeader { } PotectedValueEncryption get innerRandomStreamEncryption => - PotectedValueEncryption.values[ - fields[HeaderFields.InnerRandomStreamID].bytes.asUint32List().single]; -} - -class Credentials { - Credentials(this._password); - - final ProtectedValue _password; - - Uint8List getHash() { - final output = convert.AccumulatorSink(); - final input = crypto.sha256.startChunkedConversion(output); - input.add(_password.hash); - input.close(); - return output.events.single.bytes as Uint8List; - } + PotectedValueEncryption.values[fields[HeaderFields.InnerRandomStreamID].bytes.asUint32List().single]; } class KdbxException implements Exception {} @@ -158,8 +134,7 @@ class HashedBlockReader { final blockSize = reader.readUint32(); if (blockSize > 0) { final blockData = reader.readBytes(blockSize).asUint8List(); - if (!ByteUtils.eq(crypto.sha256.convert(blockData).bytes as Uint8List, - blockHash.asUint8List())) { + if (!ByteUtils.eq(crypto.sha256.convert(blockData).bytes as Uint8List, blockHash.asUint8List())) { throw KdbxCorruptedFileException(); } yield blockData; @@ -176,8 +151,7 @@ class ReaderHelper { final Uint8List data; int pos = 0; - ByteBuffer _nextByteBuffer(int byteCount) => - data.sublist(pos, pos += byteCount).buffer; + ByteBuffer _nextByteBuffer(int byteCount) => (data.sublist(pos, pos += byteCount) as Uint8List).buffer; int readUint32() => _nextByteBuffer(4).asUint32List().first; @@ -187,5 +161,5 @@ class ReaderHelper { ByteBuffer readBytes(int size) => _nextByteBuffer(size); - Uint8List readRemaining() => data.sublist(pos); + Uint8List readRemaining() => data.sublist(pos) as Uint8List; } diff --git a/lib/src/kdbx_object.dart b/lib/src/kdbx_object.dart index 895b9b4..f92224c 100644 --- a/lib/src/kdbx_object.dart +++ b/lib/src/kdbx_object.dart @@ -13,17 +13,11 @@ class KdbxTimes { DateTime.parse(node.findElements(nodeName).single.text); } -abstract class KdbxObject { - KdbxObject.create(String nodeName) - : uuid = Uuid().v4(), - node = XmlElement(XmlName(nodeName)); - - KdbxObject.read(this.node) { - uuid = node.findElements('UUID').single.text; - } +abstract class KdbxNode { + KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)); + KdbxNode.read(this.node); final XmlElement node; - String uuid; @protected String text(String nodeName) => _opt(nodeName)?.text; @@ -31,3 +25,15 @@ abstract class KdbxObject { XmlElement _opt(String nodeName) => node.findElements(nodeName).singleWhere((x) => true, orElse: () => null); } + +abstract class KdbxObject extends KdbxNode { + KdbxObject.create(String nodeName) + : uuid = Uuid().v4(), + super.create(nodeName); + + KdbxObject.read(XmlElement node) : super.read(node) { + uuid = node.findElements('UUID').single.text; + } + + String uuid; +} diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart index a9ef8d5..1acbcc2 100644 --- a/test/kdbx_test.dart +++ b/test/kdbx_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/crypto/protected_value.dart'; @@ -10,14 +11,13 @@ import 'package:test/test.dart'; void main() { Logger.root.level = Level.ALL; - Logger.root.onRecord.listen(PrintAppender().logListener()); + PrintAppender().attachToLogger(Logger.root); group('A group of tests', () { setUp(() {}); test('First Test', () async { - final data = await File('test/FooBar.kdbx').readAsBytes(); - await KdbxFormat.read( - data, Credentials(ProtectedValue.fromString('FooBar'))); + final data = await File('test/FooBar.kdbx').readAsBytes() as Uint8List; + await KdbxFormat.read(data, Credentials(ProtectedValue.fromString('FooBar'))); }); }); }