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 writeBytes(Uint8List bytes, [LengthWriter lengthWriter]) {
lengthWriter?.call(4);
lengthWriter?.call(bytes.length);
output.add(bytes);
// output.asUint8List().addAll(bytes);
}

76
lib/src/kdbx_format.dart

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

9
lib/src/kdbx_header.dart

@ -229,6 +229,7 @@ class KdbxHeader {
.where((f) => f != InnerHeaderFields.EndOfHeader)) {
_writeInnerField(writer, field);
}
_setInnerHeaderField(InnerHeaderFields.EndOfHeader, Uint8List(0));
_writeInnerField(writer, InnerHeaderFields.EndOfHeader);
}
@ -237,7 +238,8 @@ class KdbxHeader {
if (value == null) {
return;
}
_logger.finer('Writing header $field (${value.bytes.lengthInBytes})');
_logger.finer(
'Writing header $field (${field.index}) (${value.bytes.lengthInBytes})');
writer.writeUint8(field.index);
_writeFieldSize(writer, value.bytes.lengthInBytes);
writer.writeBytes(value.bytes);
@ -351,9 +353,10 @@ class KdbxHeader {
final headerId = reader.readUint8();
final int bodySize =
versionMajor >= 4 ? reader.readUint32() : reader.readUint16();
_logger.fine('Reading header with id $headerId (size: $bodySize)}');
final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null;
// _logger.finer(
// 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}');
_logger.finer(
'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}');
if (headerId > 0) {
final TE field = fields[headerId];
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);
extension on WriterHelper {
LengthWriter _lengthWriter() => (int length) => writeInt32(length);
LengthWriter _lengthWriter() => (int length) => writeUint32(length);
}
@immutable
@ -68,6 +68,10 @@ class ValueType<T> {
typeString,
typeBytes,
];
void encode(WriterHelper writer, T value) {
encoder(writer, value);
}
}
class VarDictionaryItem<T> {
@ -113,7 +117,9 @@ class VarDictionary {
final writer = WriterHelper();
writer.writeUint16(DEFAULT_VERSION);
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);
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:kdbx/kdbx.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_appenders/logging_appenders.dart';
import 'package:test/test.dart';
@ -63,13 +64,12 @@ class Argon2Test implements Argon2 {
int type,
int version,
) {
// print('hash: ${hashStuff('abc')}');
final keyArray = Uint8Array.fromTypedList(key);
// final saltArray = Uint8Array.fromTypedList(salt);
final saltArray = allocate<Uint8>(count: salt.length);
final saltList = saltArray.asTypedList(length);
saltList.setAll(0, salt);
const int memoryCost = 1 << 16;
// const memoryCost = 1 << 16;
// _logger.fine('saltArray: ${ByteUtils.toHexList(saltArray.view)}');
@ -78,21 +78,19 @@ class Argon2Test implements Argon2 {
keyArray.length,
saltArray,
salt.length,
memoryCost,
memory,
iterations,
parallelism,
length,
type,
version,
);
keyArray.free();
// saltArray.free();
free(saltArray);
final resultString = Utf8.fromUtf8(result);
return base64.decode(resultString);
}
// String hashStuff(String password) =>
// Utf8.fromUtf8(_hashStuff(Utf8.toUtf8(password)));
}
@ -114,21 +112,51 @@ void main() {
final pwd = firstEntry.getString(KdbxKey('Password')).getText();
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', () {
test('Create and save', () {
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 entry = KdbxEntry.create(kdbx, rootGroup);
rootGroup.addEntry(entry);
entry.setString(KdbxKey('Username'), PlainValue('user1'));
entry.setString(
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 loadedKdbx = kdbxFormat.read(saved, credentials);
final loadedKdbx = kdbxFormat.read(
saved, Credentials(ProtectedValue.fromString('asdf')));
_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