Browse Source

basic support for creating and saving kdbx files.

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
3ec6883c75
  1. 11
      lib/src/crypto/protected_salt_generator.dart
  2. 20
      lib/src/internal/byte_utils.dart
  3. 11
      lib/src/internal/crypto_utils.dart
  4. 36
      lib/src/kdbx_entry.dart
  5. 88
      lib/src/kdbx_format.dart
  6. 9
      lib/src/kdbx_group.dart
  7. 61
      lib/src/kdbx_header.dart
  8. 7
      lib/src/kdbx_object.dart
  9. 6
      lib/src/kdbx_xml.dart
  10. 40
      test/kdbx_test.dart

11
lib/src/crypto/protected_salt_generator.dart

@ -11,15 +11,20 @@ class ProtectedSaltGenerator {
return ProtectedSaltGenerator._(cipher);
}
ProtectedSaltGenerator._(this.cipher);
ProtectedSaltGenerator._(this._cipher);
static final SalsaNonce = Uint8List.fromList([0xE8, 0x30, 0x09, 0x4B, 0x97, 0x20, 0x5D, 0x2A]);
final StreamCipher cipher;
final StreamCipher _cipher;
String decryptBase64(String protectedValue) {
final bytes = base64.decode(protectedValue);
final result = cipher.process(bytes);
final result = _cipher.process(bytes);
final decrypted = utf8.decode(result);
return decrypted;
}
String encryptToBase64(String plainValue) {
final encrypted = _cipher.process(utf8.encode(plainValue) as Uint8List);
return base64.encode(encrypted);
}
}

20
lib/src/internal/byte_utils.dart

