diff --git a/lib/src/internal/byte_utils.dart b/lib/src/internal/byte_utils.dart index 560cf50..74d6842 100644 --- a/lib/src/internal/byte_utils.dart +++ b/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); } diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 3f1a9e0..3442915 100644 --- a/lib/src/kdbx_format.dart +++ b/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 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); + return crypto.sha512.convert(blockKeySrc.output.toBytes()).bytes + as Uint8List; + } + + Uint8List _hmacHashForBlock( + Uint8List hmacKey, int blockIndex, Uint8List blockData) { + final blockKey = _hmacKeyForBlockIndex(hmacKey, blockIndex); + final tmp = WriterHelper(); + tmp.writeUint64(blockIndex); + 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); + final calculatedHash = hmac.convert(tmp.output.toBytes()); + return calculatedHash.bytes as Uint8List; + } + Uint8List hmacBlockTransformer(Uint8List hmacKey, ReaderHelper reader) { final ret = []; int blockIndex = 0; while (true) { - final blockKeySrc = WriterHelper() - ..writeUint64(blockIndex) - ..writeBytes(hmacKey); - final blockKey = crypto.sha512.convert(blockKeySrc.output.toBytes()); - final blockHash = reader.readBytes(32); final blockLength = reader.readUint32(); final blockBytes = reader.readBytes(blockLength); - final tmp = WriterHelper(); - tmp.writeUint64(blockIndex); - tmp.writeInt32(blockLength); - tmp.writeBytes(blockBytes); -// _logger.fine('blockHash: ${ByteUtils.toHexList(tmp.output.toBytes())}'); -// _logger.fine('blockKey: ${ByteUtils.toHexList(blockKey.bytes)}'); - final hmac = crypto.Hmac(crypto.sha256, blockKey.bytes); - final calculatedHash = hmac.convert(tmp.output.toBytes()); + 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( diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart index e181b34..dd05f03 100644 --- a/lib/src/kdbx_header.dart +++ b/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); diff --git a/lib/src/kdbx_var_dictionary.dart b/lib/src/kdbx_var_dictionary.dart index 3f01e01..0ed250d 100644 --- a/lib/src/kdbx_var_dictionary.dart +++ b/lib/src/kdbx_var_dictionary.dart @@ -10,7 +10,7 @@ typedef Decoder = T Function(ReaderHelper reader, int length); typedef Encoder = 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 { typeString, typeBytes, ]; + + void encode(WriterHelper writer, T value) { + encoder(writer, value); + } } class VarDictionaryItem { @@ -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(); diff --git a/test/kdbx4_keeweb.kdbx b/test/kdbx4_keeweb.kdbx new file mode 100644 index 0000000..c9f6b18 Binary files /dev/null and b/test/kdbx4_keeweb.kdbx differ diff --git a/test/kdbx4_test.dart b/test/kdbx4_test.dart index baf4306..832d223 100644 --- a/test/kdbx4_test.dart +++ b/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(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('Password'), ProtectedValue.fromString('LoremIpsum')); + { + 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'))); }); }); } diff --git a/test/var_dictionary_test.dart b/test/var_dictionary_test.dart new file mode 100644 index 0000000..e877afb --- /dev/null +++ b/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); + }); +}