diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d857f..37e182e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.2.0 + +- If argon2 ffi implementation is not available, fallback to pointycastle (dart-only) + implementation. + ## 2.1.1 - Throw KdbxInvalidFileStructure for invalid files. diff --git a/lib/src/internal/pointycastle_argon2.dart b/lib/src/internal/pointycastle_argon2.dart new file mode 100644 index 0000000..7e77c9d --- /dev/null +++ b/lib/src/internal/pointycastle_argon2.dart @@ -0,0 +1,38 @@ +import 'dart:typed_data'; + +import 'package:argon2_ffi_base/argon2_ffi_base.dart'; +import 'package:pointycastle/export.dart' as pc; +import 'package:pointycastle/pointycastle.dart' as pc; + +/// Dart-only implementation using pointycastle's Argon KDF. +class PointyCastleArgon2 extends Argon2 { + const PointyCastleArgon2(); + + @override + bool get isFfi => false; + + @override + bool get isImplemented => true; + + pc.KeyDerivator argon2Kdf() => pc.Argon2BytesGenerator(); + + @override + Uint8List argon2(Argon2Arguments args) { + final kdf = argon2Kdf(); + kdf.init(pc.Argon2Parameters( + args.type, + args.salt, + desiredKeyLength: args.length, + iterations: args.iterations, + memory: args.memory, + lanes: args.parallelism, + version: args.version, + )); + return kdf.process(args.key); + } + + @override + Future argon2Async(Argon2Arguments args) { + return Future.value(argon2(args)); + } +} diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 773bffd..881e22b 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -14,6 +14,7 @@ import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/internal/consts.dart'; import 'package:kdbx/src/internal/crypto_utils.dart'; import 'package:kdbx/src/internal/extension_utils.dart'; +import 'package:kdbx/src/internal/pointycastle_argon2.dart'; import 'package:kdbx/src/kdbx_deleted_object.dart'; import 'package:kdbx/src/kdbx_entry.dart'; import 'package:kdbx/src/kdbx_group.dart'; @@ -524,9 +525,13 @@ class _KeysV4 { } class KdbxFormat { - KdbxFormat([this.argon2]) : assert(kdbxKeyCommonAssertConsistency()); + KdbxFormat([Argon2? argon2]) + : assert(kdbxKeyCommonAssertConsistency()), + argon2 = argon2 == null || !argon2.isImplemented + ? const PointyCastleArgon2() + : argon2; - final Argon2? argon2; + final Argon2 argon2; static bool dartWebWorkaround = false; /// Creates a new, empty [KdbxFile] with default settings. @@ -537,7 +542,7 @@ class KdbxFormat { String? generator, KdbxHeader? header, }) { - header ??= argon2 == null ? KdbxHeader.createV3() : KdbxHeader.createV4(); + header ??= KdbxHeader.createV4(); final ctx = KdbxReadWriteContext(binaries: [], header: header); final meta = KdbxMeta.create( databaseName: name, @@ -789,7 +794,7 @@ class KdbxFormat { final credentialHash = credentials.getHash(); final key = - await KeyEncrypterKdf(argon2!).encrypt(credentialHash, kdfParameters); + await KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters); // final keyWithSeed = Uint8List(65); // keyWithSeed.replaceRange(0, masterSeed.length, masterSeed); diff --git a/pubspec.yaml b/pubspec.yaml index db0b3f4..d39621a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: kdbx description: KeepassX format implementation in pure dart. (kdbx 3.x and 4.x support). -version: 2.1.0 +version: 2.2.0 homepage: https://github.com/authpass/kdbx.dart environment: @@ -27,7 +27,7 @@ dependencies: # required for bin/ args: '>1.5.0 <3.0.0' logging_appenders: '>=0.1.0 <2.0.0' - argon2_ffi_base: ^1.0.0+2 + argon2_ffi_base: ^1.1.0+1 dev_dependencies: pedantic: '>=1.7.0 <2.0.0' diff --git a/test/kdbx4_test.dart b/test/kdbx4_test.dart index f6a1782..a2216f2 100644 --- a/test/kdbx4_test.dart +++ b/test/kdbx4_test.dart @@ -15,6 +15,9 @@ final _logger = Logger('kdbx4_test'); void main() { final testUtil = TestUtil(); final kdbxFormat = testUtil.kdbxFormat; + if (!kdbxFormat.argon2.isFfi) { + throw StateError('Expected ffi!'); + } group('Reading', () { test('bubb', () async { final data = await File('test/keepassxcpasswords.kdbx').readAsBytes(); diff --git a/test/kdbx4_test_pointycastle.dart b/test/kdbx4_test_pointycastle.dart new file mode 100644 index 0000000..e84804a --- /dev/null +++ b/test/kdbx4_test_pointycastle.dart @@ -0,0 +1,59 @@ +import 'dart:io'; + +import 'package:kdbx/kdbx.dart'; +import 'package:kdbx/src/kdbx_header.dart'; + +import 'package:logging/logging.dart'; +import 'package:test/test.dart'; + +import 'internal/test_utils.dart'; + +final _logger = Logger('kdbx4_test_pointycastle'); + +void main() { + // ignore: unused_local_variable + final testUtil = TestUtil(); + final kdbxFormat = KdbxFormat(); + if (kdbxFormat.argon2.isFfi) { + throw StateError('Expected non-ffi implementation.'); + } + _logger.fine('argon2 implementation: ${kdbxFormat.argon2}'); + group('Reading pointycastle argon2', () { + test('pc: Reading kdbx4_keeweb', () async { + final data = await File('test/kdbx4_keeweb.kdbx').readAsBytes(); + final file = await kdbxFormat.read( + data, Credentials(ProtectedValue.fromString('asdf'))); + final firstEntry = file.body.rootGroup.entries.first; + final pwd = firstEntry.getString(KdbxKeyCommon.PASSWORD)!.getText(); + expect(pwd, 'def'); + }); + }); + group('Writing pointycastle argon2', () { + test('Create and save', () async { + final credentials = Credentials(ProtectedValue.fromString('asdf')); + final kdbx = kdbxFormat.create( + credentials, + 'Test Keystore', + header: KdbxHeader.createV4(), + ); + final rootGroup = kdbx.body.rootGroup; + _createEntry(kdbx, rootGroup, 'user1', 'LoremIpsum'); + _createEntry(kdbx, rootGroup, 'user2', 'Second Password'); + final saved = await kdbx.save(); + + final loadedKdbx = await kdbxFormat.read( + saved, Credentials(ProtectedValue.fromString('asdf'))); + _logger.fine('Successfully loaded kdbx $loadedKdbx'); + File('test_v4x.kdbx').writeAsBytesSync(saved); + }); + }); +} + +KdbxEntry _createEntry( + KdbxFile file, KdbxGroup group, String username, String password) { + final entry = KdbxEntry.create(file, group); + group.addEntry(entry); + entry.setString(KdbxKeyCommon.USER_NAME, PlainValue(username)); + entry.setString(KdbxKeyCommon.PASSWORD, ProtectedValue.fromString(password)); + return entry; +}