@ -1,8 +1,14 @@
import 'dart:ffi';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
class ByteUtils {
static final _random = Random.secure();
static Uint8List randomBytes(int length) =>
Uint8List.fromList(List.generate(length, (i) => _random.nextInt(1 << 8)));
static bool eq(List<int> a, List<int> b) {
if (a.length != b.length) {
return false;
@ -17,7 +23,8 @@ class ByteUtils {
static String toHex(int val) => '0x${val.toRadixString(16)}';
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 {
@ -37,11 +44,14 @@ class ReaderHelper {
ByteBuffer readBytes(int size) => _nextByteBuffer(size);
ByteBuffer readBytesUpTo(int maxSize) =>
_nextByteBuffer(min(maxSize, data.lengthInBytes - pos));
Uint8List readRemaining() => data.sublist(pos) as Uint8List;
}
class WriterHelper {
WriterHelper(this.output);
WriterHelper([BytesBuilder output]) : output = output ?? BytesBuilder();
final BytesBuilder output;
@ -55,8 +65,12 @@ class WriterHelper {
// output.asUint32List().add(value);
}
void writeUint64(int value) {
output.add(Uint64List.fromList([value]).buffer.asUint8List());
}
void writeUint16(int value) {
output.add(Uint32List.fromList([value]).buffer.asUint32List());
output.add(Uint16List.fromList([value]).buffer.asUint8List());
}
void writeUint8(int value) {

11
lib/src/internal/crypto_utils.dart

@ -67,6 +67,17 @@ class AesHelper {
return Uint8List(len)..setRange(0, len, src);
}
static Uint8List pad(Uint8List src, int blockSize) {
final pad = PKCS7Padding();
pad.init(null);
final padLength = blockSize - (src.length % blockSize);
final out = Uint8List(src.length + padLength)..setAll(0, src);
pad.addPadding(out, src.length);
return out;
}
static Uint8List processBlocks(BlockCipher cipher, Uint8List inp) {
final out = Uint8List(inp.lengthInBytes);

36
lib/src/kdbx_entry.dart

@ -6,8 +6,6 @@ import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_object.dart';
import 'package:xml/xml.dart';
String _canonicalizeKey(String key) => key?.toLowerCase();
/// Represents a case insensitive (but case preserving) key.
class KdbxKey {
KdbxKey(this.key) : _canonicalKey = key.toLowerCase();
@ -29,7 +27,7 @@ class KdbxEntry extends KdbxObject {
}
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 = KdbxKey(el.findElements('Key').single.text);
final valueNode = el.findElements('Value').single;
if (valueNode.getAttribute('Protected')?.toLowerCase() == 'true') {
@ -40,11 +38,39 @@ class KdbxEntry extends KdbxObject {
}));
}
XmlElement toXml() {
final el = node.copy() as XmlElement;
el.children.removeWhere((e) => e is XmlElement && e.name.local == 'String');
el.children.addAll(strings.entries.map((stringEntry) {
final value = XmlElement(XmlName('Value'));
if (stringEntry.value is ProtectedValue) {
value.attributes.add(XmlAttribute(XmlName('Protected'), 'true'));
KdbxFile.setProtectedValueForNode(
value, stringEntry.value as ProtectedValue);
} else {
value.children.add(XmlText(stringEntry.value.getText()));
}
return XmlElement(XmlName('String'))
..children.addAll([
XmlElement(XmlName('Key'))
..children.add(XmlText(stringEntry.key.key)),
value,
]);
}));
return el;
}
KdbxGroup parent;
Map<KdbxKey, StringValue> strings = <KdbxKey, StringValue>{};
final Map<KdbxKey, StringValue> _strings = {};
Map<KdbxKey, StringValue> get strings => UnmodifiableMapView(_strings);
void setString(KdbxKey key, StringValue value) {
_strings[key] = value;
}
String _plainValue(KdbxKey key) {
final value = strings[key];
final value = _strings[key];
if (value is PlainValue) {
return value.getText();
}

88
lib/src/kdbx_format.dart

@ -43,6 +43,10 @@ class KdbxFile {
return protectedValues[node];
}
static void setProtectedValueForNode(xml.XmlElement node, ProtectedValue value) {
protectedValues[node] = value;
}
final Credentials credentials;
final KdbxHeader header;
final KdbxBody body;
@ -55,6 +59,7 @@ class KdbxFile {
body.write(writer, this);
return output.toBytes();
}
}
class KdbxBody extends KdbxNode {
@ -64,7 +69,9 @@ class KdbxBody extends KdbxNode {
node.children.add(rootNode);
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;
@ -72,18 +79,63 @@ class KdbxBody extends KdbxNode {
void write(WriterHelper writer, KdbxFile kdbxFile) {
assert(kdbxFile.header.versionMajor == 3);
_writeV3(writer, kdbxFile);
final streamKey = kdbxFile
.header.fields[HeaderFields.ProtectedStreamKey].bytes
.asUint8List();
final gen = ProtectedSaltGenerator(streamKey);
_writeV3(writer, kdbxFile, gen);
}
void _writeV3(WriterHelper writer, KdbxFile kdbxFile) {
meta.headerHash.set((crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List).buffer);
void _writeV3(WriterHelper writer, KdbxFile kdbxFile,
ProtectedSaltGenerator saltGenerator) {
meta.headerHash.set(
(crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List)
.buffer);
final xml = toXml(saltGenerator);
final xmlBytes = utf8.encode(xml.toXmlString());
final Uint8List compressedBytes = (kdbxFile.header.compression == Compression.gzip ?
GZipCodec().encode(xmlBytes) : xmlBytes) as Uint8List;
final byteWriter = WriterHelper();
byteWriter.writeBytes(kdbxFile.header.fields[HeaderFields.StreamStartBytes].bytes.asUint8List());
HashedBlockReader.writeBlocks(ReaderHelper(compressedBytes), byteWriter);
final bytes = byteWriter.output.toBytes();
final masterKey = KdbxFormat._generateMasterKeyV3(kdbxFile.header, kdbxFile.credentials);
final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes, kdbxFile.header.fields[HeaderFields.EncryptionIV].bytes.asUint8List());
// writer.writeBytes(kdbxFile.header.fields[HeaderFields.StreamStartBytes].bytes.asUint8List());
writer.writeBytes(encrypted);
}
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;
xml.XmlDocument toXml(ProtectedSaltGenerator saltGenerator) {
final rootGroupNode = rootGroup.toXml();
// update protected values...
for (final el in rootGroupNode
.findAllElements('Value')
.where((el) => el.getAttribute('Protected')?.toLowerCase() == 'true')) {
final pv = KdbxFile.protectedValues[el];
if (pv != null) {
final newValue = saltGenerator.encryptToBase64(pv.getText());
el.children.clear();
el.children.add(xml.XmlText(newValue));
} else {
_logger.warning('Unable to find protected value for $el ${el.parent}');
}
}
final builder = xml.XmlBuilder();
builder.processing('xml', 'version="1.0" encoding="utf-8" standalone="yes"');
builder.element('KeePassFile', nest: [
meta.toXml(),
() => builder.element('Root', nest: rootGroupNode),],);
// final doc = xml.XmlDocument();
// doc.children.add(xml.XmlProcessing(
// 'xml', 'version="1.0" encoding="utf-8" standalone="yes"'));
final node = builder.build() as xml.XmlDocument;
return node;
}
}
@ -91,15 +143,19 @@ class KdbxMeta extends KdbxNode {
KdbxMeta.create({@required String databaseName}) : super.create('Meta') {
this.databaseName.set(databaseName);
}
KdbxMeta.read(xml.XmlElement node) : super.read(node);
StringNode get databaseName => StringNode(this, 'DatabaseName');
Base64Node get headerHash => Base64Node(this, 'HeaderHash');
xml.XmlElement toXml() {
return node;
}
}
class KdbxFormat {
static KdbxFile create(Credentials credentials, String name) {
final header = KdbxHeader.create();
final meta = KdbxMeta.create(databaseName: name);
@ -108,7 +164,6 @@ class KdbxFormat {
return KdbxFile(credentials, header, body);
}
static KdbxFile read(Uint8List input, Credentials credentials) {
final reader = ReaderHelper(input);
final header = KdbxHeader.read(reader);
@ -167,7 +222,8 @@ class KdbxFormat {
final decryptCipher = CBCBlockCipher(AESFastEngine());
decryptCipher.init(false,
ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List()));
final decrypted = AesHelper.processBlocks(decryptCipher, encryptedPayload);
final paddedDecrypted = AesHelper.processBlocks(decryptCipher, encryptedPayload);
final decrypted = paddedDecrypted;//AesHelper.unpad(paddedDecrypted);
final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes;
@ -205,4 +261,12 @@ class KdbxFormat {
.bytes as Uint8List;
return masterKey;
}
static Uint8List _encryptDataAes(Uint8List masterKey, Uint8List payload, Uint8List encryptionIv) {
final encryptCipher = CBCBlockCipher(AESFastEngine());
encryptCipher.init(true,
ParametersWithIV(KeyParameter(masterKey), encryptionIv));
return AesHelper.processBlocks(encryptCipher, AesHelper.pad(payload, encryptCipher.blockSize));
}
}

9
lib/src/kdbx_group.dart

@ -23,6 +23,15 @@ class KdbxGroup extends KdbxObject {
.map((el) => KdbxEntry.read(this, el))
.forEach(_entries.add);
}
XmlElement toXml() {
final el = node.copy() as XmlElement;
XmlUtils.removeChildrenByName(el, 'Group');
XmlUtils.removeChildrenByName(el, 'Entry');
el.children.addAll(groups.map((g) => g.toXml()));
el.children.addAll(_entries.map((e) => e.toXml()));
return el;
}
/// Returns all groups plus this group itself.
List<KdbxGroup> getAllGroups() => groups

61
lib/src/kdbx_header.dart

@ -70,8 +70,10 @@ class KdbxHeader {
fields: _defaultFieldValues(),
);
static ByteBuffer _intAsUintBytes(int val) =>
Uint8List.fromList([val]).buffer;
static ByteBuffer _intAsUint32Bytes(int val) =>
(WriterHelper()..writeUint32(val)).output.toBytes().buffer;
static ByteBuffer _intAsUint64Bytes(int val) =>
(WriterHelper()..writeUint64(val)).output.toBytes().buffer;
static List<HeaderFields> _requiredFields(int majorVersion) {
if (majorVersion < 3) {
@ -113,16 +115,18 @@ class KdbxHeader {
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);
// use like [SecureRandom] from PointyCastle?
_setHeaderField(HeaderFields.MasterSeed, ByteUtils.randomBytes(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);
_setHeaderField(HeaderFields.TransformSeed, ByteUtils.randomBytes(32).buffer);
_setHeaderField(
HeaderFields.StreamStartBytes, ByteUtils.randomBytes(32).buffer);
_setHeaderField(
HeaderFields.ProtectedStreamKey, ByteUtils.randomBytes(32).buffer);
_setHeaderField(HeaderFields.EncryptionIV, ByteUtils.randomBytes(16).buffer);
} else {
throw KdbxUnsupportedException('We do not support Kdbx 4.x right now. ($versionMajor.$versionMinor)');
throw KdbxUnsupportedException(
'We do not support Kdbx 4.x right now. ($versionMajor.$versionMinor)');
}
}
@ -138,6 +142,7 @@ class KdbxHeader {
in HeaderFields.values.where((f) => f != HeaderFields.EndOfHeader)) {
_writeField(writer, field);
}
fields[HeaderFields.EndOfHeader] = HeaderField(HeaderFields.EndOfHeader, Uint8List(0).buffer);
_writeField(writer, HeaderFields.EndOfHeader);
}
@ -146,6 +151,8 @@ class KdbxHeader {
if (value == null) {
return;
}
_logger.finer('Writing header $field (${value.bytes.lengthInBytes})');
writer.writeUint8(field.index);
_writeFieldSize(writer, value.bytes.lengthInBytes);
writer.writeBytes(value.bytes.asUint8List());
}
@ -162,11 +169,11 @@ class KdbxHeader {
Map.fromEntries([
HeaderField(HeaderFields.CipherID,
CryptoConsts.CIPHER_IDS[Cipher.aes].toBytes()),
HeaderField(HeaderFields.CompressionFlags, _intAsUintBytes(1)),
HeaderField(HeaderFields.TransformRounds, _intAsUintBytes(6000)),
HeaderField(HeaderFields.CompressionFlags, _intAsUint32Bytes(1)),
HeaderField(HeaderFields.TransformRounds, _intAsUint64Bytes(6000)),
HeaderField(
HeaderFields.InnerRandomStreamID,
_intAsUintBytes(ProtectedValueEncryption.values
_intAsUint32Bytes(ProtectedValueEncryption.values
.indexOf(ProtectedValueEncryption.salsa20))),
].map((f) => MapEntry(f.field, f)));
@ -251,13 +258,16 @@ class KdbxUnsupportedException implements KdbxException {
}
class HashedBlockReader {
static const BLOCK_SIZE = 1024 * 1024;
static const HASH_SIZE = 32;
static Uint8List readBlocks(ReaderHelper reader) =>
Uint8List.fromList(readNextBlock(reader).expand((x) => x).toList());
static Iterable<Uint8List> readNextBlock(ReaderHelper reader) sync* {
while (true) {
final blockIndex = reader.readUint32();
final blockHash = reader.readBytes(32);
final blockHash = reader.readBytes(HASH_SIZE);
final blockSize = reader.readUint32();
if (blockSize > 0) {
final blockData = reader.readBytes(blockSize).asUint8List();
@ -271,4 +281,27 @@ class HashedBlockReader {
}
}
}
// static Uint8List writeBlocks(WriterHelper writer) =>
static void writeBlocks(ReaderHelper reader, WriterHelper writer) {
while (true) {
int blockIndex = 0;
final block = reader.readBytesUpTo(BLOCK_SIZE);
if (block.lengthInBytes == 0) {
// written all data, write a last empty block.
writer.writeUint32(blockIndex++);
writer.writeBytes(Uint8List.fromList(List.generate(HASH_SIZE, (i) => 0)));
writer.writeUint32(0);
return;
}
final blockSize = block.lengthInBytes;
final blockHash = crypto.sha256.convert(block.asUint8List());
assert(blockHash.bytes.length == HASH_SIZE);
writer.writeUint32(blockIndex++);
writer.writeBytes(blockHash.bytes as Uint8List);
writer.writeUint32(blockSize);
writer.writeBytes(block.asUint8List());
}
}
}

7
lib/src/kdbx_object.dart

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:uuid/uuid.dart';
import 'package:uuid/uuid_util.dart';
import 'package:xml/xml.dart';
class KdbxTimes {
@ -47,8 +48,12 @@ abstract class KdbxObject extends KdbxNode {
class KdbxUuid {
const KdbxUuid(this.uuid);
KdbxUuid.random() : this(uuidGenerator.v4());
static final Uuid uuidGenerator = Uuid(options: <String, dynamic>{
'grng': UuidUtil.cryptoRNG
});
KdbxUuid.random() : this(Uuid().v4());
/// base64 representation of uuid.
final String uuid;

6
lib/src/kdbx_xml.dart

@ -115,3 +115,9 @@ class BooleanNode extends KdbxSubTextNode<bool> {
String encode(bool value) => value ? 'true' : 'false';
}
class XmlUtils {
static void removeChildrenByName(XmlNode node, String name) {
node.children.removeWhere((node) => node is XmlElement && node.name.local == name);
}
}

40
test/kdbx_test.dart

@ -2,13 +2,23 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/crypto/protected_salt_generator.dart';
import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:kdbx/src/kdbx_format.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';
class FakeProtectedSaltGenerator implements ProtectedSaltGenerator {
@override
String decryptBase64(String protectedValue) => 'fake';
@override
String encryptToBase64(String plainValue) => 'fake';
}
void main() {
Logger.root.level = Level.ALL;
PrintAppender().attachToLogger(Logger.root);
@ -28,13 +38,35 @@ void main() {
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));
print(kdbx.body.toXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true));
});
test('Create Entry', () {
final kdbx = KdbxFormat.create(Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest');
final rootGroup = kdbx.body.rootGroup;
rootGroup.addEntry(KdbxEntry.create(rootGroup));
print(kdbx.body.toXml().toXmlString(pretty: true));
final entry = KdbxEntry.create(rootGroup);
rootGroup.addEntry(entry);
entry.setString(KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum'));
print(kdbx.body.toXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true));
});
});
group('Integration', () {
test('Simple save and load', () {
final credentials = Credentials(ProtectedValue.fromString('FooBar'));
Uint8List saved = (() {
final kdbx = KdbxFormat.create(credentials, 'CreateTest');
final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(rootGroup);
rootGroup.addEntry(entry);
entry.setString(
KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum'));
return kdbx.save();
})();
// print(ByteUtils.toHexList(saved));
final kdbx = KdbxFormat.read(saved, credentials);
expect(kdbx.body.rootGroup.entries.first.strings[KdbxKey('Password')].getText(), 'LoremIpsum');
});
});
}

Loading…
Cancel
Save