Browse Source

add support for adding binaries, and correctly serialize them for kdbx 3 and 4.

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
75aa334737
  1. 21
      example/pubspec.lock
  2. 17
      lib/src/kdbx_binary.dart
  3. 64
      lib/src/kdbx_entry.dart
  4. 40
      lib/src/kdbx_format.dart
  5. 9
      lib/src/kdbx_header.dart
  6. 22
      lib/src/kdbx_meta.dart
  7. 9
      lib/src/kdbx_xml.dart
  8. 2
      pubspec.yaml
  9. 59
      test/kdbx_binaries_test.dart

21
example/pubspec.lock

@ -132,6 +132,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.2" version: "0.2.2"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.6"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -167,6 +174,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@ -186,6 +200,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.5" version: "1.5.5"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:

17
lib/src/kdbx_binary.dart

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.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';
@ -24,6 +25,15 @@ class KdbxBinary {
); );
} }
InnerHeaderField writeToInnerHeader() {
final writer = WriterHelper();
final flags = isProtected ? 0x01 : 0x00;
writer.writeUint8(flags);
writer.writeBytes(value);
return InnerHeaderField(
InnerHeaderFields.Binary, writer.output.takeBytes());
}
static KdbxBinary readBinaryXml(XmlElement valueNode, static KdbxBinary readBinaryXml(XmlElement valueNode,
{@required bool isInline}) { {@required bool isInline}) {
assert(isInline != null); assert(isInline != null);
@ -39,4 +49,11 @@ class KdbxBinary {
value: value, value: value,
); );
} }
void saveToXml(XmlElement valueNode) {
final content = base64.encode(gzip.encode(value));
valueNode.addAttributeBool(KdbxXml.ATTR_PROTECTED, isProtected);
valueNode.addAttributeBool(KdbxXml.ATTR_COMPRESSED, true);
valueNode.children.add(XmlText(content));
}
} }

64
lib/src/kdbx_entry.dart

