diff --git a/bin/kdbx.dart b/bin/kdbx.dart index 85c285b..18a97c0 100644 --- a/bin/kdbx.dart +++ b/bin/kdbx.dart @@ -131,6 +131,9 @@ class CatCommand extends KdbxFileCommand { final value = entry.getString(KdbxKey('Password')); print( '$indent `- ${entry.getString(KdbxKey('Title'))?.getText()}: ${forceDecrypt ? value?.getText() : value?.toString()}'); + print(entry.binaryEntries + .map((b) => '$indent `- file: ${b.key} - ${b.value.value.length}') + .join('\n')); } } } diff --git a/lib/src/kdbx_binary.dart b/lib/src/kdbx_binary.dart new file mode 100644 index 0000000..f737df6 --- /dev/null +++ b/lib/src/kdbx_binary.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:kdbx/src/kdbx_header.dart'; +import 'package:kdbx/src/kdbx_xml.dart'; +import 'package:meta/meta.dart'; +import 'package:xml/xml.dart'; + +class KdbxBinary { + KdbxBinary({this.isInline, this.isProtected, this.value}); + final bool isInline; + final bool isProtected; + final Uint8List value; + + static KdbxBinary readBinaryInnerHeader(InnerHeaderField field) { + final flags = field.bytes[0]; + final isProtected = flags & 0x01 == 0x01; + final value = Uint8List.sublistView(field.bytes, 1); + return KdbxBinary( + isInline: false, + isProtected: isProtected, + value: value, + ); + } + + static KdbxBinary readBinaryXml(XmlElement valueNode, + {@required bool isInline}) { + assert(isInline != null); + final isProtected = valueNode.getAttributeBool(KdbxXml.ATTR_PROTECTED); + final isCompressed = valueNode.getAttributeBool(KdbxXml.ATTR_COMPRESSED); + var value = base64.decode(valueNode.text.trim()); + if (isCompressed) { + value = gzip.decode(value) as Uint8List; + } + return KdbxBinary( + isInline: isInline, + isProtected: isProtected, + value: value, + ); + } +} diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index c1e32f0..96a7285 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -1,7 +1,13 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/crypto/protected_value.dart'; +import 'package:kdbx/src/kdbx_binary.dart'; import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_group.dart'; +import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_xml.dart'; import 'package:logging/logging.dart'; @@ -36,7 +42,7 @@ class KdbxEntry extends KdbxObject { icon.set(KdbxIcon.Key); } - KdbxEntry.read(KdbxGroup parent, XmlElement node, + KdbxEntry.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node, {this.isHistoryEntry = false}) : super.read(parent, node) { _strings.addEntries(node.findElements(KdbxXml.NODE_STRING).map((el) { @@ -49,18 +55,32 @@ class KdbxEntry extends KdbxObject { return MapEntry(key, PlainValue(valueNode.text)); } })); + _binaries.addEntries(node.findElements(KdbxXml.NODE_BINARY).map((el) { + final key = KdbxKey(el.findElements(KdbxXml.NODE_KEY).single.text); + final valueNode = el.findElements(KdbxXml.NODE_VALUE).single; + final ref = valueNode.getAttribute(KdbxXml.ATTR_REF); + if (ref != null) { + final refId = int.parse(ref); + final binary = ctx.binaryById(refId); + if (binary == null) { + throw KdbxCorruptedFileException( + 'Unable to find binary with id $refId'); + } + return MapEntry(key, binary); + } + + return MapEntry(key, KdbxBinary.readBinaryXml(valueNode, isInline: true)); + })); + history = _historyElement + .findElements('Entry') + .map( + (entry) => KdbxEntry.read(ctx, parent, entry, isHistoryEntry: true)) + .toList(); } final bool isHistoryEntry; - List _history; - - List get history => _history ??= (() { - return _historyElement - .findElements('Entry') - .map((entry) => KdbxEntry.read(parent, entry, isHistoryEntry: true)) - .toList(); - })(); + List history; XmlElement get _historyElement => node .findElements(KdbxXml.NODE_HISTORY) @@ -114,6 +134,11 @@ class KdbxEntry extends KdbxObject { final Map _strings = {}; + final Map _binaries = {}; + + Iterable> get binaryEntries => + _binaries.entries; + // Map get strings => UnmodifiableMapView(_strings); Iterable> get stringEntries => diff --git a/lib/src/kdbx_file.dart b/lib/src/kdbx_file.dart index 16926c4..eb70523 100644 --- a/lib/src/kdbx_file.dart +++ b/lib/src/kdbx_file.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:kdbx/src/crypto/protected_value.dart'; +import 'package:kdbx/src/kdbx_dao.dart'; import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_object.dart'; import 'package:logging/logging.dart'; import 'package:xml/xml.dart' as xml; -import 'package:kdbx/src/kdbx_dao.dart'; final _logger = Logger('kdbx_file'); diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index ee50301..a5b7011 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -14,6 +14,7 @@ import 'package:kdbx/src/crypto/protected_value.dart'; import 'package:kdbx/src/internal/byte_utils.dart'; import 'package:kdbx/src/internal/consts.dart'; import 'package:kdbx/src/internal/crypto_utils.dart'; +import 'package:kdbx/src/kdbx_binary.dart'; import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_header.dart'; @@ -61,6 +62,21 @@ class KeyFileComposite implements Credentials { } } +/// Context used during reading and writing. +class KdbxReadWriteContext { + KdbxReadWriteContext({@required this.binaries}) : assert(binaries != null); + + @protected + final List binaries; + + KdbxBinary binaryById(int id) { + if (id >= binaries.length) { + return null; + } + return binaries[id]; + } +} + abstract class CredentialsPart { Uint8List getBinary(); } @@ -129,8 +145,11 @@ class KdbxBody extends KdbxNode { rootNode.children.add(rootGroup.node); } - KdbxBody.read(xml.XmlElement node, this.meta, this.rootGroup) - : super.read(node); + KdbxBody.read( + xml.XmlElement node, + this.meta, + this.rootGroup, + ) : super.read(node); // final xml.XmlDocument xmlDocument; final KdbxMeta meta; @@ -330,13 +349,14 @@ class KdbxFormat { final blocks = HashedBlockReader.readBlocks(ReaderHelper(content)); _logger.finer('compression: ${header.compression}'); + final ctx = KdbxReadWriteContext(binaries: []); if (header.compression == Compression.gzip) { final xml = GZipCodec().decode(blocks); final string = utf8.decode(xml); - return KdbxFile(this, credentials, header, _loadXml(header, string)); + return KdbxFile(this, credentials, header, _loadXml(ctx, header, string)); } else { - return KdbxFile( - this, credentials, header, _loadXml(header, utf8.decode(blocks))); + return KdbxFile(this, credentials, header, + _loadXml(ctx, header, utf8.decode(blocks))); } } @@ -370,11 +390,15 @@ class KdbxFormat { if (header.compression == Compression.gzip) { final content = GZipCodec().decode(decrypted) as Uint8List; final contentReader = ReaderHelper(content); - final headerFields = KdbxHeader.readInnerHeaderFields(contentReader, 4); + final innerHeader = KdbxHeader.readInnerHeaderFields(contentReader, 4); + // _logger.fine('inner header fields: $headerFields'); - header.innerFields.addAll(headerFields); +// header.innerFields.addAll(headerFields); + header.innerHeader.updateFrom(innerHeader); final xml = utf8.decode(contentReader.readRemaining()); - return KdbxFile(this, credentials, header, _loadXml(header, xml)); + final context = KdbxReadWriteContext(binaries: []); + return KdbxFile( + this, credentials, header, _loadXml(context, header, xml)); } throw StateError('Kdbx4 without compression is not yet supported.'); } @@ -518,14 +542,15 @@ class KdbxFormat { } } - KdbxBody _loadXml(KdbxHeader header, String xmlString) { + KdbxBody _loadXml( + KdbxReadWriteContext ctx, KdbxHeader header, String xmlString) { final gen = _createProtectedSaltGenerator(header); final document = xml.parse(xmlString); for (final el in document - .findAllElements('Value') - .where((el) => el.getAttribute('Protected')?.toLowerCase() == 'true')) { + .findAllElements(KdbxXml.NODE_VALUE) + .where((el) => el.getAttributeBool(KdbxXml.ATTR_PROTECTED))) { final pw = gen.decryptBase64(el.text.trim()); if (pw == null) { continue; @@ -536,9 +561,19 @@ class KdbxFormat { final keePassFile = document.findElements('KeePassFile').single; final meta = keePassFile.findElements('Meta').single; final root = keePassFile.findElements('Root').single; - final rootGroup = KdbxGroup.read(null, root.findElements('Group').single); + + final kdbxMeta = KdbxMeta.read(meta); + if (kdbxMeta.binaries?.isNotEmpty == true) { + ctx.binaries.addAll(kdbxMeta.binaries); + } else if (header.innerHeader.binaries.isNotEmpty) { + ctx.binaries.addAll(header.innerHeader.binaries + .map((e) => KdbxBinary.readBinaryInnerHeader(e))); + } + + final rootGroup = + KdbxGroup.read(ctx, null, root.findElements('Group').single); _logger.fine('got meta: ${meta.toXmlString(pretty: true)}'); - return KdbxBody.read(keePassFile, KdbxMeta.read(meta), rootGroup); + return KdbxBody.read(keePassFile, kdbxMeta, rootGroup); } Uint8List _decryptContent( diff --git a/lib/src/kdbx_group.dart b/lib/src/kdbx_group.dart index 7768e24..5da2bb8 100644 --- a/lib/src/kdbx_group.dart +++ b/lib/src/kdbx_group.dart @@ -19,14 +19,15 @@ class KdbxGroup extends KdbxObject { expanded.set(true); } - KdbxGroup.read(KdbxGroup parent, XmlElement node) : super.read(parent, node) { + KdbxGroup.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node) + : super.read(parent, node) { node .findElements('Group') - .map((el) => KdbxGroup.read(this, el)) + .map((el) => KdbxGroup.read(ctx, this, el)) .forEach(_groups.add); node .findElements('Entry') - .map((el) => KdbxEntry.read(this, el)) + .map((el) => KdbxEntry.read(ctx, this, el)) .forEach(_entries.add); } diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart index 8ba2348..2cb7365 100644 --- a/lib/src/kdbx_header.dart +++ b/lib/src/kdbx_header.dart @@ -90,7 +90,7 @@ class KdbxHeader { @required this.fields, @required this.endPos, Map innerFields, - }) : innerFields = innerFields ?? {}; + }) : innerHeader = InnerHeader(fields: innerFields ?? {}); KdbxHeader.create() : this( @@ -163,7 +163,7 @@ class KdbxHeader { InnerHeaderFields.InnerRandomStreamKey ]; for (final field in requiredFields) { - if (innerFields[field] == null) { + if (innerHeader.fields[field] == null) { throw KdbxCorruptedFileException('Missing inner header $field'); } } @@ -174,7 +174,7 @@ class KdbxHeader { } void _setInnerHeaderField(InnerHeaderFields field, Uint8List bytes) { - innerFields[field] = InnerHeaderField(field, bytes); + innerHeader.fields[field] = InnerHeaderField(field, bytes); } void generateSalts() { @@ -229,12 +229,13 @@ class KdbxHeader { .where((f) => f != InnerHeaderFields.EndOfHeader)) { _writeInnerField(writer, field); } + // TODO write attachments _setInnerHeaderField(InnerHeaderFields.EndOfHeader, Uint8List(0)); _writeInnerField(writer, InnerHeaderFields.EndOfHeader); } void _writeInnerField(WriterHelper writer, InnerHeaderFields field) { - final value = innerFields[field]; + final value = innerHeader.fields[field]; if (value == null) { return; } @@ -333,32 +334,41 @@ class KdbxHeader { readAllFields(reader, versionMajor, HeaderFields.values, (HeaderFields field, value) => HeaderField(field, value)); - static Map readInnerHeaderFields( + static InnerHeader readInnerHeaderFields( ReaderHelper reader, int versionMajor) => - readAllFields(reader, versionMajor, InnerHeaderFields.values, - (InnerHeaderFields field, value) => InnerHeaderField(field, value)); + InnerHeader.fromFields( + readField( + reader, + versionMajor, + InnerHeaderFields.values, + (InnerHeaderFields field, value) => + InnerHeaderField(field, value)).toList(growable: false), + ); static Map readAllFields, TE>( ReaderHelper reader, int versionMajor, List fields, - T createField(TE field, Uint8List bytes)) => + T Function(TE field, Uint8List bytes) createField) => Map.fromEntries( readField(reader, versionMajor, fields, createField) .map((field) => MapEntry(field.field, field))); - static Iterable readField(ReaderHelper reader, int versionMajor, - List fields, T createField(TE field, Uint8List bytes)) sync* { + static Iterable readField( + ReaderHelper reader, + int versionMajor, + List fields, + T Function(TE field, Uint8List bytes) createField) sync* { while (true) { final headerId = reader.readUint8(); final bodySize = versionMajor >= 4 ? reader.readUint32() : reader.readUint16(); -// _logger.fine('Reading header with id $headerId (size: $bodySize)}'); final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null; // _logger.finer( // 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}'); if (headerId > 0) { final field = fields[headerId]; + _logger.finest('Reading header $field ($headerId) (size: $bodySize)}'); yield createField(field, bodyBytes); /* else { if (field == InnerHeaderFields.InnerRandomStreamID) { @@ -368,6 +378,7 @@ class KdbxHeader { } }*/ } else { + _logger.finest('EndOfHeader ${fields[headerId]}'); break; } } @@ -378,7 +389,7 @@ class KdbxHeader { final int versionMinor; final int versionMajor; final Map fields; - final Map innerFields; + final InnerHeader innerHeader; /// end position of the header, if we have been reading from a stream. final int endPos; @@ -400,11 +411,11 @@ class KdbxHeader { .values[ReaderHelper.singleUint32(_innerRandomStreamEncryptionBytes)]; Uint8List get _innerRandomStreamEncryptionBytes => versionMajor >= 4 - ? innerFields[InnerHeaderFields.InnerRandomStreamID].bytes + ? innerHeader.fields[InnerHeaderFields.InnerRandomStreamID].bytes : fields[HeaderFields.InnerRandomStreamID].bytes; Uint8List get protectedStreamKey => versionMajor >= 4 - ? innerFields[InnerHeaderFields.InnerRandomStreamKey].bytes + ? innerHeader.fields[InnerHeaderFields.InnerRandomStreamKey].bytes : fields[HeaderFields.ProtectedStreamKey].bytes; VarDictionary get readKdfParameters => VarDictionary.read( @@ -491,3 +502,30 @@ class HashedBlockReader { } } } + +class InnerHeader { + InnerHeader({ + @required this.fields, + List binaries, + }) : binaries = binaries ?? [], + assert(fields != null); + + factory InnerHeader.fromFields(Iterable fields) { + final fieldMap = Map.fromEntries(fields + .where((f) => f.field != InnerHeaderFields.Binary) + .map((e) => MapEntry(e.field, e))); + final binaries = + fields.where((f) => f.field == InnerHeaderFields.Binary).toList(); + return InnerHeader(fields: fieldMap, binaries: binaries); + } + + final Map fields; + final List binaries; + + void updateFrom(InnerHeader other) { + fields.clear(); + fields.addAll(other.fields); + binaries.clear(); + binaries.addAll(other.binaries); + } +} diff --git a/lib/src/kdbx_meta.dart b/lib/src/kdbx_meta.dart index d51a6ce..08e8db1 100644 --- a/lib/src/kdbx_meta.dart +++ b/lib/src/kdbx_meta.dart @@ -1,5 +1,7 @@ import 'package:kdbx/src/internal/extension_utils.dart'; +import 'package:kdbx/src/kdbx_binary.dart'; import 'package:kdbx/src/kdbx_custom_data.dart'; +import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_xml.dart'; import 'package:meta/meta.dart'; @@ -10,6 +12,7 @@ class KdbxMeta extends KdbxNode { @required String databaseName, String generator, }) : customData = KdbxCustomData.create(), + binaries = [], super.create('Meta') { this.databaseName.set(databaseName); this.generator.set(generator ?? 'kdbx.dart'); @@ -20,10 +23,25 @@ class KdbxMeta extends KdbxNode { .singleElement('CustomData') ?.let((e) => KdbxCustomData.read(e)) ?? KdbxCustomData.create(), + binaries = node.singleElement(KdbxXml.NODE_BINARIES)?.let((el) sync* { + var i = 0; + for (final binaryNode in el.findElements(KdbxXml.NODE_BINARY)) { + final id = int.parse(binaryNode.getAttribute(KdbxXml.ATTR_ID)); + if (id != i) { + throw KdbxCorruptedFileException( + 'Invalid ID for binary. expected $i, but was $id'); + } + i++; + yield KdbxBinary.readBinaryXml(binaryNode, isInline: false); + } + })?.toList(), super.read(node); final KdbxCustomData customData; + /// only used in Kdbx 3 + final List binaries; + StringNode get generator => StringNode(this, 'Generator'); StringNode get databaseName => StringNode(this, 'DatabaseName'); diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index 1c42a15..08f0f85 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -13,11 +13,21 @@ class KdbxXml { static const NODE_KEY = 'Key'; static const NODE_VALUE = 'Value'; static const ATTR_PROTECTED = 'Protected'; + static const ATTR_COMPRESSED = 'Compressed'; static const NODE_HISTORY = 'History'; + static const NODE_BINARIES = 'Binaries'; + static const ATTR_ID = 'ID'; + static const NODE_BINARY = 'Binary'; + static const ATTR_REF = 'Ref'; static const NODE_CUSTOM_DATA_ITEM = 'Item'; } +extension XmlElementKdbx on XmlElement { + bool getAttributeBool(String name) => + getAttribute(name)?.toLowerCase() == 'true'; +} + abstract class KdbxSubNode { KdbxSubNode(this.node, this.name); diff --git a/test/internal/test_utils.dart b/test/internal/test_utils.dart new file mode 100644 index 0000000..873d35b --- /dev/null +++ b/test/internal/test_utils.dart @@ -0,0 +1,58 @@ +//typedef HashStuff = Pointer Function(Pointer str); +import 'dart:ffi'; +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:kdbx/kdbx.dart'; + +typedef Argon2HashNative = Pointer Function( + Pointer key, + IntPtr keyLen, + Pointer salt, + Uint64 saltlen, + Uint32 m_cost, // memory cost + Uint32 t_cost, // time cost (number iterations) + Uint32 parallelism, + IntPtr hashlen, + Uint8 type, + Uint32 version, +); +typedef Argon2Hash = Pointer Function( + Pointer key, + int keyLen, + Pointer salt, + int saltlen, + int m_cost, // memory cost + int t_cost, // time cost (number iterations) + int parallelism, + int hashlen, + int type, + int version, +); + +class Argon2Test extends Argon2Base { + Argon2Test() { + final argon2lib = Platform.isMacOS + ? DynamicLibrary.open('libargon2_ffi.dylib') + : DynamicLibrary.open('./libargon2_ffi.so'); + argon2hash = argon2lib + .lookup>('hp_argon2_hash') + .asFunction(); + } + + @override + Argon2Hash argon2hash; +} + +class TestUtil { + static Future readKdbxFile( + String filePath, { + String password = 'asdf', + }) async { + final kdbxFormat = KdbxFormat(Argon2Test()); + final data = await File(filePath).readAsBytes(); + final file = await kdbxFormat.read( + data, Credentials(ProtectedValue.fromString(password))); + return file; + } +} diff --git a/test/kdbx4_test.dart b/test/kdbx4_test.dart index b9f76ea..6c8cd72 100644 --- a/test/kdbx4_test.dart +++ b/test/kdbx4_test.dart @@ -1,68 +1,17 @@ -import 'dart:ffi'; import 'dart:io'; -import 'package:ffi/ffi.dart'; import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/kdbx_header.dart'; import 'package:logging/logging.dart'; import 'package:logging_appenders/logging_appenders.dart'; import 'package:test/test.dart'; +import 'internal/test_utils.dart'; + final _logger = Logger('kdbx4_test'); // ignore_for_file: non_constant_identifier_names -//typedef HashStuff = Pointer Function(Pointer str); -typedef Argon2HashNative = Pointer Function( - Pointer key, - IntPtr keyLen, - Pointer salt, - Uint64 saltlen, - Uint32 m_cost, // memory cost - Uint32 t_cost, // time cost (number iterations) - Uint32 parallelism, - IntPtr hashlen, - Uint8 type, - Uint32 version, -); -typedef Argon2Hash = Pointer Function( - Pointer key, - int keyLen, - Pointer salt, - int saltlen, - int m_cost, // memory cost - int t_cost, // time cost (number iterations) - int parallelism, - int hashlen, - int type, - int version, -); - -class Argon2Test extends Argon2Base { - Argon2Test() { - final argon2lib = Platform.isMacOS - ? DynamicLibrary.open('libargon2_ffi.dylib') - : DynamicLibrary.open('./libargon2_ffi.so'); - argon2hash = argon2lib - .lookup>('hp_argon2_hash') - .asFunction(); - } - - @override - Argon2Hash argon2hash; -} - -Future _readKdbxFile( - String filePath, { - String password = 'asdf', -}) async { - final kdbxFormat = KdbxFormat(Argon2Test()); - final data = await File(filePath).readAsBytes(); - final file = await kdbxFormat.read( - data, Credentials(ProtectedValue.fromString(password))); - return file; -} - void main() { Logger.root.level = Level.ALL; PrintAppender().attachToLogger(Logger.root); @@ -145,12 +94,12 @@ void main() { }); group('recycle bin test', () { test('empty recycle bin with "zero" uuid', () async { - final file = await _readKdbxFile('test/keepass2test.kdbx'); + final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx'); final recycleBin = file.recycleBin; expect(recycleBin, isNull); }); test('check deleting item', () async { - final file = await _readKdbxFile('test/keepass2test.kdbx'); + final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx'); expect(file.recycleBin, isNull); final entry = file.body.rootGroup.getAllEntries().first; file.deleteEntry(entry); diff --git a/test/kdbx_binaries_test.dart b/test/kdbx_binaries_test.dart new file mode 100644 index 0000000..1a92ee4 --- /dev/null +++ b/test/kdbx_binaries_test.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:kdbx/kdbx.dart'; +import 'package:logging/logging.dart'; +import 'package:logging_appenders/logging_appenders.dart'; +import 'package:test/test.dart'; + +import 'internal/test_utils.dart'; + +void main() { + Logger.root.level = Level.ALL; + PrintAppender().attachToLogger(Logger.root); + + group('kdbx3 attachment', () { + test('read binary', () async { + final file = await TestUtil.readKdbxFile('test/keepass2binaries.kdbx'); + final entry = file.body.rootGroup.entries.first; + final binaries = entry.binaryEntries; + expect(binaries, hasLength(3)); + for (final binary in binaries) { + switch (binary.key.key) { + case 'example1.txt': + expect(utf8.decode(binary.value.value), 'content1 example\n\n'); + break; + case 'example2.txt': + expect(utf8.decode(binary.value.value), 'content2 example\n\n'); + break; + case 'keepasslogo.jpeg': + expect(binary.value.value, hasLength(7092)); + break; + default: + fail('invalid key. ${binary.key}'); + } + } + }); + }); + group('kdbx4 attachment', () { + test('read binary', () async { + final file = + await TestUtil.readKdbxFile('test/keepass2kdbx4binaries.kdbx'); + void expectBinary(KdbxEntry entry, String key, dynamic matcher) { + final binaries = entry.binaryEntries; + expect(binaries, hasLength(1)); + final binary = binaries.first; + expect(binary.key.key, key); + expect(binary.value.value, matcher); + } + + expect(file.body.rootGroup.entries, hasLength(2)); + expectBinary(file.body.rootGroup.entries.first, 'example2.txt', + IsUtf8String('content2 example\n\n')); + expectBinary(file.body.rootGroup.entries.last, 'keepasslogo.jpeg', + hasLength(7092)); + }); + }); +} + +class IsUtf8String extends CustomMatcher { + IsUtf8String(dynamic matcher) : super('is utf8 string', 'utf8', matcher); + + @override + Object featureValueOf(dynamic actual) { + if (actual is Uint8List) { + return utf8.decode(actual); + } + return super.featureValueOf(actual); + } +} diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart index 02ee702..bb2f8d5 100644 --- a/test/kdbx_test.dart +++ b/test/kdbx_test.dart @@ -81,7 +81,7 @@ void main() { group('Integration', () { test('Simple save and load', () async { final credentials = Credentials(ProtectedValue.fromString('FooBar')); - final Uint8List saved = await (() async { + final saved = await (() async { final kdbx = kdbxForamt.create(credentials, 'CreateTest'); final rootGroup = kdbx.body.rootGroup; final entry = KdbxEntry.create(kdbx, rootGroup); diff --git a/test/keepass2binaries.kdbx b/test/keepass2binaries.kdbx new file mode 100644 index 0000000..b317623 Binary files /dev/null and b/test/keepass2binaries.kdbx differ diff --git a/test/keepass2kdbx4binaries.kdbx b/test/keepass2kdbx4binaries.kdbx new file mode 100644 index 0000000..508cb80 Binary files /dev/null and b/test/keepass2kdbx4binaries.kdbx differ