Browse Source

first version of reading binaries.

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
c60602a37c
  1. 3
      bin/kdbx.dart
  2. 42
      lib/src/kdbx_binary.dart
  3. 43
      lib/src/kdbx_entry.dart
  4. 2
      lib/src/kdbx_file.dart
  5. 61
      lib/src/kdbx_format.dart
  6. 7
      lib/src/kdbx_group.dart
  7. 66
      lib/src/kdbx_header.dart
  8. 18
      lib/src/kdbx_meta.dart
  9. 10
      lib/src/kdbx_xml.dart
  10. 58
      test/internal/test_utils.dart
  11. 59
      test/kdbx4_test.dart
  12. 69
      test/kdbx_binaries_test.dart
  13. 2
      test/kdbx_test.dart
  14. BIN
      test/keepass2binaries.kdbx
  15. BIN
      test/keepass2kdbx4binaries.kdbx

3
bin/kdbx.dart

@ -131,6 +131,9 @@ class CatCommand extends KdbxFileCommand {
final value = entry.getString(KdbxKey('Password')); final value = entry.getString(KdbxKey('Password'));
print( print(
'$indent `- ${entry.getString(KdbxKey('Title'))?.getText()}: ${forceDecrypt ? value?.getText() : value?.toString()}'); '$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'));
} }
} }
} }

42
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,
);
}
}