@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:kdbx/kdbx.dart'; 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_binary.dart';
@ -8,6 +10,8 @@ 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';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
final _logger = Logger('kdbx.kdbx_entry'); final _logger = Logger('kdbx.kdbx_entry');
@ -103,13 +107,12 @@ class KdbxEntry extends KdbxObject {
final el = super.toXml(); final el = super.toXml();
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_STRING); XmlUtils.removeChildrenByName(el, KdbxXml.NODE_STRING);
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_HISTORY); XmlUtils.removeChildrenByName(el, KdbxXml.NODE_HISTORY);
el.children.removeWhere( XmlUtils.removeChildrenByName(el, KdbxXml.NODE_BINARY);
(e) => e is XmlElement && e.name.local == KdbxXml.NODE_STRING);
el.children.addAll(stringEntries.map((stringEntry) { el.children.addAll(stringEntries.map((stringEntry) {
final value = XmlElement(XmlName(KdbxXml.NODE_VALUE)); final value = XmlElement(XmlName(KdbxXml.NODE_VALUE));
if (stringEntry.value is ProtectedValue) { if (stringEntry.value is ProtectedValue) {
value.attributes value.attributes.add(
.add(XmlAttribute(XmlName(KdbxXml.ATTR_PROTECTED), 'True')); XmlAttribute(XmlName(KdbxXml.ATTR_PROTECTED), KdbxXml.VALUE_TRUE));
KdbxFile.setProtectedValueForNode( KdbxFile.setProtectedValueForNode(
value, stringEntry.value as ProtectedValue); value, stringEntry.value as ProtectedValue);
} else if (stringEntry.value is StringValue) { } else if (stringEntry.value is StringValue) {
@ -122,6 +125,22 @@ class KdbxEntry extends KdbxObject {
value, value,
]); ]);
})); }));
el.children.addAll(binaryEntries.map((binaryEntry) {
final key = binaryEntry.key;
final binary = binaryEntry.value;
final value = XmlElement(XmlName(KdbxXml.NODE_VALUE));
if (binary.isInline) {
binary.saveToXml(value);
} else {
final binaryIndex = file.ctx.findBinaryId(binary);
value.addAttribute(KdbxXml.ATTR_REF, binaryIndex.toString());
}
return XmlElement(XmlName(KdbxXml.NODE_BINARY))
..children.addAll([
XmlElement(XmlName(KdbxXml.NODE_KEY))..children.add(XmlText(key.key)),
value,
]);
}));
if (!isHistoryEntry) { if (!isHistoryEntry) {
el.children.add( el.children.add(
XmlElement(XmlName(KdbxXml.NODE_HISTORY)) XmlElement(XmlName(KdbxXml.NODE_HISTORY))
@ -177,6 +196,43 @@ class KdbxEntry extends KdbxObject {
String get label => _plainValue(KdbxKey('Title')); String get label => _plainValue(KdbxKey('Title'));
set label(String label) => setString(KdbxKey('Title'), PlainValue(label));
KdbxBinary createBinary({
@required bool isProtected,
@required String name,
@required Uint8List bytes,
}) {
assert(isProtected != null);
assert(bytes != null);
assert(name != null);
// make sure we don't have a path, just the file name.
final key = _uniqueBinaryName(path.basename(name));
final binary = KdbxBinary(
isInline: false,
isProtected: isProtected,
value: bytes,
);
file.ctx.addBinary(binary);
_binaries[key] = binary;
isDirty = true;
return binary;
}
KdbxKey _uniqueBinaryName(String fileName) {
final lastIndex = fileName.lastIndexOf('.');
final baseName =
lastIndex > -1 ? fileName.substring(0, lastIndex) : fileName;
final ext = lastIndex > -1 ? fileName.substring(lastIndex + 1) : 'ext';
for (var i = 0; i < 1000; i++) {
final k = i == 0 ? KdbxKey(fileName) : KdbxKey('$baseName$i.$ext');
if (!_binaries.containsKey(k)) {
return k;
}
}
throw StateError('Unable to find unique name for $fileName');
}
@override @override
String toString() { String toString() {
return 'KdbxGroup{uuid=$uuid,name=$label}'; return 'KdbxGroup{uuid=$uuid,name=$label}';

40
lib/src/kdbx_format.dart

@ -64,11 +64,15 @@ class KeyFileComposite implements Credentials {
/// Context used during reading and writing. /// Context used during reading and writing.
class KdbxReadWriteContext { class KdbxReadWriteContext {
KdbxReadWriteContext({@required this.binaries, @required this.header}) KdbxReadWriteContext({
: assert(binaries != null), @required List<KdbxBinary> binaries,
assert(header != null); @required this.header,
}) : assert(binaries != null),
assert(header != null),
_binaries = binaries;
static final kdbxContext = Expando<KdbxReadWriteContext>(); static final kdbxContext = Expando<KdbxReadWriteContext>();
static KdbxReadWriteContext kdbxContextForNode(xml.XmlParent node) { static KdbxReadWriteContext kdbxContextForNode(xml.XmlParent node) {
final ret = kdbxContext[node.document]; final ret = kdbxContext[node.document];
if (ret == null) { if (ret == null) {
@ -83,17 +87,36 @@ class KdbxReadWriteContext {
} }
@protected @protected
final List<KdbxBinary> binaries; final List<KdbxBinary> _binaries;
Iterable<KdbxBinary> get binariesIterable => _binaries;
final KdbxHeader header; final KdbxHeader header;
int get versionMajor => header.versionMajor; int get versionMajor => header.versionMajor;
KdbxBinary binaryById(int id) { KdbxBinary binaryById(int id) {
if (id >= binaries.length) { if (id >= _binaries.length) {
return null; return null;
} }
return binaries[id]; return _binaries[id];
}
void addBinary(KdbxBinary binary) {
_binaries.add(binary);
}
/// finds the ID of the given binary.
/// if it can't be found, [KdbxCorruptedFileException] is thrown.
int findBinaryId(KdbxBinary binary) {
assert(binary != null);
assert(!binary.isInline);
final id = _binaries.indexOf(binary);
if (id < 0) {
throw KdbxCorruptedFileException('Unable to find binary.'
' (${binary.value.length},${binary.isInline})');
}
return id;
} }
} }
@ -191,6 +214,7 @@ class KdbxBody extends KdbxNode {
ProtectedSaltGenerator saltGenerator, _KeysV4 keys) { ProtectedSaltGenerator saltGenerator, _KeysV4 keys) {
final bodyWriter = WriterHelper(); final bodyWriter = WriterHelper();
final xml = generateXml(saltGenerator); final xml = generateXml(saltGenerator);
kdbxFile.header.innerHeader.updateBinaries(kdbxFile.ctx.binariesIterable);
kdbxFile.header.writeInnerHeader(bodyWriter); kdbxFile.header.writeInnerHeader(bodyWriter);
bodyWriter.writeBytes(utf8.encode(xml.toXmlString()) as Uint8List); bodyWriter.writeBytes(utf8.encode(xml.toXmlString()) as Uint8List);
final compressedBytes = (kdbxFile.header.compression == Compression.gzip final compressedBytes = (kdbxFile.header.compression == Compression.gzip
@ -590,9 +614,9 @@ class KdbxFormat {
final kdbxMeta = KdbxMeta.read(meta, ctx); final kdbxMeta = KdbxMeta.read(meta, ctx);
if (kdbxMeta.binaries?.isNotEmpty == true) { if (kdbxMeta.binaries?.isNotEmpty == true) {
ctx.binaries.addAll(kdbxMeta.binaries); ctx._binaries.addAll(kdbxMeta.binaries);
} else if (header.innerHeader.binaries.isNotEmpty) { } else if (header.innerHeader.binaries.isNotEmpty) {
ctx.binaries.addAll(header.innerHeader.binaries ctx._binaries.addAll(header.innerHeader.binaries
.map((e) => KdbxBinary.readBinaryInnerHeader(e))); .map((e) => KdbxBinary.readBinaryInnerHeader(e)));
} }

9
lib/src/kdbx_header.dart

@ -5,10 +5,11 @@ import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/crypto/key_encrypter_kdf.dart'; import 'package:kdbx/src/crypto/key_encrypter_kdf.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/kdbx_binary.dart';
import 'package:kdbx/src/kdbx_var_dictionary.dart'; import 'package:kdbx/src/kdbx_var_dictionary.dart';
import 'package:kdbx/src/utils/scope_functions.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:kdbx/src/utils/scope_functions.dart';
final _logger = Logger('kdbx.header'); final _logger = Logger('kdbx.header');
@ -224,6 +225,7 @@ class KdbxHeader {
} }
void writeInnerHeader(WriterHelper writer) { void writeInnerHeader(WriterHelper writer) {
assert(versionMajor >= 4);
_validateInner(); _validateInner();
for (final field in InnerHeaderFields.values for (final field in InnerHeaderFields.values
.where((f) => f != InnerHeaderFields.EndOfHeader)) { .where((f) => f != InnerHeaderFields.EndOfHeader)) {
@ -541,4 +543,9 @@ class InnerHeader {
binaries.clear(); binaries.clear();
binaries.addAll(other.binaries); binaries.addAll(other.binaries);
} }
void updateBinaries(Iterable<KdbxBinary> newBinaries) {
binaries.clear();
binaries.addAll(newBinaries.map((binary) => binary.writeToInnerHeader()));
}
} }

22
lib/src/kdbx_meta.dart

@ -6,7 +6,9 @@ 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';
import 'package:quiver/iterables.dart';
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
import 'package:xml/xml.dart';
class KdbxMeta extends KdbxNode implements KdbxNodeContext { class KdbxMeta extends KdbxNode implements KdbxNodeContext {
KdbxMeta.create({ KdbxMeta.create({
@ -61,5 +63,23 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext {
DateTimeUtcNode(this, 'RecycleBinChanged'); DateTimeUtcNode(this, 'RecycleBinChanged');
@override @override
xml.XmlElement toXml() => super.toXml()..replaceSingle(customData.toXml()); xml.XmlElement toXml() {
final ret = super.toXml()..replaceSingle(customData.toXml());
XmlUtils.removeChildrenByName(ret, KdbxXml.NODE_BINARIES);
// with kdbx >= 4 we assume the binaries were already written in the header.
if (ctx.versionMajor < 4) {
ret.children.add(
XmlElement(XmlName(KdbxXml.NODE_BINARIES))
..children.addAll(
enumerate(ctx.binariesIterable).map((indexed) {
final xmlBinary = XmlUtils.createNode(KdbxXml.NODE_BINARY)
..addAttribute(KdbxXml.ATTR_ID, indexed.index.toString());
indexed.value.saveToXml(xmlBinary);
return xmlBinary;
}),
),
);
}
return ret;
}
} }

9
lib/src/kdbx_xml.dart

@ -21,11 +21,20 @@ class KdbxXml {
static const ATTR_REF = 'Ref'; static const ATTR_REF = 'Ref';
static const NODE_CUSTOM_DATA_ITEM = 'Item'; static const NODE_CUSTOM_DATA_ITEM = 'Item';
static const String VALUE_TRUE = 'True';
static const String VALUE_FALSE = 'False';
} }
extension XmlElementKdbx on XmlElement { extension XmlElementKdbx on XmlElement {
bool getAttributeBool(String name) => bool getAttributeBool(String name) =>
getAttribute(name)?.toLowerCase() == 'true'; getAttribute(name)?.toLowerCase() == 'true';
void addAttribute(String name, String value) =>
attributes.add(XmlAttribute(XmlName(name), value));
void addAttributeBool(String name, bool value) =>
addAttribute(name, value ? KdbxXml.VALUE_TRUE : KdbxXml.VALUE_FALSE);
} }
abstract class KdbxSubNode<T> { abstract class KdbxSubNode<T> {

2
pubspec.yaml

@ -20,6 +20,8 @@ dependencies:
clock: '>=1.0.0 <2.0.0' clock: '>=1.0.0 <2.0.0'
convert: '>=2.0.0 <3.0.0' convert: '>=2.0.0 <3.0.0'
isolate: '>=2.0.3 <3.0.0' isolate: '>=2.0.3 <3.0.0'
path: '>=1.6.0 <2.0.0'
quiver: '>=2.1.0 <3.0.0'
collection: '>=1.14.0 <2.0.0' collection: '>=1.14.0 <2.0.0'

59
test/kdbx_binaries_test.dart

@ -16,6 +16,37 @@ void expectBinary(KdbxEntry entry, String key, dynamic matcher) {
expect(binary.value.value, matcher); expect(binary.value.value, matcher);
} }
Future<void> _testAddNewAttachment(String filePath) async {
final saved = await (() async {
final f = await TestUtil.readKdbxFile(filePath);
final entry = KdbxEntry.create(f, f.body.rootGroup);
entry.label = 'addattachment';
f.body.rootGroup.addEntry(entry);
expect(entry.binaryEntries, hasLength(0));
entry.createBinary(
isProtected: false,
name: 'test.txt',
bytes: utf8.encode('Content1') as Uint8List);
entry.createBinary(
isProtected: false,
name: 'test.txt',
bytes: utf8.encode('Content2') as Uint8List);
return await f.save();
})();
{
final file = await TestUtil.readKdbxFileBytes(saved);
final entry = file.body.rootGroup.entries
.firstWhere((e) => e.label == 'addattachment');
final binaries = entry.binaryEntries.toList();
expect(entry.binaryEntries, hasLength(2));
expect(binaries[0].key.key, 'test.txt');
expect(binaries[0].value.value, IsUtf8String('Content1'));
// must have been renamed.
expect(binaries[1].key.key, 'test1.txt');
expect(binaries[1].value.value, IsUtf8String('Content2'));
}
}
void main() { void main() {
Logger.root.level = Level.ALL; Logger.root.level = Level.ALL;
PrintAppender().attachToLogger(Logger.root); PrintAppender().attachToLogger(Logger.root);
@ -54,6 +85,9 @@ void main() {
final entry = file.body.rootGroup.entries.first; final entry = file.body.rootGroup.entries.first;
expectKeepass2binariesContents(entry); expectKeepass2binariesContents(entry);
}); });
test('Add new attachment', () async {
await _testAddNewAttachment('test/keepass2binaries.kdbx');
});
}); });
group('kdbx4 attachment', () { group('kdbx4 attachment', () {
test('read binary', () async { test('read binary', () async {
@ -66,17 +100,20 @@ void main() {
expectBinary(file.body.rootGroup.entries.last, 'keepasslogo.jpeg', expectBinary(file.body.rootGroup.entries.last, 'keepasslogo.jpeg',
hasLength(7092)); hasLength(7092));
}); });
}); test('read, write, read kdbx4', () async {
test('read, write, read', () async { final fileRead =
final fileRead = await TestUtil.readKdbxFile('test/keepass2kdbx4binaries.kdbx');
await TestUtil.readKdbxFile('test/keepass2kdbx4binaries.kdbx'); final saved = await fileRead.save();
final saved = await fileRead.save(); final file = await TestUtil.readKdbxFileBytes(saved);
final file = await TestUtil.readKdbxFileBytes(saved); expect(file.body.rootGroup.entries, hasLength(2));
expect(file.body.rootGroup.entries, hasLength(2)); expectBinary(file.body.rootGroup.entries.first, 'example2.txt',
expectBinary(file.body.rootGroup.entries.first, 'example2.txt', IsUtf8String('content2 example\n\n'));
IsUtf8String('content2 example\n\n')); expectBinary(file.body.rootGroup.entries.last, 'keepasslogo.jpeg',
expectBinary( hasLength(7092));
file.body.rootGroup.entries.last, 'keepasslogo.jpeg', hasLength(7092)); });
test('Add new attachment kdbx4', () async {
await _testAddNewAttachment('test/keepass2kdbx4binaries.kdbx');
});
}); });
} }

Loading…
Cancel
Save