diff --git a/bin/kdbx.dart b/bin/kdbx.dart index cbab6c7..9811553 100644 --- a/bin/kdbx.dart +++ b/bin/kdbx.dart @@ -74,7 +74,7 @@ abstract class KdbxFileCommand extends Command { final bytes = await File(inputFile).readAsBytes(); final password = prompts.get('Password for $inputFile', conceal: true, validate: (str) => str.isNotEmpty); - final file = KdbxFormat(null) + final file = await KdbxFormat(null) .read(bytes, Credentials(ProtectedValue.fromString(password))); return runWithFile(file); } diff --git a/lib/kdbx.dart b/lib/kdbx.dart index dd6f1c0..a97fdeb 100644 --- a/lib/kdbx.dart +++ b/lib/kdbx.dart @@ -2,7 +2,6 @@ library kdbx; export 'src/crypto/argon2.dart'; -export 'src/crypto/key_encrypter_kdf.dart' show Argon2; export 'src/crypto/protected_value.dart' show ProtectedValue, StringValue, PlainValue; export 'src/kdbx_consts.dart'; diff --git a/lib/src/crypto/argon2.dart b/lib/src/crypto/argon2.dart index 25094bc..ac3df28 100644 --- a/lib/src/crypto/argon2.dart +++ b/lib/src/crypto/argon2.dart @@ -6,6 +6,8 @@ import 'package:ffi/ffi.dart'; import 'package:ffi_helper/ffi_helper.dart'; import 'package:meta/meta.dart'; +// ignore_for_file: non_constant_identifier_names + typedef Argon2HashNative = Pointer Function( Pointer key, IntPtr keyLen, @@ -33,16 +35,23 @@ typedef Argon2Hash = Pointer Function( ); abstract class Argon2 { - Uint8List argon2( - Uint8List key, - Uint8List salt, - int memory, - int iterations, - int length, - int parallelism, - int type, - int version, - ); + Uint8List argon2(Argon2Arguments args); + + Future argon2Async(Argon2Arguments args); +} + +class Argon2Arguments { + Argon2Arguments(this.key, this.salt, this.memory, this.iterations, + this.length, this.parallelism, this.type, this.version); + + final Uint8List key; + final Uint8List salt; + final int memory; + final int iterations; + final int length; + final int parallelism; + final int type; + final int version; } abstract class Argon2Base extends Argon2 { @@ -50,21 +59,12 @@ abstract class Argon2Base extends Argon2 { Argon2Hash get argon2hash; @override - Uint8List argon2( - Uint8List key, - Uint8List salt, - int memory, - int iterations, - int length, - int parallelism, - int type, - int version, - ) { - final keyArray = Uint8Array.fromTypedList(key); + Uint8List argon2(Argon2Arguments args) { + final keyArray = Uint8Array.fromTypedList(args.key); // final saltArray = Uint8Array.fromTypedList(salt); - final saltArray = allocate(count: salt.length); - final saltList = saltArray.asTypedList(length); - saltList.setAll(0, salt); + final saltArray = allocate(count: args.salt.length); + final saltList = saltArray.asTypedList(args.length); + saltList.setAll(0, args.salt); // const memoryCost = 1 << 16; // _logger.fine('saltArray: ${ByteUtils.toHexList(saltArray.view)}'); @@ -73,13 +73,13 @@ abstract class Argon2Base extends Argon2 { keyArray.rawPtr, keyArray.length, saltArray, - salt.length, - memory, - iterations, - parallelism, - length, - type, - version, + args.salt.length, + args.memory, + args.iterations, + args.parallelism, + args.length, + args.type, + args.version, ); keyArray.free(); @@ -88,4 +88,9 @@ abstract class Argon2Base extends Argon2 { final resultString = Utf8.fromUtf8(result); return base64.decode(resultString); } + + @override + Future argon2Async(Argon2Arguments args) async { + return argon2(args); + } } diff --git a/lib/src/crypto/key_encrypter_kdf.dart b/lib/src/crypto/key_encrypter_kdf.dart index 36ae5ee..81ff6c8 100644 --- a/lib/src/crypto/key_encrypter_kdf.dart +++ b/lib/src/crypto/key_encrypter_kdf.dart @@ -78,7 +78,7 @@ class KeyEncrypterKdf { final Argon2 argon2; - Uint8List encrypt(Uint8List key, VarDictionary kdfParameters) { + Future encrypt(Uint8List key, VarDictionary kdfParameters) async { final uuid = kdfParameters.get(ValueType.typeBytes, '\$UUID'); if (uuid == null) { throw KdbxCorruptedFileException('No Kdf UUID'); @@ -87,7 +87,7 @@ class KeyEncrypterKdf { switch (kdfUuids[kdfUuid]) { case KdfType.Argon2: _logger.fine('Must be using argon2'); - return encryptArgon2(key, kdfParameters); + return await encryptArgon2(key, kdfParameters); break; case KdfType.Aes: _logger.fine('Must be using aes'); @@ -97,8 +97,9 @@ class KeyEncrypterKdf { 'unsupported KDF Type UUID ${ByteUtils.toHexList(uuid)}.'); } - Uint8List encryptArgon2(Uint8List key, VarDictionary kdfParameters) { - return argon2.argon2( + Future encryptArgon2( + Uint8List key, VarDictionary kdfParameters) async { + return await argon2.argon2Async(Argon2Arguments( key, KdfField.salt.read(kdfParameters), // 65536, //KdfField.memory.read(kdfParameters), @@ -108,7 +109,7 @@ class KeyEncrypterKdf { KdfField.parallelism.read(kdfParameters), 0, KdfField.version.read(kdfParameters), - ); + )); } Uint8List encryptAes(Uint8List key, VarDictionary kdfParameters) { diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 0e926e2..a5c70f9 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -149,7 +149,7 @@ class KdbxFile { Stream> get dirtyObjectsChanged => _dirtyObjectsChanged.stream; - Uint8List save() { + Future save() async { final output = BytesBuilder(); final writer = WriterHelper(output); header.generateSalts(); @@ -167,7 +167,7 @@ class KdbxFile { final headerBytes = writer.output.toBytes(); writer.writeBytes(headerHash); final gen = kdbxFormat._createProtectedSaltGenerator(header); - final keys = kdbxFormat._computeKeysV4(header, credentials); + final keys = await kdbxFormat._computeKeysV4(header, credentials); final headerHmac = kdbxFormat._getHeaderHmac(headerBytes, keys.hmacKey); writer.writeBytes(headerHmac.bytes as Uint8List); body.writeV4(writer, this, gen, keys); @@ -365,13 +365,13 @@ class KdbxFormat { ); } - KdbxFile read(Uint8List input, Credentials credentials) { + Future read(Uint8List input, Credentials credentials) async { final reader = ReaderHelper(input); final header = KdbxHeader.read(reader); if (header.versionMajor == 3) { return _loadV3(header, reader, credentials); } else if (header.versionMajor == 4) { - return _loadV4(header, reader, credentials); + return await _loadV4(header, reader, credentials); } else { _logger.finer('Unsupported version for $header'); throw KdbxUnsupportedException('Unsupported kdbx version ' @@ -399,20 +399,20 @@ class KdbxFormat { } } - KdbxFile _loadV4( - KdbxHeader header, ReaderHelper reader, Credentials credentials) { + Future _loadV4( + KdbxHeader header, ReaderHelper reader, Credentials credentials) async { final headerBytes = reader.byteData.sublist(0, header.endPos); final hash = crypto.sha256.convert(headerBytes).bytes; final actualHash = reader.readBytes(hash.length); if (!ByteUtils.eq(hash, actualHash)) { - _logger.fine( - 'Does not match ${ByteUtils.toHexList(hash)} vs ${ByteUtils.toHexList(actualHash)}'); + _logger.fine('Does not match ${ByteUtils.toHexList(hash)} ' + 'vs ${ByteUtils.toHexList(actualHash)}'); throw KdbxCorruptedFileException('Header hash does not match.'); } // _logger // .finest('KdfParameters: ${header.readKdfParameters.toDebugString()}'); _logger.finest('Header hash matches.'); - final keys = _computeKeysV4(header, credentials); + final keys = await _computeKeysV4(header, credentials); final headerHmac = _getHeaderHmac(reader.byteData.sublist(0, header.endPos), keys.hmacKey); final expectedHmac = reader.readBytes(headerHmac.bytes.length); @@ -435,7 +435,7 @@ class KdbxFormat { final xml = utf8.decode(contentReader.readRemaining()); return KdbxFile(this, credentials, header, _loadXml(header, xml)); } - return null; + throw StateError('Kdbx4 without compression is not yet supported.'); } Uint8List hmacBlockTransformerEncrypt(Uint8List hmacKey, Uint8List data) { @@ -539,7 +539,8 @@ class KdbxFormat { return hmacKeyStuff.convert(src); } - _KeysV4 _computeKeysV4(KdbxHeader header, Credentials credentials) { + Future<_KeysV4> _computeKeysV4( + KdbxHeader header, Credentials credentials) async { final masterSeed = header.fields[HeaderFields.MasterSeed].bytes; final kdfParameters = header.readKdfParameters; if (masterSeed.length != 32) { @@ -547,7 +548,8 @@ class KdbxFormat { } final credentialHash = credentials.getHash(); - final key = KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters); + final key = + await KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters); // final keyWithSeed = Uint8List(65); // keyWithSeed.replaceRange(0, masterSeed.length, masterSeed); diff --git a/test/kdbx4_test.dart b/test/kdbx4_test.dart index 0c93850..f8a3d15 100644 --- a/test/kdbx4_test.dart +++ b/test/kdbx4_test.dart @@ -1,7 +1,5 @@ -import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; -import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'package:kdbx/kdbx.dart'; @@ -60,21 +58,17 @@ void main() { final kdbxFormat = KdbxFormat(Argon2Test()); group('Reading', () { test('bubb', () async { - final key = utf8.encode('asdf') as Uint8List; - final salt = Uint8List(8); -// final result = Argon2Test().argon2(key, salt, 1 << 16, 5, 16, 1, 0x13, 1); -// _logger.fine('hashing: $result'); final data = await File('test/keepassxcpasswords.kdbx').readAsBytes(); - final file = - kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); + final file = await kdbxFormat.read( + data, Credentials(ProtectedValue.fromString('asdf'))); final firstEntry = file.body.rootGroup.entries.first; 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 file = await kdbxFormat.read( + data, Credentials(ProtectedValue.fromString('asdf'))); final firstEntry = file.body.rootGroup.entries.first; final pwd = firstEntry.getString(KdbxKey('Password')).getText(); expect(pwd, 'def'); @@ -84,25 +78,25 @@ void main() { await File('test/keyfile/BinaryKeyFilePasswords.kdbx').readAsBytes(); final keyFile = await File('test/keyfile/binarykeyfile.key').readAsBytes(); - final file = kdbxFormat.read(data, + final file = await kdbxFormat.read(data, Credentials.composite(ProtectedValue.fromString('asdf'), keyFile)); expect(file.body.rootGroup.entries, hasLength(1)); }); test('Reading chacha20', () async { final data = await File('test/chacha20.kdbx').readAsBytes(); - final file = - kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); + final file = await kdbxFormat.read( + data, Credentials(ProtectedValue.fromString('asdf'))); expect(file.body.rootGroup.entries, hasLength(1)); }); test('Reading aes-kdf', () async { final data = await File('test/aeskdf.kdbx').readAsBytes(); - final file = - kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); + final file = await kdbxFormat.read( + data, Credentials(ProtectedValue.fromString('asdf'))); expect(file.body.rootGroup.entries, hasLength(1)); }); }); group('Writing', () { - test('Create and save', () { + test('Create and save', () async { final credentials = Credentials(ProtectedValue.fromString('asdf')); final kdbx = kdbxFormat.create( credentials, @@ -112,27 +106,28 @@ void main() { final rootGroup = kdbx.body.rootGroup; _createEntry(kdbx, rootGroup, 'user1', 'LoremIpsum'); _createEntry(kdbx, rootGroup, 'user2', 'Second Password'); - final saved = kdbx.save(); + final saved = await kdbx.save(); - final loadedKdbx = kdbxFormat.read( + final loadedKdbx = await kdbxFormat.read( saved, Credentials(ProtectedValue.fromString('asdf'))); _logger.fine('Successfully loaded kdbx $loadedKdbx'); 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'))); + final file = await kdbxFormat.read( + data, Credentials(ProtectedValue.fromString('asdf'))); + _logger.fine('successfully read ${file.body.rootGroup.name}'); }); test('write chacha20', () async { final data = await File('test/chacha20.kdbx').readAsBytes(); - final file = - kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); + final file = await kdbxFormat.read( + data, Credentials(ProtectedValue.fromString('asdf'))); expect(file.body.rootGroup.entries, hasLength(1)); _createEntry(file, file.body.rootGroup, 'user1', 'LoremIpsum'); // and try to write it. - final output = file.save(); + final output = await file.save(); expect(output, isNotNull); File('test_output_chacha20.kdbx').writeAsBytesSync(output); }); diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart index cf44a2a..02ee702 100644 --- a/test/kdbx_test.dart +++ b/test/kdbx_test.dart @@ -1,4 +1,3 @@ -import 'dart:ffi'; import 'dart:io'; import 'dart:typed_data'; @@ -27,7 +26,8 @@ void main() { test('First Test', () async { final data = await File('test/FooBar.kdbx').readAsBytes(); - kdbxForamt.read(data, Credentials(ProtectedValue.fromString('FooBar'))); + await kdbxForamt.read( + data, Credentials(ProtectedValue.fromString('FooBar'))); }); }); @@ -38,7 +38,7 @@ void main() { final cred = Credentials.composite( ProtectedValue.fromString('asdf'), keyFileBytes); final data = await File('test/password-and-keyfile.kdbx').readAsBytes(); - final file = kdbxForamt.read(data, cred); + final file = await kdbxForamt.read(data, cred); expect(file.body.rootGroup.entries, hasLength(2)); }); test('Read with PW and hex keyfile', () async { @@ -47,7 +47,7 @@ void main() { final cred = Credentials.composite( ProtectedValue.fromString('testing99'), keyFileBytes); final data = await File('test/keyfile/newdatabase2.kdbx').readAsBytes(); - final file = kdbxForamt.read(data, cred); + final file = await kdbxForamt.read(data, cred); expect(file.body.rootGroup.entries, hasLength(3)); }); }); @@ -79,9 +79,9 @@ void main() { }); group('Integration', () { - test('Simple save and load', () { + test('Simple save and load', () async { final credentials = Credentials(ProtectedValue.fromString('FooBar')); - final Uint8List saved = (() { + final Uint8List saved = await (() async { final kdbx = kdbxForamt.create(credentials, 'CreateTest'); final rootGroup = kdbx.body.rootGroup; final entry = KdbxEntry.create(kdbx, rootGroup); @@ -93,7 +93,7 @@ void main() { // print(ByteUtils.toHexList(saved)); - final kdbx = kdbxForamt.read(saved, credentials); + final kdbx = await kdbxForamt.read(saved, credentials); expect( kdbx.body.rootGroup.entries.first .getString(KdbxKey('Password'))