Browse Source

creating new kdbx files, started with writing support.

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
15af0093e9
  1. 2
      bin/kdbx.dart
  2. 48
      lib/src/internal/byte_utils.dart
  3. 13
      lib/src/internal/consts.dart
  4. 24
      lib/src/kdbx_entry.dart
  5. 60
      lib/src/kdbx_format.dart
  6. 9
      lib/src/kdbx_group.dart
  7. 160
      lib/src/kdbx_header.dart
  8. 39
      lib/src/kdbx_object.dart
  9. 78
      lib/src/kdbx_xml.dart
  10. 17
      test/internal/byte_utils_test.dart
  11. 15
      test/kdbx_test.dart

2
bin/kdbx.dart

@ -127,6 +127,6 @@ class DumpXmlCommand extends KdbxFileCommand {
@override @override
Future<void> runWithFile(KdbxFile file) async { Future<void> runWithFile(KdbxFile file) async {
print(file.body.xmlDocument.toXmlString(pretty: true)); print(file.body.node.toXmlString(pretty: true));
} }
} }

48
lib/src/internal/byte_utils.dart

@ -1,3 +1,7 @@
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
class ByteUtils { class ByteUtils {
static bool eq(List<int> a, List<int> b) { static bool eq(List<int> a, List<int> b) {
if (a.length != b.length) { if (a.length != b.length) {
@ -15,3 +19,47 @@ class ByteUtils {
static String toHexList(List<int> list) => list.map((val) => toHex(val)).join(' '); static String toHexList(List<int> list) => list.map((val) => toHex(val)).join(' ');
} }
class ReaderHelper {
ReaderHelper(this.data);
final Uint8List data;
int pos = 0;
ByteBuffer _nextByteBuffer(int byteCount) =>
(data.sublist(pos, pos += byteCount) as Uint8List).buffer;
int readUint32() => _nextByteBuffer(4).asUint32List().first;
int readUint16() => _nextByteBuffer(2).asUint16List().first;
int readUint8() => data[pos++];
ByteBuffer readBytes(int size) => _nextByteBuffer(size);
Uint8List readRemaining() => data.sublist(pos) as Uint8List;
}
class WriterHelper {
WriterHelper(this.output);
final BytesBuilder output;
void writeBytes(Uint8List bytes) {
output.add(bytes);
// output.asUint8List().addAll(bytes);
}
void writeUint32(int value) {
output.add(Uint32List.fromList([value]).buffer.asUint8List());
// output.asUint32List().add(value);
}
void writeUint16(int value) {
output.add(Uint32List.fromList([value]).buffer.asUint32List());
}
void writeUint8(int value) {
output.addByte(value);
}
}

13
lib/src/internal/consts.dart

@ -0,0 +1,13 @@
import 'package:kdbx/src/kdbx_object.dart';
enum Cipher {
aes,
chaCha20,
}
class CryptoConsts {
static const CIPHER_IDS = <Cipher, KdbxUuid>{
Cipher.aes: KdbxUuid('McHy5r9xQ1C+WAUhavxa/w=='),
Cipher.chaCha20: KdbxUuid('1gOKK4tvTLWlJDOaMdu1mg=='),
};
}

24
lib/src/kdbx_entry.dart

@ -7,10 +7,25 @@ import 'package:xml/xml.dart';
String _canonicalizeKey(String key) => key?.toLowerCase(); String _canonicalizeKey(String key) => key?.toLowerCase();
/// Represents a case insensitive (but case preserving) key.
class KdbxKey {
KdbxKey(this.key) : _canonicalKey = key.toLowerCase();
final String key;
final String _canonicalKey;
@override
bool operator ==(Object other) =>
other is KdbxKey && _canonicalKey == other._canonicalKey;
@override
int get hashCode => _canonicalKey.hashCode;
}
class KdbxEntry extends KdbxObject { class KdbxEntry extends KdbxObject {
KdbxEntry.read(this.parent, XmlElement node) : super.read(node) { KdbxEntry.read(this.parent, XmlElement node) : super.read(node) {
strings.addEntries(node.findElements('String').map((el) { strings.addEntries(node.findElements('String').map((el) {
final key = el.findElements('Key').single.text; final key = KdbxKey(el.findElements('Key').single.text);
final valueNode = el.findElements('Value').single; final valueNode = el.findElements('Value').single;
if (valueNode.getAttribute('Protected')?.toLowerCase() == 'true') { if (valueNode.getAttribute('Protected')?.toLowerCase() == 'true') {
return MapEntry(key, KdbxFile.protectedValueForNode(valueNode)); return MapEntry(key, KdbxFile.protectedValueForNode(valueNode));
@ -21,10 +36,9 @@ class KdbxEntry extends KdbxObject {
} }
KdbxGroup parent; KdbxGroup parent;
Map<String, StringValue> strings = Map<KdbxKey, StringValue> strings = <KdbxKey, StringValue>{};
CanonicalizedMap<String, String, StringValue>(_canonicalizeKey);
String _plainValue(String key) { String _plainValue(KdbxKey key) {
final value = strings[key]; final value = strings[key];
if (value is PlainValue) { if (value is PlainValue) {
return value.getText(); return value.getText();
@ -32,5 +46,5 @@ class KdbxEntry extends KdbxObject {
return value?.toString(); return value?.toString();
} }
String get label => _plainValue('Title'); String get label => _plainValue(KdbxKey('Title'));
} }

60
lib/src/kdbx_format.dart

@ -10,7 +10,9 @@ import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:kdbx/src/internal/crypto_utils.dart'; import 'package:kdbx/src/internal/crypto_utils.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_xml.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
@ -44,23 +46,69 @@ class KdbxFile {
final Credentials credentials; final Credentials credentials;
final KdbxHeader header; final KdbxHeader header;
final KdbxBody body; final KdbxBody body;
Uint8List save() {
final output = BytesBuilder();
final writer = WriterHelper(output);
header.generateSalts();
header.write(writer);
body.write(writer, this);
return output.toBytes();
}
} }
class KdbxBody { class KdbxBody extends KdbxNode {
KdbxBody(this.xmlDocument, this.meta, this.rootGroup); KdbxBody.create(this.meta, this.rootGroup) : super.create('KeePassFile') {
node.children.add(meta.node);
final rootNode = xml.XmlElement(xml.XmlName('Root'));
node.children.add(rootNode);
rootNode.children.add(rootGroup.node);
}
KdbxBody.read(xml.XmlElement node, this.meta, this.rootGroup) : super.read(node);
final xml.XmlDocument xmlDocument; // final xml.XmlDocument xmlDocument;
final KdbxMeta meta; final KdbxMeta meta;
final KdbxGroup rootGroup; final KdbxGroup rootGroup;
void write(WriterHelper writer, KdbxFile kdbxFile) {
assert(kdbxFile.header.versionMajor == 3);
_writeV3(writer, kdbxFile);
}
void _writeV3(WriterHelper writer, KdbxFile kdbxFile) {
meta.headerHash.set((crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List).buffer);
}
xml.XmlDocument toXml() {
final doc = xml.XmlDocument();
doc.children.add(xml.XmlProcessing('xml', 'version="1.0" encoding="utf-8" standalone="yes"'));
doc.children.add(node.copy());
return doc;
}
} }
class KdbxMeta extends KdbxNode { class KdbxMeta extends KdbxNode {
KdbxMeta.create({@required String databaseName}) : super.create('Meta') {
this.databaseName.set(databaseName);
}
KdbxMeta.read(xml.XmlElement node) : super.read(node); KdbxMeta.read(xml.XmlElement node) : super.read(node);
String get databaseName => text('DatabaseName'); StringNode get databaseName => StringNode(this, 'DatabaseName');
Base64Node get headerHash => Base64Node(this, 'HeaderHash');
} }
class KdbxFormat { class KdbxFormat {
static KdbxFile create(Credentials credentials, String name) {
final header = KdbxHeader.create();
final meta = KdbxMeta.create(databaseName: name);
final rootGroup = KdbxGroup.create(parent: null, name: name);
final body = KdbxBody.create(meta, rootGroup);
return KdbxFile(credentials, header, body);
}
static KdbxFile read(Uint8List input, Credentials credentials) { static KdbxFile read(Uint8List input, Credentials credentials) {
final reader = ReaderHelper(input); final reader = ReaderHelper(input);
final header = KdbxHeader.read(reader); final header = KdbxHeader.read(reader);
@ -88,7 +136,7 @@ class KdbxFormat {
static KdbxBody _loadXml(KdbxHeader header, String xmlString) { static KdbxBody _loadXml(KdbxHeader header, String xmlString) {
final protectedValueEncryption = header.innerRandomStreamEncryption; final protectedValueEncryption = header.innerRandomStreamEncryption;
if (protectedValueEncryption != PotectedValueEncryption.salsa20) { if (protectedValueEncryption != ProtectedValueEncryption.salsa20) {
throw KdbxUnsupportedException( throw KdbxUnsupportedException(
'Inner encryption: $protectedValueEncryption'); 'Inner encryption: $protectedValueEncryption');
} }
@ -110,7 +158,7 @@ class KdbxFormat {
final root = keePassFile.findElements('Root').single; final root = keePassFile.findElements('Root').single;
final rootGroup = KdbxGroup.read(null, root.findElements('Group').single); final rootGroup = KdbxGroup.read(null, root.findElements('Group').single);
_logger.fine('got meta: ${meta.toXmlString(pretty: true)}'); _logger.fine('got meta: ${meta.toXmlString(pretty: true)}');
return KdbxBody(document, KdbxMeta.read(meta), rootGroup); return KdbxBody.read(keePassFile, KdbxMeta.read(meta), rootGroup);
} }
static Uint8List _decryptContent( static Uint8List _decryptContent(

9
lib/src/kdbx_group.dart

@ -1,10 +1,14 @@
import 'package:kdbx/src/kdbx_entry.dart'; import 'package:kdbx/src/kdbx_entry.dart';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:meta/meta.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
import 'kdbx_object.dart'; import 'kdbx_object.dart';
class KdbxGroup extends KdbxObject { class KdbxGroup extends KdbxObject {
KdbxGroup(this.parent) : super.create('Group'); KdbxGroup.create({@required this.parent, @required String name}) : super.create('Group') {
this.name.set(name);
}
KdbxGroup.read(this.parent, XmlElement node) : super.read(node) { KdbxGroup.read(this.parent, XmlElement node) : super.read(node) {
node node
@ -31,5 +35,6 @@ class KdbxGroup extends KdbxObject {
final List<KdbxGroup> groups = []; final List<KdbxGroup> groups = [];
final List<KdbxEntry> entries = []; final List<KdbxEntry> entries = [];
String get name => text('Name') ?? ''; StringNode get name => StringNode(this, 'Name');
// String get name => text('Name') ?? '';
} }

160
lib/src/kdbx_header.dart

@ -1,8 +1,12 @@
import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
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:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:pointycastle/api.dart';
final _logger = Logger('kdbx.header'); final _logger = Logger('kdbx.header');
@ -21,7 +25,7 @@ enum Compression {
} }
/// how protected values are encrypted in the xml. /// how protected values are encrypted in the xml.
enum PotectedValueEncryption { plainText, arc4variant, salsa20 } enum ProtectedValueEncryption { plainText, arc4variant, salsa20 }
enum HeaderFields { enum HeaderFields {
EndOfHeader, EndOfHeader,
@ -34,7 +38,7 @@ enum HeaderFields {
EncryptionIV, EncryptionIV,
ProtectedStreamKey, ProtectedStreamKey,
StreamStartBytes, StreamStartBytes,
InnerRandomStreamID, InnerRandomStreamID, // crsAlgorithm
KdfParameters, KdfParameters,
PublicCustomData, PublicCustomData,
} }
@ -49,12 +53,122 @@ class HeaderField {
} }
class KdbxHeader { class KdbxHeader {
KdbxHeader( KdbxHeader({
{this.sig1, @required this.sig1,
this.sig2, @required this.sig2,
this.versionMinor, @required this.versionMinor,
this.versionMajor, @required this.versionMajor,
this.fields}); @required this.fields,
});
KdbxHeader.create()
: this(
sig1: Consts.FileMagic,
sig2: Consts.Sig2Kdbx,
versionMinor: 1,
versionMajor: 3,
fields: _defaultFieldValues(),
);
static ByteBuffer _intAsUintBytes(int val) =>
Uint8List.fromList([val]).buffer;
static List<HeaderFields> _requiredFields(int majorVersion) {
if (majorVersion < 3) {
throw KdbxUnsupportedException('Unsupported version: $majorVersion');
}
final baseHeaders = [
HeaderFields.CipherID,
HeaderFields.CompressionFlags,
HeaderFields.MasterSeed,
HeaderFields.EncryptionIV
];
if (majorVersion < 4) {
return baseHeaders +
[
HeaderFields.TransformSeed,
HeaderFields.TransformRounds,
HeaderFields.ProtectedStreamKey,
HeaderFields.StreamStartBytes,
HeaderFields.InnerRandomStreamID
];
} else {
// TODO kdbx 4 support
throw KdbxUnsupportedException('We do not support kdbx 4.x right now');
return baseHeaders + [HeaderFields.KdfParameters]; // ignore: dead_code
}
}
void _validate() {
for (HeaderFields required in _requiredFields(versionMajor)) {
if (fields[required] == null) {
throw KdbxCorruptedFileException('Missing header $required');
}
}
}
void _setHeaderField(HeaderFields field, ByteBuffer bytes) {
fields[field] = HeaderField(field, bytes);
}
void generateSalts() {
// TODO make sure default algorithm is "secure" engouh. Or whether we should
// use [Random.secure]?
final random = SecureRandom();
_setHeaderField(HeaderFields.MasterSeed, random.nextBytes(32).buffer);
if (versionMajor < 4) {
_setHeaderField(HeaderFields.TransformSeed, random.nextBytes(32).buffer);
_setHeaderField(HeaderFields.StreamStartBytes, random.nextBytes(32).buffer);
_setHeaderField(HeaderFields.ProtectedStreamKey, random.nextBytes(32).buffer);
_setHeaderField(HeaderFields.EncryptionIV, random.nextBytes(16).buffer);
} else {
throw KdbxUnsupportedException('We do not support Kdbx 4.x right now. ($versionMajor.$versionMinor)');
}
}
void write(WriterHelper writer) {
_validate();
// write signature
writer.writeUint32(Consts.FileMagic);
writer.writeUint32(Consts.Sig2Kdbx);
// write version
writer.writeUint16(versionMinor);
writer.writeUint16(versionMajor);
for (final field
in HeaderFields.values.where((f) => f != HeaderFields.EndOfHeader)) {
_writeField(writer, field);
}
_writeField(writer, HeaderFields.EndOfHeader);
}
void _writeField(WriterHelper writer, HeaderFields field) {
final value = fields[field];
if (value == null) {
return;
}
_writeFieldSize(writer, value.bytes.lengthInBytes);
writer.writeBytes(value.bytes.asUint8List());
}
void _writeFieldSize(WriterHelper writer, int size) {
if (versionMajor >= 4) {
writer.writeUint32(size);
} else {
writer.writeUint16(size);
}
}
static Map<HeaderFields, HeaderField> _defaultFieldValues() =>
Map.fromEntries([
HeaderField(HeaderFields.CipherID,
CryptoConsts.CIPHER_IDS[Cipher.aes].toBytes()),
HeaderField(HeaderFields.CompressionFlags, _intAsUintBytes(1)),
HeaderField(HeaderFields.TransformRounds, _intAsUintBytes(6000)),
HeaderField(
HeaderFields.InnerRandomStreamID,
_intAsUintBytes(ProtectedValueEncryption.values
.indexOf(ProtectedValueEncryption.salsa20))),
].map((f) => MapEntry(f.field, f)));
static KdbxHeader read(ReaderHelper reader) { static KdbxHeader read(ReaderHelper reader) {
// reading signature // reading signature
@ -115,8 +229,8 @@ class KdbxHeader {
} }
} }
PotectedValueEncryption get innerRandomStreamEncryption => ProtectedValueEncryption get innerRandomStreamEncryption =>
PotectedValueEncryption.values[ ProtectedValueEncryption.values[
fields[HeaderFields.InnerRandomStreamID].bytes.asUint32List().single]; fields[HeaderFields.InnerRandomStreamID].bytes.asUint32List().single];
} }
@ -124,7 +238,11 @@ class KdbxException implements Exception {}
class KdbxInvalidKeyException implements KdbxException {} class KdbxInvalidKeyException implements KdbxException {}
class KdbxCorruptedFileException implements KdbxException {} class KdbxCorruptedFileException implements KdbxException {
KdbxCorruptedFileException([this.message]);
final String message;
}
class KdbxUnsupportedException implements KdbxException { class KdbxUnsupportedException implements KdbxException {
KdbxUnsupportedException(this.hint); KdbxUnsupportedException(this.hint);
@ -154,23 +272,3 @@ class HashedBlockReader {
} }
} }
} }
class ReaderHelper {
ReaderHelper(this.data);
final Uint8List data;
int pos = 0;
ByteBuffer _nextByteBuffer(int byteCount) =>
(data.sublist(pos, pos += byteCount) as Uint8List).buffer;
int readUint32() => _nextByteBuffer(4).asUint32List().first;
int readUint16() => _nextByteBuffer(2).asUint16List().first;
int readUint8() => data[pos++];
ByteBuffer readBytes(int size) => _nextByteBuffer(size);
Uint8List readRemaining() => data.sublist(pos) as Uint8List;
}

39
lib/src/kdbx_object.dart

@ -1,4 +1,7 @@
import 'package:meta/meta.dart'; import 'dart:convert';
import 'dart:typed_data';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
@ -15,25 +18,41 @@ class KdbxTimes {
abstract class KdbxNode { abstract class KdbxNode {
KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)); KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName));
KdbxNode.read(this.node); KdbxNode.read(this.node);
final XmlElement node; final XmlElement node;
@protected // @protected
String text(String nodeName) => _opt(nodeName)?.text; // String text(String nodeName) => _opt(nodeName)?.text;
KdbxSubTextNode textNode(String nodeName) => StringNode(this, nodeName);
XmlElement _opt(String nodeName) =>
node.findElements(nodeName).singleWhere((x) => true, orElse: () => null);
} }
abstract class KdbxObject extends KdbxNode { abstract class KdbxObject extends KdbxNode {
KdbxObject.create(String nodeName) KdbxObject.create(String nodeName)
: uuid = Uuid().v4(), : super.create(nodeName) {
super.create(nodeName); _uuid.set(KdbxUuid.random());
}
KdbxObject.read(XmlElement node) : super.read(node) { KdbxObject.read(XmlElement node) : super.read(node);
uuid = node.findElements('UUID').single.text;
KdbxUuid get uuid => _uuid.get();
UuidNode get _uuid => UuidNode(this, 'UUID');
} }
String uuid; class KdbxUuid {
const KdbxUuid(this.uuid);
KdbxUuid.random() : this(Uuid().v4());
/// base64 representation of uuid.
final String uuid;
ByteBuffer toBytes() => base64.decode(uuid).buffer;
@override
String toString() => uuid;
} }

78
lib/src/kdbx_xml.dart

@ -0,0 +1,78 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:kdbx/kdbx.dart';
import 'package:meta/meta.dart';
import 'package:xml/xml.dart';
abstract class KdbxSubNode<T> {
KdbxSubNode(this.node, this.name);
final KdbxNode node;
final String name;
T get();
void set(T value);
}
abstract class KdbxSubTextNode<T> extends KdbxSubNode<T> {
KdbxSubTextNode(KdbxNode node, String name) : super(node, name);
@protected
String encode(T value);
@protected
T decode(String value);
XmlElement _opt(String nodeName) =>
node.node.findElements(nodeName).singleWhere((x) => true, orElse: () => null);
@override
T get() {
return decode(_opt(name)?.text);
}
@override
void set(T value) {
final stringValue = encode(value);
final el =
node.node.findElements(name).singleWhere((x) => true, orElse: () {
final el = XmlElement(XmlName(name));
node.node.children.add(el);
return el;
});
el.children.clear();
el.children.add(XmlText(stringValue));
}
}
class StringNode extends KdbxSubTextNode<String> {
StringNode(KdbxNode node, String name) : super(node, name);
@override
String decode(String value) => value;
@override
String encode(String value) => value;
}
class Base64Node extends KdbxSubTextNode<ByteBuffer> {
Base64Node(KdbxNode node, String name) : super(node, name);
@override
ByteBuffer decode(String value) => base64.decode(value).buffer;
@override
String encode(ByteBuffer value) => base64.encode(value.asUint8List());
}
class UuidNode extends KdbxSubTextNode<KdbxUuid> {
UuidNode(KdbxNode node, String name) : super(node, name);
@override
KdbxUuid decode(String value) => KdbxUuid(value);
@override
String encode(KdbxUuid value) => value.uuid;
}

17
test/internal/byte_utils_test.dart

@ -0,0 +1,17 @@
import 'dart:io';
import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:test/test.dart';
void main() {
group('WriteHelper', () {
test('writing bytes', () {
final bytesBuilder = BytesBuilder();
final writer = WriterHelper(bytesBuilder);
writer.writeUint32(1);
print('result: ' + ByteUtils.toHexList(writer.output.toBytes()));
expect(writer.output.toBytes(), hasLength(4));
});
});
}

15
test/kdbx_test.dart

@ -12,12 +12,23 @@ import 'package:test/test.dart';
void main() { void main() {
Logger.root.level = Level.ALL; Logger.root.level = Level.ALL;
PrintAppender().attachToLogger(Logger.root); PrintAppender().attachToLogger(Logger.root);
group('A group of tests', () { group('Reading', () {
setUp(() {}); setUp(() {});
test('First Test', () async { test('First Test', () async {
final data = await File('test/FooBar.kdbx').readAsBytes() as Uint8List; final data = await File('test/FooBar.kdbx').readAsBytes() as Uint8List;
await KdbxFormat.read(data, Credentials(ProtectedValue.fromString('FooBar'))); KdbxFormat.read(data, Credentials(ProtectedValue.fromString('FooBar')));
});
});
group('Creating', () {
test('Simple create', () {
final kdbx = KdbxFormat.create(Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest');
expect(kdbx, isNotNull);
expect(kdbx.body.rootGroup, isNotNull);
expect(kdbx.body.rootGroup.name.get(), 'CreateTest');
expect(kdbx.body.meta.databaseName.get(), 'CreateTest');
print(kdbx.body.toXml().toXmlString(pretty: true));
}); });
}); });
} }

Loading…
Cancel
Save