43
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/crypto/protected_value.dart';
import 'package:kdbx/src/kdbx_binary.dart';
import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_consts.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_header.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:logging/logging.dart'; import 'package:logging/logging.dart';
@ -36,7 +42,7 @@ class KdbxEntry extends KdbxObject {
icon.set(KdbxIcon.Key); icon.set(KdbxIcon.Key);
} }
KdbxEntry.read(KdbxGroup parent, XmlElement node, KdbxEntry.read(KdbxReadWriteContext ctx, KdbxGroup parent, XmlElement node,
{this.isHistoryEntry = false}) {this.isHistoryEntry = false})
: super.read(parent, node) { : super.read(parent, node) {
_strings.addEntries(node.findElements(KdbxXml.NODE_STRING).map((el) { _strings.addEntries(node.findElements(KdbxXml.NODE_STRING).map((el) {
@ -49,18 +55,32 @@ class KdbxEntry extends KdbxObject {
return MapEntry(key, PlainValue(valueNode.text)); 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);
} }
final bool isHistoryEntry; return MapEntry(key, KdbxBinary.readBinaryXml(valueNode, isInline: true));
}));
List<KdbxEntry> _history; history = _historyElement
List<KdbxEntry> get history => _history ??= (() {
return _historyElement
.findElements('Entry') .findElements('Entry')
.map((entry) => KdbxEntry.read(parent, entry, isHistoryEntry: true)) .map(
(entry) => KdbxEntry.read(ctx, parent, entry, isHistoryEntry: true))
.toList(); .toList();
})(); }
final bool isHistoryEntry;
List<KdbxEntry> history;
XmlElement get _historyElement => node XmlElement get _historyElement => node
.findElements(KdbxXml.NODE_HISTORY) .findElements(KdbxXml.NODE_HISTORY)
@ -114,6 +134,11 @@ class KdbxEntry extends KdbxObject {
final Map<KdbxKey, StringValue> _strings = {}; final Map<KdbxKey, StringValue> _strings = {};
final Map<KdbxKey, KdbxBinary> _binaries = {};
Iterable<MapEntry<KdbxKey, KdbxBinary>> get binaryEntries =>
_binaries.entries;
// Map<KdbxKey, StringValue> get strings => UnmodifiableMapView(_strings); // Map<KdbxKey, StringValue> get strings => UnmodifiableMapView(_strings);
Iterable<MapEntry<KdbxKey, StringValue>> get stringEntries => Iterable<MapEntry<KdbxKey, StringValue>> get stringEntries =>

2
lib/src/kdbx_file.dart

@ -2,13 +2,13 @@ import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:kdbx/src/crypto/protected_value.dart'; 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_format.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
import 'package:kdbx/src/kdbx_dao.dart';
final _logger = Logger('kdbx_file'); final _logger = Logger('kdbx_file');

61
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/byte_utils.dart';
import 'package:kdbx/src/internal/consts.dart'; import 'package:kdbx/src/internal/consts.dart';
import 'package:kdbx/src/internal/crypto_utils.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_file.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.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<KdbxBinary> binaries;
KdbxBinary binaryById(int id) {
if (id >= binaries.length) {
return null;
}
return binaries[id];
}
}
abstract class CredentialsPart { abstract class CredentialsPart {
Uint8List getBinary(); Uint8List getBinary();
} }
@ -129,8 +145,11 @@ class KdbxBody extends KdbxNode {
rootNode.children.add(rootGroup.node); rootNode.children.add(rootGroup.node);
} }
KdbxBody.read(xml.XmlElement node, this.meta, this.rootGroup) KdbxBody.read(
: super.read(node); xml.XmlElement node,
this.meta,
this.rootGroup,
) : super.read(node);
// final xml.XmlDocument xmlDocument; // final xml.XmlDocument xmlDocument;
final KdbxMeta meta; final KdbxMeta meta;
@ -330,13 +349,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: []);
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(header, string)); return KdbxFile(this, credentials, header, _loadXml(ctx, header, string));
} else { } else {
return KdbxFile( return KdbxFile(this, credentials, header,
this, credentials, header, _loadXml(header, utf8.decode(blocks))); _loadXml(ctx, header, utf8.decode(blocks)));
} }
} }
@ -370,11 +390,15 @@ class KdbxFormat {
if (header.compression == Compression.gzip) { if (header.compression == Compression.gzip) {
final content = GZipCodec().decode(decrypted) as Uint8List; final content = GZipCodec().decode(decrypted) as Uint8List;
final contentReader = ReaderHelper(content); final contentReader = ReaderHelper(content);
final headerFields = KdbxHeader.readInnerHeaderFields(contentReader, 4); final innerHeader = KdbxHeader.readInnerHeaderFields(contentReader, 4);
// _logger.fine('inner header fields: $headerFields'); // _logger.fine('inner header fields: $headerFields');
header.innerFields.addAll(headerFields); // header.innerFields.addAll(headerFields);
header.innerHeader.updateFrom(innerHeader);
final xml = utf8.decode(contentReader.readRemaining()); 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.'); 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 gen = _createProtectedSaltGenerator(header);
final document = xml.parse(xmlString); final document = xml.parse(xmlString);
for (final el in document for (final el in document
.findAllElements('Value') .findAllElements(KdbxXml.NODE_VALUE)
.where((el) => el.getAttribute('Protected')?.toLowerCase() == 'true')) { .where((el) => el.getAttributeBool(KdbxXml.ATTR_PROTECTED))) {
final pw = gen.decryptBase64(el.text.trim()); final pw = gen.decryptBase64(el.text.trim());
if (pw == null) { if (pw == null) {
continue; continue;
@ -536,9 +561,19 @@ class KdbxFormat {
final keePassFile = document.findElements('KeePassFile').single; final keePassFile = document.findElements('KeePassFile').single;
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 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)}'); _logger.fine('got meta: ${meta.toXmlString(pretty: true)}');
return KdbxBody.read(keePassFile, KdbxMeta.read(meta), rootGroup); return KdbxBody.read(keePassFile, kdbxMeta, rootGroup);
} }
Uint8List _decryptContent( Uint8List _decryptContent(

7
lib/src/kdbx_group.dart

@ -19,14 +19,15 @@ class KdbxGroup extends KdbxObject {
expanded.set(true); 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 node
.findElements('Group') .findElements('Group')
.map((el) => KdbxGroup.read(this, el)) .map((el) => KdbxGroup.read(ctx, this, el))
.forEach(_groups.add); .forEach(_groups.add);
node node
.findElements('Entry') .findElements('Entry')
.map((el) => KdbxEntry.read(this, el)) .map((el) => KdbxEntry.read(ctx, this, el))
.forEach(_entries.add); .forEach(_entries.add);
} }

66
lib/src/kdbx_header.dart

@ -90,7 +90,7 @@ class KdbxHeader {
@required this.fields, @required this.fields,
@required this.endPos, @required this.endPos,
Map<InnerHeaderFields, InnerHeaderField> innerFields, Map<InnerHeaderFields, InnerHeaderField> innerFields,
}) : innerFields = innerFields ?? {}; }) : innerHeader = InnerHeader(fields: innerFields ?? {});
KdbxHeader.create() KdbxHeader.create()
: this( : this(
@ -163,7 +163,7 @@ class KdbxHeader {
InnerHeaderFields.InnerRandomStreamKey InnerHeaderFields.InnerRandomStreamKey
]; ];
for (final field in requiredFields) { for (final field in requiredFields) {
if (innerFields[field] == null) { if (innerHeader.fields[field] == null) {
throw KdbxCorruptedFileException('Missing inner header $field'); throw KdbxCorruptedFileException('Missing inner header $field');
} }
} }
@ -174,7 +174,7 @@ class KdbxHeader {
} }
void _setInnerHeaderField(InnerHeaderFields field, Uint8List bytes) { void _setInnerHeaderField(InnerHeaderFields field, Uint8List bytes) {
innerFields[field] = InnerHeaderField(field, bytes); innerHeader.fields[field] = InnerHeaderField(field, bytes);
} }
void generateSalts() { void generateSalts() {
@ -229,12 +229,13 @@ class KdbxHeader {
.where((f) => f != InnerHeaderFields.EndOfHeader)) { .where((f) => f != InnerHeaderFields.EndOfHeader)) {
_writeInnerField(writer, field); _writeInnerField(writer, field);
} }
// TODO write attachments
_setInnerHeaderField(InnerHeaderFields.EndOfHeader, Uint8List(0)); _setInnerHeaderField(InnerHeaderFields.EndOfHeader, Uint8List(0));
_writeInnerField(writer, InnerHeaderFields.EndOfHeader); _writeInnerField(writer, InnerHeaderFields.EndOfHeader);
} }
void _writeInnerField(WriterHelper writer, InnerHeaderFields field) { void _writeInnerField(WriterHelper writer, InnerHeaderFields field) {
final value = innerFields[field]; final value = innerHeader.fields[field];
if (value == null) { if (value == null) {
return; return;
} }
@ -333,32 +334,41 @@ class KdbxHeader {
readAllFields(reader, versionMajor, HeaderFields.values, readAllFields(reader, versionMajor, HeaderFields.values,
(HeaderFields field, value) => HeaderField(field, value)); (HeaderFields field, value) => HeaderField(field, value));
static Map<InnerHeaderFields, InnerHeaderField> readInnerHeaderFields( static InnerHeader readInnerHeaderFields(
ReaderHelper reader, int versionMajor) => ReaderHelper reader, int versionMajor) =>
readAllFields(reader, versionMajor, InnerHeaderFields.values, InnerHeader.fromFields(
(InnerHeaderFields field, value) => InnerHeaderField(field, value)); readField(
reader,
versionMajor,
InnerHeaderFields.values,
(InnerHeaderFields field, value) =>
InnerHeaderField(field, value)).toList(growable: false),
);
static Map<TE, T> readAllFields<T extends HeaderFieldBase<TE>, TE>( static Map<TE, T> readAllFields<T extends HeaderFieldBase<TE>, TE>(
ReaderHelper reader, ReaderHelper reader,
int versionMajor, int versionMajor,
List<TE> fields, List<TE> fields,
T createField(TE field, Uint8List bytes)) => T Function(TE field, Uint8List bytes) createField) =>
Map<TE, T>.fromEntries( Map<TE, T>.fromEntries(
readField(reader, versionMajor, fields, createField) readField(reader, versionMajor, fields, createField)
.map((field) => MapEntry(field.field, field))); .map((field) => MapEntry(field.field, field)));
static Iterable<T> readField<T, TE>(ReaderHelper reader, int versionMajor, static Iterable<T> readField<T, TE>(
List<TE> fields, T createField(TE field, Uint8List bytes)) sync* { ReaderHelper reader,
int versionMajor,
List<TE> fields,
T Function(TE field, Uint8List bytes) createField) sync* {
while (true) { while (true) {
final headerId = reader.readUint8(); final headerId = reader.readUint8();
final bodySize = final bodySize =
versionMajor >= 4 ? reader.readUint32() : reader.readUint16(); versionMajor >= 4 ? reader.readUint32() : reader.readUint16();
// _logger.fine('Reading header with id $headerId (size: $bodySize)}');
final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null; final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null;
// _logger.finer( // _logger.finer(
// 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}'); // 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}');
if (headerId > 0) { if (headerId > 0) {
final field = fields[headerId]; final field = fields[headerId];
_logger.finest('Reading header $field ($headerId) (size: $bodySize)}');
yield createField(field, bodyBytes); yield createField(field, bodyBytes);
/* else { /* else {
if (field == InnerHeaderFields.InnerRandomStreamID) { if (field == InnerHeaderFields.InnerRandomStreamID) {
@ -368,6 +378,7 @@ class KdbxHeader {
} }
}*/ }*/
} else { } else {
_logger.finest('EndOfHeader ${fields[headerId]}');
break; break;
} }
} }
@ -378,7 +389,7 @@ class KdbxHeader {
final int versionMinor; final int versionMinor;
final int versionMajor; final int versionMajor;
final Map<HeaderFields, HeaderField> fields; final Map<HeaderFields, HeaderField> fields;
final Map<InnerHeaderFields, InnerHeaderField> innerFields; final InnerHeader innerHeader;
/// end position of the header, if we have been reading from a stream. /// end position of the header, if we have been reading from a stream.
final int endPos; final int endPos;
@ -400,11 +411,11 @@ class KdbxHeader {
.values[ReaderHelper.singleUint32(_innerRandomStreamEncryptionBytes)]; .values[ReaderHelper.singleUint32(_innerRandomStreamEncryptionBytes)];
Uint8List get _innerRandomStreamEncryptionBytes => versionMajor >= 4 Uint8List get _innerRandomStreamEncryptionBytes => versionMajor >= 4
? innerFields[InnerHeaderFields.InnerRandomStreamID].bytes ? innerHeader.fields[InnerHeaderFields.InnerRandomStreamID].bytes
: fields[HeaderFields.InnerRandomStreamID].bytes; : fields[HeaderFields.InnerRandomStreamID].bytes;
Uint8List get protectedStreamKey => versionMajor >= 4 Uint8List get protectedStreamKey => versionMajor >= 4
? innerFields[InnerHeaderFields.InnerRandomStreamKey].bytes ? innerHeader.fields[InnerHeaderFields.InnerRandomStreamKey].bytes
: fields[HeaderFields.ProtectedStreamKey].bytes; : fields[HeaderFields.ProtectedStreamKey].bytes;
VarDictionary get readKdfParameters => VarDictionary.read( VarDictionary get readKdfParameters => VarDictionary.read(
@ -491,3 +502,30 @@ class HashedBlockReader {
} }
} }
} }
class InnerHeader {
InnerHeader({
@required this.fields,
List<InnerHeaderField> binaries,
}) : binaries = binaries ?? [],
assert(fields != null);
factory InnerHeader.fromFields(Iterable<InnerHeaderField> 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<InnerHeaderFields, InnerHeaderField> fields;
final List<InnerHeaderField> binaries;
void updateFrom(InnerHeader other) {
fields.clear();
fields.addAll(other.fields);
binaries.clear();
binaries.addAll(other.binaries);
}
}

18
lib/src/kdbx_meta.dart

@ -1,5 +1,7 @@
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_custom_data.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_object.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@ -10,6 +12,7 @@ class KdbxMeta extends KdbxNode {
@required String databaseName, @required String databaseName,
String generator, String generator,
}) : customData = KdbxCustomData.create(), }) : customData = KdbxCustomData.create(),
binaries = [],
super.create('Meta') { super.create('Meta') {
this.databaseName.set(databaseName); this.databaseName.set(databaseName);
this.generator.set(generator ?? 'kdbx.dart'); this.generator.set(generator ?? 'kdbx.dart');
@ -20,10 +23,25 @@ class KdbxMeta extends KdbxNode {
.singleElement('CustomData') .singleElement('CustomData')
?.let((e) => KdbxCustomData.read(e)) ?? ?.let((e) => KdbxCustomData.read(e)) ??
KdbxCustomData.create(), 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); super.read(node);
final KdbxCustomData customData; final KdbxCustomData customData;
/// only used in Kdbx 3
final List<KdbxBinary> binaries;
StringNode get generator => StringNode(this, 'Generator'); StringNode get generator => StringNode(this, 'Generator');
StringNode get databaseName => StringNode(this, 'DatabaseName'); StringNode get databaseName => StringNode(this, 'DatabaseName');

10
lib/src/kdbx_xml.dart

@ -13,11 +13,21 @@ class KdbxXml {
static const NODE_KEY = 'Key'; static const NODE_KEY = 'Key';
static const NODE_VALUE = 'Value'; static const NODE_VALUE = 'Value';
static const ATTR_PROTECTED = 'Protected'; static const ATTR_PROTECTED = 'Protected';
static const ATTR_COMPRESSED = 'Compressed';
static const NODE_HISTORY = 'History'; 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'; static const NODE_CUSTOM_DATA_ITEM = 'Item';
} }
extension XmlElementKdbx on XmlElement {
bool getAttributeBool(String name) =>
getAttribute(name)?.toLowerCase() == 'true';
}
abstract class KdbxSubNode<T> { abstract class KdbxSubNode<T> {
KdbxSubNode(this.node, this.name); KdbxSubNode(this.node, this.name);

58
test/internal/test_utils.dart

@ -0,0 +1,58 @@
//typedef HashStuff = Pointer<Utf8> Function(Pointer<Utf8> str);
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:kdbx/kdbx.dart';
typedef Argon2HashNative = Pointer<Utf8> Function(
Pointer<Uint8> key,
IntPtr keyLen,
Pointer<Uint8> 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<Utf8> Function(
Pointer<Uint8> key,
int keyLen,
Pointer<Uint8> 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<NativeFunction<Argon2HashNative>>('hp_argon2_hash')
.asFunction();
}
@override
Argon2Hash argon2hash;
}
class TestUtil {
static Future<KdbxFile> 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;
}
}

59
test/kdbx4_test.dart

@ -1,68 +1,17 @@
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:logging/logging.dart'; 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';
final _logger = Logger('kdbx4_test'); final _logger = Logger('kdbx4_test');
// ignore_for_file: non_constant_identifier_names // ignore_for_file: non_constant_identifier_names
//typedef HashStuff = Pointer<Utf8> Function(Pointer<Utf8> str);
typedef Argon2HashNative = Pointer<Utf8> Function(
Pointer<Uint8> key,
IntPtr keyLen,
Pointer<Uint8> 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<Utf8> Function(
Pointer<Uint8> key,
int keyLen,
Pointer<Uint8> 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<NativeFunction<Argon2HashNative>>('hp_argon2_hash')
.asFunction();
}
@override
Argon2Hash argon2hash;
}
Future<KdbxFile> _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() { void main() {
Logger.root.level = Level.ALL; Logger.root.level = Level.ALL;
PrintAppender().attachToLogger(Logger.root); PrintAppender().attachToLogger(Logger.root);
@ -145,12 +94,12 @@ void main() {
}); });
group('recycle bin test', () { group('recycle bin test', () {
test('empty recycle bin with "zero" uuid', () async { 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; final recycleBin = file.recycleBin;
expect(recycleBin, isNull); expect(recycleBin, isNull);
}); });
test('check deleting item', () async { test('check deleting item', () async {
final file = await _readKdbxFile('test/keepass2test.kdbx'); final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx');
expect(file.recycleBin, isNull); expect(file.recycleBin, isNull);
final entry = file.body.rootGroup.getAllEntries().first; final entry = file.body.rootGroup.getAllEntries().first;
file.deleteEntry(entry); file.deleteEntry(entry);

69
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);
}
}

2
test/kdbx_test.dart

@ -81,7 +81,7 @@ void main() {
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'));
final Uint8List saved = await (() async { final saved = await (() async {
final kdbx = kdbxForamt.create(credentials, 'CreateTest'); final kdbx = kdbxForamt.create(credentials, 'CreateTest');
final rootGroup = kdbx.body.rootGroup; final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(kdbx, rootGroup); final entry = KdbxEntry.create(kdbx, rootGroup);

BIN
test/keepass2binaries.kdbx

Binary file not shown.

BIN
test/keepass2kdbx4binaries.kdbx

Binary file not shown.
Loading…
Cancel
Save