From 902e0ac3e3df91067603e4bff62517bc187a31c8 Mon Sep 17 00:00:00 2001 From: Herbert Poul Date: Wed, 26 Feb 2020 17:09:27 +0100 Subject: [PATCH] support for writing kdbx 4 files. --- lib/src/internal/byte_utils.dart | 2 +- lib/src/kdbx_format.dart | 84 ++++++++++++++++++++++--------- lib/src/kdbx_header.dart | 9 ++-- lib/src/kdbx_var_dictionary.dart | 10 +++- test/kdbx4_keeweb.kdbx | Bin 0 -> 1477 bytes test/kdbx4_test.dart | 52 ++++++++++++++----- test/var_dictionary_test.dart | 24 +++++++++ 7 files changed, 139 insertions(+), 42 deletions(-) create mode 100644 test/kdbx4_keeweb.kdbx create mode 100644 test/var_dictionary_test.dart 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 0000000000000000000000000000000000000000..c9f6b186e6b4461a41673ec6b3f2552ec35f544e GIT binary patch literal 1477 zcmV;$1v>fz*`k_f`%AR}00aO65C8xGF~RcYzi~rQzE}kzYW!ON0|Wp70096100bZa z002HjHj44D*k@u;j1LFz|LID5(08=0U001;r z!5McGY#h9R&u%G{4U8$D9sw$_XqivxFb2btMNI?&0000`1ONa40RR911pxp607(b{ z0006200000000F60000@2mk;8000mG000001OWg509FJ5000vJ000001ONa40MM-p zomH$9M}s1iyzCJiVQaLKGJ<0WCaJEpH z>9Om)v0*=9CpoX4*K*X&q$RScq*xfVd>zVcdVDKJRvGJA^npJ*4$}^wKm-5)YmzU) z&cH=Kv+Z_Duy2=x-A6pdaGH+fdNM+8gX)>=1$L$&k$`(gfKC~nTL=T+IfC0%-)#5}#v7N4G4`Va)6#$KaCO3oXs;0jkWg@p&TeCyzP# zM)VDV253w(i%t2-=|FhY(iTHDDWCihmCzg=?ZRJ4EURKa!L%Fv@89X=CcGAj50zKw z7*TUoKeD3o*R(@P66Y$;z08JaW=qCg_6iSTKE=Z3%&@R0ctNe#{v;S0t3w9INadC; zxq78O_4exHiY2+u7>B{V4-B@>XLI%mYyxbGe+KOt9x8+$FNEeQ?Nb%+z1AoGRm+?i z$jl_Pk9| z4qZhW)+d4HL|pdI5CH90_^!tf7ZZdwU$sWv29Pa(WbL8h{o=rQT|UIgfWjQ2hLBfo zfRaVK-mBSRAN;<>^{l{p@;&9xgF-DS?6A-NW#{pr2=h#Wdpj$n?E#7Gx_?S4Bf`~i zo(o>*?~-QR&wxA-D({c}*(d;UM@m3)*TUL>PDD#kf{Y<&8jb6t>-%*R;HC`wE_Hp# zJ!z51VM$fvU>t4x$EkH*on<*<>sDL-!B0W0pA6ml>^S$7xy7*eaZ9ueUGLi~x8sTF z^di-l?Jd-NTpGY1j@A^B4>ezt5oHKCjTy5^kDx!qj@H_9*E|O+Cfsmk*6pYU?N5;h z4Yd0Ty-*mJS?97~cR0oC2&v@jdhN(1E&%!WRk$dW40;Qnq^7sP7Q}q?)}$x!?mbz* zC2I(0o6*mBD225GF04p|0P%6|Fx+&XJgAr{)ie3#m>ii?_XtP{tKOtCV) zG1Df(rd+mWj*1395#pw_Y`S=NAn!(|AB52$|0I z(xL9!Qq-jWrDd+HQdm52M$-Muu$3(aSJErL@bXo?_bp)s^7yQk{UJJrF~SJfux|z< z$*6A1+(SJw;J&e<|9w|^`GkoSrbu33wOs1PN8;Dx+$tUVDIK=E!1 zhO4+*u*t0z1q7gO%GF*|Fg@2%X(s%kQoE_FP!i;EOufHx84WNCdt%#-rb|0=Ha^V;?|{z6EG*7Xy0Q0;X2!8|UXMj2 zM{Voj`(<7d5@*evl=s3s fg+uYGN1?Fv#7NTJFJSF;zS2Ty1TO9d000006^*SL literal 0 HcmV?d00001 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); + }); +}