Browse Source

support for writing kdbx 4 files.

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
902e0ac3e3
  1. 2
      lib/src/internal/byte_utils.dart
  2. 76
      lib/src/kdbx_format.dart
  3. 9
      lib/src/kdbx_header.dart
  4. 10
      lib/src/kdbx_var_dictionary.dart
  5. BIN
      test/kdbx4_keeweb.kdbx
  6. 44
      test/kdbx4_test.dart
  7. 24
      test/var_dictionary_test.dart

2
lib/src/internal/byte_utils.dart

@ -100,7 +100,7 @@ class WriterHelper {
void _write(ByteData byteData) => output.add(byteData.buffer.asUint8List()); void _write(ByteData byteData) => output.add(byteData.buffer.asUint8List());
void writeBytes(Uint8List bytes, [LengthWriter lengthWriter]) { void writeBytes(Uint8List bytes, [LengthWriter lengthWriter]) {
lengthWriter?.call(4); lengthWriter?.call(bytes.length);
output.add(bytes); output.add(bytes);
// output.asUint8List().addAll(bytes); // output.asUint8List().addAll(bytes);
} }

76
lib/src/kdbx_format.dart

@ -252,7 +252,9 @@ class KdbxBody extends KdbxNode {
compressedBytes, compressedBytes,
keys.cipherKey, keys.cipherKey,
); );
writer.writeBytes(encrypted); final transformed = kdbxFile.kdbxFormat
.hmacBlockTransformerEncrypt(keys.hmacKey, encrypted);
writer.writeBytes(transformed);
} }
Uint8List _encryptV3(KdbxFile kdbxFile, Uint8List compressedBytes) { Uint8List _encryptV3(KdbxFile kdbxFile, Uint8List compressedBytes) {
@ -344,15 +346,20 @@ class KdbxFormat {
Credentials credentials, Credentials credentials,
String name, { String name, {
String generator, String generator,
KdbxHeader header,
}) { }) {
final header = KdbxHeader.create();
final meta = KdbxMeta.create( final meta = KdbxMeta.create(
databaseName: name, databaseName: name,
generator: generator, generator: generator,
); );
final rootGroup = KdbxGroup.create(parent: null, name: name); final rootGroup = KdbxGroup.create(parent: null, name: name);
final body = KdbxBody.create(meta, rootGroup); final body = KdbxBody.create(meta, rootGroup);
return KdbxFile(this, credentials, header, body); return KdbxFile(
this,
credentials,
header ?? KdbxHeader.create(),
body,
);
} }
KdbxFile read(Uint8List input, Credentials credentials) { KdbxFile read(Uint8List input, Credentials credentials) {
@ -428,29 +435,58 @@ class KdbxFormat {
return null; return null;
} }
Uint8List hmacBlockTransformer(Uint8List hmacKey, ReaderHelper reader) { Uint8List hmacBlockTransformerEncrypt(Uint8List hmacKey, Uint8List data) {
final ret = <int>[]; final writer = WriterHelper();
final reader = ReaderHelper(data);
const blockSize = 1024 * 1024;
int blockIndex = 0; int blockIndex = 0;
while (true) { while (true) {
final blockData = reader.readBytesUpTo(blockSize);
final calculatedHash = _hmacHashForBlock(hmacKey, blockIndex, blockData);
writer.writeBytes(calculatedHash);
writer.writeUint32(blockData.length);
if (blockData.isEmpty) {
// writer.writeUint32(0);
return writer.output.toBytes();
}
writer.writeBytes(blockData);
blockIndex++;
}
}
Uint8List _hmacKeyForBlockIndex(Uint8List hmacKey, int blockIndex) {
final blockKeySrc = WriterHelper() final blockKeySrc = WriterHelper()
..writeUint64(blockIndex) ..writeUint64(blockIndex)
..writeBytes(hmacKey); ..writeBytes(hmacKey);
final blockKey = crypto.sha512.convert(blockKeySrc.output.toBytes()); return crypto.sha512.convert(blockKeySrc.output.toBytes()).bytes
as Uint8List;
}
final blockHash = reader.readBytes(32); Uint8List _hmacHashForBlock(
final blockLength = reader.readUint32(); Uint8List hmacKey, int blockIndex, Uint8List blockData) {
final blockBytes = reader.readBytes(blockLength); final blockKey = _hmacKeyForBlockIndex(hmacKey, blockIndex);
final tmp = WriterHelper(); final tmp = WriterHelper();
tmp.writeUint64(blockIndex); tmp.writeUint64(blockIndex);
tmp.writeInt32(blockLength); tmp.writeInt32(blockData.length);
tmp.writeBytes(blockBytes); tmp.writeBytes(blockData);
// _logger.fine('blockHash: ${ByteUtils.toHexList(tmp.output.toBytes())}'); // _logger.fine('blockHash: ${ByteUtils.toHexList(tmp.output.toBytes())}');
// _logger.fine('blockKey: ${ByteUtils.toHexList(blockKey.bytes)}'); // _logger.fine('blockKey: ${ByteUtils.toHexList(blockKey.bytes)}');
final hmac = crypto.Hmac(crypto.sha256, blockKey.bytes); final hmac = crypto.Hmac(crypto.sha256, blockKey);
final calculatedHash = hmac.convert(tmp.output.toBytes()); final calculatedHash = hmac.convert(tmp.output.toBytes());
return calculatedHash.bytes as Uint8List;
}
Uint8List hmacBlockTransformer(Uint8List hmacKey, ReaderHelper reader) {
final ret = <int>[];
int blockIndex = 0;
while (true) {
final blockHash = reader.readBytes(32);
final blockLength = reader.readUint32();
final blockBytes = reader.readBytes(blockLength);
final calculatedHash = _hmacHashForBlock(hmacKey, blockIndex, blockBytes);
// _logger // _logger
// .fine('CalculatedHash: ${ByteUtils.toHexList(calculatedHash.bytes)}'); // .fine('CalculatedHash: ${ByteUtils.toHexList(calculatedHash.bytes)}');
if (!ByteUtils.eq(blockHash, calculatedHash.bytes)) { if (!ByteUtils.eq(blockHash, calculatedHash)) {
throw KdbxCorruptedFileException('Invalid hash block.'); throw KdbxCorruptedFileException('Invalid hash block.');
} }
@ -499,7 +535,9 @@ class KdbxFormat {
} }
final credentialHash = credentials.getHash(); final credentialHash = credentials.getHash();
_logger.finest('credentialHash: ${ByteUtils.toHexList(credentialHash)}');
final key = KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters); final key = KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters);
_logger.finest('key: ${ByteUtils.toHexList(key)}');
// final keyWithSeed = Uint8List(65); // final keyWithSeed = Uint8List(65);
// keyWithSeed.replaceRange(0, masterSeed.length, masterSeed); // keyWithSeed.replaceRange(0, masterSeed.length, masterSeed);
@ -589,17 +627,15 @@ class KdbxFormat {
return decrypted; return decrypted;
} }
/// TODO combine this with [_decryptContentV4] /// TODO combine this with [_decryptContentV4] (or [_encryptDataAes]?)
Uint8List _encryptContentV4Aes( Uint8List _encryptContentV4Aes(
KdbxHeader header, Uint8List cipherKey, Uint8List bytes) { KdbxHeader header, Uint8List cipherKey, Uint8List bytes) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes; final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine()); final encryptCypher = CBCBlockCipher(AESFastEngine());
decryptCipher.init( encryptCypher.init(
true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv)); true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
final paddedDecrypted = AesHelper.processBlocks(decryptCipher, bytes); final paddedBytes = AesHelper.pad(bytes, encryptCypher.blockSize);
return AesHelper.processBlocks(encryptCypher, paddedBytes);
final decrypted = AesHelper.unpad(paddedDecrypted);
return decrypted;
} }
static Uint8List _generateMasterKeyV3( static Uint8List _generateMasterKeyV3(

9
lib/src/kdbx_header.dart

@ -229,6 +229,7 @@ class KdbxHeader {
.where((f) => f != InnerHeaderFields.EndOfHeader)) { .where((f) => f != InnerHeaderFields.EndOfHeader)) {
_writeInnerField(writer, field); _writeInnerField(writer, field);
} }
_setInnerHeaderField(InnerHeaderFields.EndOfHeader, Uint8List(0));
_writeInnerField(writer, InnerHeaderFields.EndOfHeader); _writeInnerField(writer, InnerHeaderFields.EndOfHeader);
} }
@ -237,7 +238,8 @@ class KdbxHeader {
if (value == null) { if (value == null) {
return; return;
} }
_logger.finer('Writing header $field (${value.bytes.lengthInBytes})'); _logger.finer(
'Writing header $field (${field.index}) (${value.bytes.lengthInBytes})');
writer.writeUint8(field.index); writer.writeUint8(field.index);
_writeFieldSize(writer, value.bytes.lengthInBytes); _writeFieldSize(writer, value.bytes.lengthInBytes);
writer.writeBytes(value.bytes); writer.writeBytes(value.bytes);
@ -351,9 +353,10 @@ class KdbxHeader {
final headerId = reader.readUint8(); final headerId = reader.readUint8();
final int bodySize = final int 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 TE field = fields[headerId]; final TE field = fields[headerId];
yield createField(field, bodyBytes); yield createField(field, bodyBytes);

10
lib/src/kdbx_var_dictionary.dart

@ -10,7 +10,7 @@ typedef Decoder<T> = T Function(ReaderHelper reader, int length);
typedef Encoder<T> = void Function(WriterHelper writer, T value); typedef Encoder<T> = void Function(WriterHelper writer, T value);
extension on WriterHelper { extension on WriterHelper {
LengthWriter _lengthWriter() => (int length) => writeInt32(length); LengthWriter _lengthWriter() => (int length) => writeUint32(length);
} }
@immutable @immutable
@ -68,6 +68,10 @@ class ValueType<T> {
typeString, typeString,
typeBytes, typeBytes,
]; ];
void encode(WriterHelper writer, T value) {
encoder(writer, value);
}
} }
class VarDictionaryItem<T> { class VarDictionaryItem<T> {
@ -113,7 +117,9 @@ class VarDictionary {
final writer = WriterHelper(); final writer = WriterHelper();
writer.writeUint16(DEFAULT_VERSION); writer.writeUint16(DEFAULT_VERSION);
for (final item in _items) { for (final item in _items) {
item._valueType.encoder(writer, item._value); writer.writeUint8(item._valueType.code);
ValueType.typeString.encode(writer, item._key);
item._valueType.encode(writer, item._value);
} }
writer.writeUint8(0); writer.writeUint8(0);
return writer.output.toBytes(); return writer.output.toBytes();

BIN
test/kdbx4_keeweb.kdbx

Binary file not shown.

44
test/kdbx4_test.dart

@ -7,6 +7,7 @@ import 'package:ffi/ffi.dart';
import 'package:ffi_helper/ffi_helper.dart'; import 'package:ffi_helper/ffi_helper.dart';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/crypto/key_encrypter_kdf.dart'; import 'package:kdbx/src/crypto/key_encrypter_kdf.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';
@ -63,13 +64,12 @@ class Argon2Test implements Argon2 {
int type, int type,
int version, int version,
) { ) {
// print('hash: ${hashStuff('abc')}');
final keyArray = Uint8Array.fromTypedList(key); final keyArray = Uint8Array.fromTypedList(key);
// final saltArray = Uint8Array.fromTypedList(salt); // final saltArray = Uint8Array.fromTypedList(salt);
final saltArray = allocate<Uint8>(count: salt.length); final saltArray = allocate<Uint8>(count: salt.length);
final saltList = saltArray.asTypedList(length); final saltList = saltArray.asTypedList(length);
saltList.setAll(0, salt); saltList.setAll(0, salt);
const int memoryCost = 1 << 16; // const memoryCost = 1 << 16;
// _logger.fine('saltArray: ${ByteUtils.toHexList(saltArray.view)}'); // _logger.fine('saltArray: ${ByteUtils.toHexList(saltArray.view)}');
@ -78,21 +78,19 @@ class Argon2Test implements Argon2 {
keyArray.length, keyArray.length,
saltArray, saltArray,
salt.length, salt.length,
memoryCost, memory,
iterations, iterations,
parallelism, parallelism,
length, length,
type, type,
version, version,
); );
keyArray.free(); keyArray.free();
// saltArray.free(); // saltArray.free();
free(saltArray); free(saltArray);
final resultString = Utf8.fromUtf8(result); final resultString = Utf8.fromUtf8(result);
return base64.decode(resultString); return base64.decode(resultString);
} }
// String hashStuff(String password) => // String hashStuff(String password) =>
// Utf8.fromUtf8(_hashStuff(Utf8.toUtf8(password))); // Utf8.fromUtf8(_hashStuff(Utf8.toUtf8(password)));
} }
@ -114,21 +112,51 @@ void main() {
final pwd = firstEntry.getString(KdbxKey('Password')).getText(); final pwd = firstEntry.getString(KdbxKey('Password')).getText();
expect(pwd, 'MyPassword'); expect(pwd, 'MyPassword');
}); });
test('Reading kdbx4_keeweb', () async {
final data = await File('test/kdbx4_keeweb.kdbx').readAsBytes();
final file =
kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf')));
final firstEntry = file.body.rootGroup.entries.first;
final pwd = firstEntry.getString(KdbxKey('Password')).getText();
expect(pwd, 'def');
});
}); });
group('Writing', () { group('Writing', () {
test('Create and save', () { test('Create and save', () {
final credentials = Credentials(ProtectedValue.fromString('asdf')); final credentials = Credentials(ProtectedValue.fromString('asdf'));
final kdbx = kdbxFormat.create(credentials, 'Test Keystore'); final kdbx = kdbxFormat.create(
credentials,
'Test Keystore',
header: KdbxHeader.createV4(),
);
final rootGroup = kdbx.body.rootGroup; final rootGroup = kdbx.body.rootGroup;
{
final entry = KdbxEntry.create(kdbx, rootGroup); final entry = KdbxEntry.create(kdbx, rootGroup);
rootGroup.addEntry(entry); rootGroup.addEntry(entry);
entry.setString(KdbxKey('Username'), PlainValue('user1'));
entry.setString( entry.setString(
KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum')); KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum'));
}
{
final entry = KdbxEntry.create(kdbx, rootGroup);
rootGroup.addEntry(entry);
entry.setString(KdbxKey('Username'), PlainValue('user2'));
entry.setString(
KdbxKey('Password'),
ProtectedValue.fromString('Second Password'),
);
}
final saved = kdbx.save(); final saved = kdbx.save();
final loadedKdbx = kdbxFormat.read(saved, credentials); final loadedKdbx = kdbxFormat.read(
saved, Credentials(ProtectedValue.fromString('asdf')));
_logger.fine('Successfully loaded kdbx $loadedKdbx'); _logger.fine('Successfully loaded kdbx $loadedKdbx');
File('test_v4.kdbx').writeAsBytesSync(saved); File('test_v4x.kdbx').writeAsBytesSync(saved);
});
test('Reading it', () async {
final data = await File('test/test_v4x.kdbx').readAsBytes();
final file =
kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf')));
}); });
}); });
} }

24
test/var_dictionary_test.dart

@ -0,0 +1,24 @@
import 'package:kdbx/src/crypto/key_encrypter_kdf.dart';
import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:kdbx/src/kdbx_var_dictionary.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:test/test.dart';
final _logger = Logger('var_dictionary_test');
void main() {
Logger.root.level = Level.ALL;
PrintAppender().attachToLogger(Logger.root);
test('write and read var dictionary', () {
final dict = VarDictionary([
KdfField.rounds.item(99),
KdfField.uuid
.item(KeyEncrypterKdf.kdfUuidForType(KdfType.Argon2).toBytes()),
]);
final serialized = dict.write();
_logger.fine('Serialized dictionary: ${ByteUtils.toHexList(serialized)}');
final r = VarDictionary.read(ReaderHelper(serialized));
expect(KdfField.rounds.read(r), 99);
});
}
Loading…
Cancel
Save