From 3882326613ddc5ca4d6fdfd75025b5e3e1278f86 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Tue, 15 Oct 2024 15:20:17 +0200 Subject: [PATCH 01/19] chore: wip --- .../lib/src/crypto/crypto_service.dart | 15 ++++ .../crypto/cryptography_crypto_service.dart | 68 +++++++++++++++++++ .../storage/vault/secure_storage_vault.dart | 27 ++++++-- .../catalyst_voices_services/pubspec.yaml | 1 + 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/cryptography_crypto_service.dart diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart new file mode 100644 index 00000000000..9509de9e4ee --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart @@ -0,0 +1,15 @@ +import 'package:flutter/foundation.dart'; + +abstract interface class CryptoService { + Future deriveKey(Uint8List data); + + Future decrypt( + Uint8List data, { + required Uint8List key, + }); + + Future encrypt( + Uint8List data, { + required Uint8List key, + }); +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/cryptography_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/cryptography_crypto_service.dart new file mode 100644 index 00000000000..eac166a9738 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/cryptography_crypto_service.dart @@ -0,0 +1,68 @@ +import 'dart:math'; + +import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:flutter/foundation.dart'; + +final class CryptographyCryptoService implements CryptoService { + /// Salt length for Argon2 key derivation + static const int _saltLength = 16; + + final Random _random; + + CryptographyCryptoService({ + Random? secureRandom, + }) : _random = secureRandom ?? Random.secure(); + + @override + Future deriveKey(Uint8List data) async { + final algorithm = Argon2id( + parallelism: 4, + memory: 10000, // 10 000 x 1kB block = 10 MB + iterations: 3, + hashLength: 32, + ); + + final salt = _generateRandomList(length: _saltLength); + final secretKey = await algorithm.deriveKey( + secretKey: SecretKey(data), + nonce: salt, + ); + + final keyBytes = await secretKey.extractBytes().then(Uint8List.fromList); + + // Combine salt and hashed password for storage + return Uint8List.fromList([...salt, ...keyBytes]); + } + + @override + Future decrypt( + Uint8List data, { + required Uint8List key, + }) { + // TODO: implement decrypt + throw UnimplementedError(); + } + + @override + Future encrypt( + Uint8List data, { + required Uint8List key, + }) { + // TODO: implement encrypt + throw UnimplementedError(); + } + + /// Builds list with [length] and random bytes in it. + Uint8List _generateRandomList({ + required int length, + }) { + final list = Uint8List(length); + + for (var i = 0; i < length; i++) { + list[i] = _random.nextInt(16); + } + + return list; + } +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart index d33fa8f1b55..1b01fe1d6cb 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; +import 'package:catalyst_voices_services/src/crypto/cryptography_crypto_service.dart'; import 'package:catalyst_voices_services/src/storage/storage_string_mixin.dart'; import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; import 'package:catalyst_voices_services/src/storage/vault/lock_factor_codec.dart'; @@ -10,18 +12,22 @@ const _keyPrefix = 'SecureStorageVault'; const _lockKey = 'LockFactorKey'; const _unlockKey = 'UnlockFactorKey'; -// TODO(damian-molinski): Maybe we'll need to encrypt data with LockFactor /// Implementation of [Vault] that uses [FlutterSecureStorage] as /// facade for read/write operations. base class SecureStorageVault with StorageAsStringMixin implements Vault { final FlutterSecureStorage _secureStorage; final LockFactorCodec _lockCodec; + final CryptoService _cryptoService; - const SecureStorageVault({ + bool _isUnlocked = false; + + SecureStorageVault({ FlutterSecureStorage secureStorage = const FlutterSecureStorage(), LockFactorCodec lockCodec = const DefaultLockFactorCodec(), + CryptoService cryptoService = const CryptographyCryptoService(), }) : _secureStorage = secureStorage, - _lockCodec = lockCodec; + _lockCodec = lockCodec, + _cryptoService = cryptoService; /// If storage does not have [LockFactor] this getter will /// return [VoidLockFactor] as fallback. @@ -32,16 +38,20 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { Future get _unlock => _readLock(_unlockKey); @override + // TODO(damian-molinski): have to hash unlock and compare it with lock. Future get isUnlocked async { - final lock = await _lock; - final unlock = await _unlock; + // final lock = await _lock; + // final unlock = await _unlock; + // + // return unlock.unlocks(lock); - return unlock.unlocks(lock); + return _isUnlocked; } @override Future lock() => _writeLock(null, key: _unlockKey); + // TODO(damian-molinski): do not store unlock @override Future unlock(LockFactor unlock) async { await _writeLock(unlock, key: _unlockKey); @@ -56,7 +66,8 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { @override Future contains({required String key}) async { - return await _guardedRead(key: key, requireUnlocked: false) != null; + final effectiveKey = _buildVaultKey(key); + return _secureStorage.containsKey(key: effectiveKey); } @override @@ -81,6 +92,7 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { } Future _readLock(String key) async { + // TODO(damian-molinski): this will be encrypted. Can not decode it here. final value = await _guardedRead(key: key, requireUnlocked: false); return value != null ? _lockCodec.decode(value) : const VoidLockFactor(); @@ -90,6 +102,7 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { LockFactor? lock, { required String key, }) { + // TODO(damian-molinski): encrypt lock final encodedLock = lock != null ? _lockCodec.encode(lock) : null; return _guardedWrite( diff --git a/catalyst_voices/packages/catalyst_voices_services/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_services/pubspec.yaml index 08f8c5c9b3d..f44a07f10c3 100644 --- a/catalyst_voices/packages/catalyst_voices_services/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_services/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: path: ../catalyst_voices_repositories chopper: ^7.2.0 convert: ^3.1.1 + cryptography: ^2.7.0 ed25519_hd_key: ^2.3.0 flutter: sdk: flutter From e5a9e84c8c9a264208a25e8c54f6691492cd701a Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 12:36:24 +0200 Subject: [PATCH 02/19] feat: first iteration of AesCryptoService --- .../lib/src/crypto/aes_crypto_service.dart | 129 ++++++++++++++++++ .../lib/src/crypto/crypto_service.dart | 10 +- .../crypto/cryptography_crypto_service.dart | 68 --------- .../src/storage/vault/lock_factor_codec.dart | 41 ------ .../storage/vault/lock_factor_codec_test.dart | 20 --- 5 files changed, 138 insertions(+), 130 deletions(-) create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart delete mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/cryptography_crypto_service.dart delete mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor_codec.dart delete mode 100644 catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_codec_test.dart diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart new file mode 100644 index 00000000000..04e04f1ca4f --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart @@ -0,0 +1,129 @@ +import 'dart:math'; + +import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:flutter/foundation.dart'; + +final class AesCryptoService implements CryptoService { + /// Salt length for Argon2 key derivation. + static const int _saltLength = 16; + + /// Salt length for AesGcm data encryption. + static const int _viLength = 12; + + /// Derived key hash length. + static const int _keyLength = 16; + + final Random _random; + + AesCryptoService({ + Random? random, + }) : _random = random ?? Random.secure(); + + @override + Future deriveKey( + Uint8List seed, { + Uint8List? salt, + }) async { + final algorithm = Argon2id( + parallelism: 2, + memory: 10000, // 10 000 x 1kB block = 10 MB + iterations: 1, + hashLength: _keyLength, + ); + + salt ??= _generateRandomList(length: _saltLength); + + final secretKey = await algorithm.deriveKey( + secretKey: SecretKey(seed), + nonce: salt, + ); + + final keyBytes = await secretKey.extractBytes().then(Uint8List.fromList); + + secretKey.destroy(); + + // Combine salt and hashed password for storage + return Uint8List.fromList([...salt, ...keyBytes]); + } + + @override + Future verifyKey( + Uint8List seed, { + required Uint8List key, + }) async { + final salt = key.sublist(0, _saltLength); + if (salt.length < _saltLength) { + return false; + } + + final derivedKey = await deriveKey(seed, salt: salt); + return listEquals(derivedKey, key); + } + + @override + Future decrypt( + Uint8List data, { + required Uint8List key, + }) async { + final algorithm = AesGcm.with256bits(nonceLength: _viLength); + + final secretKey = SecretKey(key); + + try { + final secretBox = SecretBox.fromConcatenation( + data, + nonceLength: _viLength, + macLength: algorithm.macAlgorithm.macLength, + ); + + await secretBox.checkMac( + macAlgorithm: algorithm.macAlgorithm, + secretKey: secretKey, + aad: [], + ); + + return algorithm + .decrypt(secretBox, secretKey: secretKey) + .then(Uint8List.fromList); + } finally { + secretKey.destroy(); + } + } + + @override + Future encrypt( + Uint8List data, { + required Uint8List key, + }) async { + final algorithm = AesGcm.with256bits(nonceLength: _viLength); + final secretKey = SecretKey(key); + + try { + final secretBox = await algorithm.encrypt( + data, + secretKey: secretKey, + nonce: null, + aad: [], + possibleBuffer: null, + ); + + return secretBox.concatenation(); + } finally { + secretKey.destroy(); + } + } + + /// Builds list with [length] and random bytes in it. + Uint8List _generateRandomList({ + required int length, + }) { + final list = Uint8List(length); + + for (var i = 0; i < length; i++) { + list[i] = _random.nextInt(16); + } + + return list; + } +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart index 9509de9e4ee..8bbb7cc56a8 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart @@ -1,7 +1,15 @@ import 'package:flutter/foundation.dart'; abstract interface class CryptoService { - Future deriveKey(Uint8List data); + Future deriveKey( + Uint8List seed, { + Uint8List? salt, + }); + + Future verifyKey( + Uint8List seed, { + required Uint8List key, + }); Future decrypt( Uint8List data, { diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/cryptography_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/cryptography_crypto_service.dart deleted file mode 100644 index eac166a9738..00000000000 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/cryptography_crypto_service.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'dart:math'; - -import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; -import 'package:cryptography/cryptography.dart'; -import 'package:flutter/foundation.dart'; - -final class CryptographyCryptoService implements CryptoService { - /// Salt length for Argon2 key derivation - static const int _saltLength = 16; - - final Random _random; - - CryptographyCryptoService({ - Random? secureRandom, - }) : _random = secureRandom ?? Random.secure(); - - @override - Future deriveKey(Uint8List data) async { - final algorithm = Argon2id( - parallelism: 4, - memory: 10000, // 10 000 x 1kB block = 10 MB - iterations: 3, - hashLength: 32, - ); - - final salt = _generateRandomList(length: _saltLength); - final secretKey = await algorithm.deriveKey( - secretKey: SecretKey(data), - nonce: salt, - ); - - final keyBytes = await secretKey.extractBytes().then(Uint8List.fromList); - - // Combine salt and hashed password for storage - return Uint8List.fromList([...salt, ...keyBytes]); - } - - @override - Future decrypt( - Uint8List data, { - required Uint8List key, - }) { - // TODO: implement decrypt - throw UnimplementedError(); - } - - @override - Future encrypt( - Uint8List data, { - required Uint8List key, - }) { - // TODO: implement encrypt - throw UnimplementedError(); - } - - /// Builds list with [length] and random bytes in it. - Uint8List _generateRandomList({ - required int length, - }) { - final list = Uint8List(length); - - for (var i = 0; i < length; i++) { - list[i] = _random.nextInt(16); - } - - return list; - } -} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor_codec.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor_codec.dart deleted file mode 100644 index c6754ed75e0..00000000000 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor_codec.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:convert'; - -import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; - -abstract class LockFactorCodec extends Codec { - const LockFactorCodec(); -} - -/// Uses [LockFactor.toJson] and [LockFactor.fromJson] to serialize to -/// [String] using [json]. -class DefaultLockFactorCodec extends LockFactorCodec { - const DefaultLockFactorCodec(); - - @override - Converter get decoder => const _LockFactorDecoder(); - - @override - Converter get encoder => const _LockFactorEncoder(); -} - -class _LockFactorDecoder extends Converter { - const _LockFactorDecoder(); - - @override - LockFactor convert(String input) { - final json = jsonDecode(input) as Map; - - return LockFactor.fromJson(json); - } -} - -class _LockFactorEncoder extends Converter { - const _LockFactorEncoder(); - - @override - String convert(LockFactor input) { - final json = input.toJson(); - - return jsonEncode(json); - } -} diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_codec_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_codec_test.dart deleted file mode 100644 index d4857ba8eba..00000000000 --- a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_codec_test.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; -import 'package:catalyst_voices_services/src/storage/vault/lock_factor_codec.dart'; -import 'package:test/expect.dart'; -import 'package:test/scaffolding.dart'; - -void main() { - test('encoding and decoding results in same lock factor', () { - // Given - const lock = PasswordLockFactor('pass1234'); - const LockFactorCodec codec = DefaultLockFactorCodec(); - - // When - final encoded = codec.encoder.convert(lock); - final decoded = codec.decoder.convert(encoded); - - // Then - expect(decoded, isA()); - expect(decoded.unlocks(lock), isTrue); - }); -} From 85cd4e59aa1b90804cee6f71c4db21cd96fa30c8 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 12:37:11 +0200 Subject: [PATCH 03/19] feat: refactor SecureStorageVault to use CryptoService --- .../lib/src/errors/errors.dart | 1 + .../lib/src/errors/vault_exception.dart | 27 ++++ .../lib/src/catalyst_voices_services.dart | 1 - .../lib/src/storage/vault/lock_factor.dart | 69 +------- .../storage/vault/secure_storage_vault.dart | 147 ++++++++++-------- .../src/storage/vault/lock_factor_test.dart | 117 ++------------ 6 files changed, 128 insertions(+), 234 deletions(-) create mode 100644 catalyst_voices/packages/catalyst_voices_models/lib/src/errors/vault_exception.dart diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/errors.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/errors.dart index f4604d99cbb..08cdc91100a 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/errors.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/errors.dart @@ -1,2 +1,3 @@ export 'network_error.dart'; export 'secure_storage_error.dart'; +export 'vault_exception.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/vault_exception.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/vault_exception.dart new file mode 100644 index 00000000000..ffd17bbaeb5 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/vault_exception.dart @@ -0,0 +1,27 @@ +import 'package:equatable/equatable.dart'; + +sealed class VaultException extends Equatable implements Exception { + const VaultException(); + + @override + List get props => []; +} + +final class LockNotFoundException extends VaultException { + final String? message; + + const LockNotFoundException([this.message]); + + @override + String toString() { + if (message != null) return 'LockNotFoundException: $message'; + return 'LockNotFoundException'; + } +} + +final class VaultLockedException extends VaultException { + const VaultLockedException(); + + @override + String toString() => 'VaultLockedException'; +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/catalyst_voices_services.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/catalyst_voices_services.dart index a08df2f9185..8ff75bee4fb 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/catalyst_voices_services.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/catalyst_voices_services.dart @@ -7,6 +7,5 @@ export 'storage/dummy_auth_storage.dart'; export 'storage/secure_storage.dart'; export 'storage/storage.dart'; export 'storage/vault/lock_factor.dart'; -export 'storage/vault/lock_factor_codec.dart' show LockFactorCodec; export 'storage/vault/secure_storage_vault.dart'; export 'storage/vault/vault.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor.dart index c3ec5cd5225..3a47ff91673 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor.dart @@ -1,6 +1,7 @@ -import 'package:catalyst_voices_services/src/storage/vault/vault.dart'; +import 'dart:convert'; -enum _LockFactorType { voidFactor, password } +import 'package:catalyst_voices_services/src/storage/vault/vault.dart'; +import 'package:flutter/foundation.dart'; // Note. // In future we may add MultiLockFactor for bio and password unlock factors @@ -8,50 +9,8 @@ enum _LockFactorType { voidFactor, password } /// Abstract representation of different factors that can lock [Vault] with. /// /// Most common is [PasswordLockFactor] which can be use as standalone factor. -/// -/// This class is serializable to/from json. -sealed class LockFactor { - /// Use [LockFactor.toJson] as parameter for this factory. - factory LockFactor.fromJson(Map json) { - final typeName = json['type']; - final type = _LockFactorType.values.asNameMap()[typeName]; - - return switch (type) { - _LockFactorType.voidFactor => const VoidLockFactor(), - _LockFactorType.password => PasswordLockFactor.fromJson(json), - null => throw ArgumentError('Unknown type name($typeName)', 'json'), - }; - } - - /// Returns true when this [LockFactor] can be used to unlock - /// other [LockFactor]. - bool unlocks(LockFactor factor); - - /// Returns json representation on this [LockFactor]. - /// - /// Should be used with [LockFactor.fromJson]. - Map toJson(); -} - -/// Can not be used to unlock anything. Useful as default value for [LockFactor] -/// variables. -/// -/// [unlocks] always returns false. -final class VoidLockFactor implements LockFactor { - const VoidLockFactor(); - - @override - bool unlocks(LockFactor factor) => false; - - @override - Map toJson() { - return { - 'type': _LockFactorType.voidFactor.name, - }; - } - - @override - String toString() => 'VoidLockFactor'; +abstract interface class LockFactor { + Uint8List get seed; } /// Password matching [LockFactor]. @@ -63,24 +22,8 @@ final class PasswordLockFactor implements LockFactor { const PasswordLockFactor(this._data); - factory PasswordLockFactor.fromJson(Map json) { - return PasswordLockFactor( - json['data'] as String, - ); - } - - @override - bool unlocks(LockFactor factor) { - return factor is PasswordLockFactor && _data == factor._data; - } - @override - Map toJson() { - return { - 'type': _LockFactorType.password.name, - 'data': _data, - }; - } + Uint8List get seed => utf8.encode(_data); @override String toString() => 'PasswordLockFactor(${_data.hashCode})'; diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart index 1b01fe1d6cb..9936f30cf68 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart @@ -1,67 +1,87 @@ import 'dart:async'; +import 'dart:convert'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/src/crypto/aes_crypto_service.dart'; import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; -import 'package:catalyst_voices_services/src/crypto/cryptography_crypto_service.dart'; import 'package:catalyst_voices_services/src/storage/storage_string_mixin.dart'; import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; -import 'package:catalyst_voices_services/src/storage/vault/lock_factor_codec.dart'; import 'package:catalyst_voices_services/src/storage/vault/vault.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; const _keyPrefix = 'SecureStorageVault'; -const _lockKey = 'LockFactorKey'; -const _unlockKey = 'UnlockFactorKey'; +const _lockKey = 'LockKey'; /// Implementation of [Vault] that uses [FlutterSecureStorage] as /// facade for read/write operations. base class SecureStorageVault with StorageAsStringMixin implements Vault { final FlutterSecureStorage _secureStorage; - final LockFactorCodec _lockCodec; + final CryptoService _cryptoService; bool _isUnlocked = false; SecureStorageVault({ FlutterSecureStorage secureStorage = const FlutterSecureStorage(), - LockFactorCodec lockCodec = const DefaultLockFactorCodec(), - CryptoService cryptoService = const CryptographyCryptoService(), + CryptoService? cryptoService, }) : _secureStorage = secureStorage, - _lockCodec = lockCodec, - _cryptoService = cryptoService; + _cryptoService = cryptoService ?? AesCryptoService(); - /// If storage does not have [LockFactor] this getter will - /// return [VoidLockFactor] as fallback. - Future get _lock => _readLock(_lockKey); + Future get _hasLock { + final effectiveKey = _buildVaultKey(_lockKey); + return _secureStorage.containsKey(key: effectiveKey); + } - /// If storage does not have [LockFactor] this getter will - /// return [VoidLockFactor] as fallback. - Future get _unlock => _readLock(_unlockKey); + Future get _lock async { + final effectiveKey = _buildVaultKey(_lockKey); + final encodedLock = await _secureStorage.read(key: effectiveKey); + return encodedLock != null ? base64.decode(encodedLock) : null; + } - @override - // TODO(damian-molinski): have to hash unlock and compare it with lock. - Future get isUnlocked async { - // final lock = await _lock; - // final unlock = await _unlock; - // - // return unlock.unlocks(lock); - - return _isUnlocked; + Future get _requireLock async { + final lock = await _lock; + if (lock == null) { + throw const LockNotFoundException(); + } + return lock; } @override - Future lock() => _writeLock(null, key: _unlockKey); + Future get isUnlocked => SynchronousFuture(_isUnlocked); + + @override + Future lock() async { + _isUnlocked = false; + } - // TODO(damian-molinski): do not store unlock @override Future unlock(LockFactor unlock) async { - await _writeLock(unlock, key: _unlockKey); + if (!await _hasLock) { + throw const LockNotFoundException('Set lock before unlocking Vault'); + } + + final seed = unlock.seed; + final lock = await _requireLock; + + _isUnlocked = await _cryptoService.verifyKey(seed, key: lock); return isUnlocked; } @override - Future setLock(LockFactor lock) { - return _writeLock(lock, key: _lockKey); + Future setLock(LockFactor lock) async { + if (await _hasLock && !await isUnlocked) { + throw const VaultLockedException(); + } + + final seed = lock.seed; + final key = await _cryptoService.deriveKey(seed); + final encodedKey = base64.encode(key); + + final effectiveLockKey = _buildVaultKey(_lockKey); + + await _secureStorage.write(key: effectiveLockKey, value: encodedKey); } @override @@ -91,42 +111,25 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { } } - Future _readLock(String key) async { - // TODO(damian-molinski): this will be encrypted. Can not decode it here. - final value = await _guardedRead(key: key, requireUnlocked: false); - - return value != null ? _lockCodec.decode(value) : const VoidLockFactor(); - } - - Future _writeLock( - LockFactor? lock, { - required String key, - }) { - // TODO(damian-molinski): encrypt lock - final encodedLock = lock != null ? _lockCodec.encode(lock) : null; - - return _guardedWrite( - encodedLock, - key: key, - requireUnlocked: false, - ); - } - /// Allows operation only when [isUnlocked] it true, otherwise returns null. /// /// Returns value assigned to [key]. May return null if not found for [key]. Future _guardedRead({ required String key, - bool requireUnlocked = true, }) async { - final hasAccess = !requireUnlocked || await isUnlocked; - if (!hasAccess) { - return null; + if (!await isUnlocked) { + throw const VaultLockedException(); } final effectiveKey = _buildVaultKey(key); + final encryptedData = await _secureStorage.read(key: effectiveKey); + if (encryptedData == null) { + return null; + } - return _secureStorage.read(key: effectiveKey); + final lock = await _requireLock; + + return _decrypt(encryptedData, key: lock); } /// Allows operation only when [isUnlocked] it true, otherwise non op. @@ -136,20 +139,40 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { Future _guardedWrite( String? value, { required String key, - bool requireUnlocked = true, }) async { - final hasAccess = !requireUnlocked || await isUnlocked; - if (!hasAccess) { - return; + if (!await isUnlocked) { + throw const VaultLockedException(); } final effectiveKey = _buildVaultKey(key); - if (value != null) { - await _secureStorage.write(key: effectiveKey, value: value); - } else { + if (value == null) { await _secureStorage.delete(key: effectiveKey); + return; } + + final lock = await _requireLock; + final encryptedData = await _encrypt(value, key: lock); + + await _secureStorage.write(key: effectiveKey, value: encryptedData); + } + + Future _encrypt( + String data, { + required Uint8List key, + }) async { + final decodedData = base64.decode(data); + final encryptedData = await _cryptoService.encrypt(decodedData, key: key); + return base64.encode(encryptedData); + } + + Future _decrypt( + String data, { + required Uint8List key, + }) async { + final decodedData = base64.decode(data); + final decryptedData = await _cryptoService.decrypt(decodedData, key: key); + return base64.encode(decryptedData); } String _buildVaultKey(String key) { diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_test.dart index 3faf9efa23b..b1e688c55a2 100644 --- a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_test.dart +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_test.dart @@ -1,120 +1,21 @@ +import 'dart:convert'; + import 'package:catalyst_voices_services/catalyst_voices_services.dart'; import 'package:test/test.dart'; void main() { - group('LockFactor', () { - test('void lock serialization does work', () { - // Given - const lock = VoidLockFactor(); - - // When - final json = lock.toJson(); - final deserializedFactor = LockFactor.fromJson(json); - - // Then - expect(deserializedFactor, isA()); - }); - - test('description', () { - // Given - const lock = PasswordLockFactor('pass1234'); - - // When - final json = lock.toJson(); - final deserializedFactor = LockFactor.fromJson(json); - - // Then - expect(deserializedFactor, isA()); - expect(deserializedFactor.unlocks(lock), isTrue); - }); - }); - - group('VoidLockFactor', () { - test('does not unlocks any other lock', () { - // Given - const lock = VoidLockFactor(); - const locks = [ - VoidLockFactor(), - PasswordLockFactor('pass1234'), - ]; - - // When - final unlocks = locks.map((e) => lock.unlocks(e)).toList(); - - // Then - expect(unlocks, everyElement(false)); - }); - - test('toJson result has type field', () { - // Given - const lock = VoidLockFactor(); - - // When - final json = lock.toJson(); - - // Then - expect(json.containsKey('type'), isTrue); - }); - - test('toString equals class name', () { - // Given - const lock = VoidLockFactor(); - - // When - final asString = lock.toString(); - - // Then - expect(asString, lock.runtimeType.toString()); - }); - }); - - group('PasswordLockFactor', () { - test('unlocks other PasswordLockFactor with matching data', () { + group(PasswordLockFactor, () { + test('seed generates utf8 version of password', () { // Given - const lock = PasswordLockFactor('admin1234'); - const otherLock = PasswordLockFactor('admin1234'); - - // When - final unlocks = lock.unlocks(otherLock); - - // Then - expect(unlocks, isTrue); - }); - - test('does not unlocks other PasswordLockFactor with different data', () { - // Given - const lock = PasswordLockFactor('admin1234'); - const otherLock = PasswordLockFactor('pass1234'); - - // When - final unlocks = lock.unlocks(otherLock); - - // Then - expect(unlocks, isFalse); - }); - - test('does not unlocks other non PasswordLockFactor', () { - // Given - const lock = PasswordLockFactor('admin1234'); - const otherLock = VoidLockFactor(); - - // When - final unlocks = lock.unlocks(otherLock); - - // Then - expect(unlocks, isFalse); - }); - - test('toJson result has type and data field', () { - // Given - const lock = PasswordLockFactor('admin1234'); + const password = 'admin1234'; + const lock = PasswordLockFactor(password); + final expected = utf8.encode(password); // When - final json = lock.toJson(); + final seed = lock.seed; // Then - expect(json.containsKey('type'), isTrue); - expect(json.containsKey('data'), isTrue); + expect(seed, expected); }); test('toString does not contain password', () { From d20746a6f31613c9e9814558d040ed3882d31b69 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 12:37:47 +0200 Subject: [PATCH 04/19] fix: keychain clearing vault --- .../catalyst_voices_services/lib/src/keychain/keychain.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart index 2d10047112f..b866c475d7a 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart @@ -48,9 +48,7 @@ class Keychain { Future clearAndLock() async { _logger.info('clearAndLock'); await _ensureUnlocked(); - await _vault.delete(key: _seedPhraseKey); - await _vault.setLock(const VoidLockFactor()); - await _vault.lock(); + await _vault.clear(); } /// Unlocks the keychain. From 48585e86eac26daf0be787c34d206216f14bcc1f Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 12:38:49 +0200 Subject: [PATCH 05/19] chore: spacing in SecureStorageVault --- .../lib/src/storage/vault/secure_storage_vault.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart index 9936f30cf68..f6b9f9b8f03 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart @@ -17,7 +17,6 @@ const _lockKey = 'LockKey'; /// facade for read/write operations. base class SecureStorageVault with StorageAsStringMixin implements Vault { final FlutterSecureStorage _secureStorage; - final CryptoService _cryptoService; bool _isUnlocked = false; From 7dd93b261a7882bc18fcd636c3716c9f5fa419e4 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 13:08:46 +0200 Subject: [PATCH 06/19] test: AesCryptoService --- .../lib/src/errors/crypto_exception.dart | 13 ++ .../lib/src/errors/errors.dart | 1 + .../lib/src/crypto/aes_crypto_service.dart | 55 ++++----- .../src/crypto/aes_crypto_service_test.dart | 115 ++++++++++++++++++ 4 files changed, 151 insertions(+), 33 deletions(-) create mode 100644 catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/test/src/crypto/aes_crypto_service_test.dart diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart new file mode 100644 index 00000000000..6c5b884563c --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; + +sealed class CryptoException extends Equatable implements Exception { + const CryptoException(); + + @override + List get props => []; +} + +/// Usually thrown when trying to decrypt with invalid key +final class CryptoAuthenticationException extends CryptoException { + const CryptoAuthenticationException(); +} diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/errors.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/errors.dart index 08cdc91100a..c467093a31c 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/errors.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/errors.dart @@ -1,3 +1,4 @@ +export 'crypto_exception.dart'; export 'network_error.dart'; export 'secure_storage_error.dart'; export 'vault_exception.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart index 04e04f1ca4f..a6588671cdf 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/foundation.dart'; @@ -67,28 +68,20 @@ final class AesCryptoService implements CryptoService { required Uint8List key, }) async { final algorithm = AesGcm.with256bits(nonceLength: _viLength); - final secretKey = SecretKey(key); - try { - final secretBox = SecretBox.fromConcatenation( - data, - nonceLength: _viLength, - macLength: algorithm.macAlgorithm.macLength, - ); - - await secretBox.checkMac( - macAlgorithm: algorithm.macAlgorithm, - secretKey: secretKey, - aad: [], - ); - - return algorithm - .decrypt(secretBox, secretKey: secretKey) - .then(Uint8List.fromList); - } finally { - secretKey.destroy(); - } + final secretBox = SecretBox.fromConcatenation( + data, + nonceLength: _viLength, + macLength: algorithm.macAlgorithm.macLength, + ); + + return algorithm + .decrypt(secretBox, secretKey: secretKey) + .then(Uint8List.fromList) + .onError( + (_, __) => throw const CryptoAuthenticationException(), + ); } @override @@ -99,19 +92,15 @@ final class AesCryptoService implements CryptoService { final algorithm = AesGcm.with256bits(nonceLength: _viLength); final secretKey = SecretKey(key); - try { - final secretBox = await algorithm.encrypt( - data, - secretKey: secretKey, - nonce: null, - aad: [], - possibleBuffer: null, - ); - - return secretBox.concatenation(); - } finally { - secretKey.destroy(); - } + final secretBox = await algorithm.encrypt( + data, + secretKey: secretKey, + nonce: null, + aad: [], + possibleBuffer: null, + ); + + return secretBox.concatenation(); } /// Builds list with [length] and random bytes in it. diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/aes_crypto_service_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/aes_crypto_service_test.dart new file mode 100644 index 00000000000..f342033ba04 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/aes_crypto_service_test.dart @@ -0,0 +1,115 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_services/src/crypto/aes_crypto_service.dart'; +import 'package:flutter/foundation.dart'; +import 'package:test/test.dart'; + +void main() { + final cryptoService = AesCryptoService(); + + group('AesCryptoService key derivation', () { + test( + 'derived key matches when verifying with same seed', + () async { + // Given + const lockFactor = PasswordLockFactor('admin'); + + // When + final seed = lockFactor.seed; + final key = await cryptoService.deriveKey(seed); + + // Then + final isVerified = await cryptoService.verifyKey(seed, key: key); + + expect(isVerified, isTrue); + }, + ); + + test( + 'derived key does not matches when verifying with different seed', + () async { + // Given + const lockFactor = PasswordLockFactor('admin'); + const otherLockFactor = PasswordLockFactor('1234'); + + // When + final seed = lockFactor.seed; + final otherSeed = otherLockFactor.seed; + + final key = await cryptoService.deriveKey(seed); + + // Then + final isVerified = await cryptoService.verifyKey(otherSeed, key: key); + + expect(isVerified, isFalse); + }, + ); + + test( + 'verifying key against too short seed returns false', + () async { + // Given + const lockFactor = PasswordLockFactor('admin'); + final invalidSeed = Uint8List.fromList([]); + + // When + final seed = lockFactor.seed; + final key = await cryptoService.deriveKey(seed); + + // Then + final isVerified = await cryptoService.verifyKey(invalidSeed, key: key); + + expect(isVerified, isFalse); + }, + ); + }); + + group('AesCryptoService crypto', () { + test('encrypted and later decrypted data with same key matches', () async { + // Given + const lockFactor = PasswordLockFactor('admin'); + const data = 'Hello Catalyst!'; + + // When + final key = await cryptoService.deriveKey(lockFactor.seed); + + // Then + final encoded = utf8.encode(data); + final encrypted = await cryptoService.encrypt(encoded, key: key); + + expect(encrypted, isNot(equals(encoded))); + + final decrypted = await cryptoService.decrypt(encrypted, key: key); + final decoded = utf8.decode(decrypted); + + expect(decoded, equals(data)); + }); + + test( + 'encrypted and later decrypted data with ' + 'different key gives throws exception', () async { + // Given + const lockFactor = PasswordLockFactor('admin'); + const unlockFactor = PasswordLockFactor('1234'); + + const data = 'Hello Catalyst!'; + + // When + final encryptKey = await cryptoService.deriveKey(lockFactor.seed); + final decryptKey = await cryptoService.deriveKey(unlockFactor.seed); + + // Then + final encoded = utf8.encode(data); + final encrypted = await cryptoService.encrypt(encoded, key: encryptKey); + + expect(encrypted, isNot(equals(encoded))); + + expect( + () => cryptoService.decrypt(encrypted, key: decryptKey), + throwsA(isA()), + ); + }); + }); +} From a969600e5f8742af8dd8eb4b883c9e6205e9f14b Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 13:25:58 +0200 Subject: [PATCH 07/19] chore: wip --- .../lib/src/crypto/aes_crypto_service.dart | 25 +++++++++++++++++-- .../storage/vault/secure_storage_vault.dart | 9 ++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart index a6588671cdf..0d24ab11dae 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:math'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; @@ -15,12 +16,18 @@ final class AesCryptoService implements CryptoService { /// Derived key hash length. static const int _keyLength = 16; + /// Versioning for future improvements + static const int _version = 1; + final Random _random; AesCryptoService({ Random? random, }) : _random = random ?? Random.secure(); + /// 3-byte marker attached at the end of encrypted data. + Uint8List get _checksum => utf8.encode('CHK'); + @override Future deriveKey( Uint8List seed, { @@ -92,15 +99,29 @@ final class AesCryptoService implements CryptoService { final algorithm = AesGcm.with256bits(nonceLength: _viLength); final secretKey = SecretKey(key); + final checksum = _checksum; + final combinedData = Uint8List.fromList([...data, ...checksum]); + final secretBox = await algorithm.encrypt( - data, + combinedData, secretKey: secretKey, nonce: null, aad: [], possibleBuffer: null, ); - return secretBox.concatenation(); + final concatenation = secretBox.concatenation(); + + // Combine version, salt, IV, and encrypted data + // Version 1, Algorithm ID 1 (AES-GCM) + final metadata = Uint8List.fromList([_version, 0x01]); + + final result = Uint8List.fromList([ + ...metadata, + ...concatenation, + ]); + + return result; } /// Builds list with [length] and random bytes in it. diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart index f6b9f9b8f03..38b7cbc774a 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart @@ -65,6 +65,8 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { _isUnlocked = await _cryptoService.verifyKey(seed, key: lock); + // TODO(damian-molinski): Erase lock; + return isUnlocked; } @@ -127,8 +129,11 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { } final lock = await _requireLock; + final decrypted = await _decrypt(encryptedData, key: lock); + + // TODO(damian-molinski): Erase lock; - return _decrypt(encryptedData, key: lock); + return decrypted; } /// Allows operation only when [isUnlocked] it true, otherwise non op. @@ -153,6 +158,8 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { final lock = await _requireLock; final encryptedData = await _encrypt(value, key: lock); + // TODO(damian-molinski): Erase lock; + await _secureStorage.write(key: effectiveKey, value: encryptedData); } From 80e6da0c8d98329c6fcf69d8ee86854637f8ba8d Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 14:13:01 +0200 Subject: [PATCH 08/19] feat: vault crypto service versioning + algorithm ID --- .../lib/src/errors/crypto_exception.dart | 40 +++++++++++++++ .../storage/vault/secure_storage_vault.dart | 4 +- .../vault/vault_crypto_service.dart} | 51 ++++++++++++++++--- .../vault/vault_crypto_service_test.dart} | 8 +-- 4 files changed, 89 insertions(+), 14 deletions(-) rename catalyst_voices/packages/catalyst_voices_services/lib/src/{crypto/aes_crypto_service.dart => storage/vault/vault_crypto_service.dart} (70%) rename catalyst_voices/packages/catalyst_voices_services/test/src/{crypto/aes_crypto_service_test.dart => storage/vault/vault_crypto_service_test.dart} (93%) diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart index 6c5b884563c..3853b3486a0 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart @@ -10,4 +10,44 @@ sealed class CryptoException extends Equatable implements Exception { /// Usually thrown when trying to decrypt with invalid key final class CryptoAuthenticationException extends CryptoException { const CryptoAuthenticationException(); + + @override + String toString() => 'CryptoAuthenticationException'; +} + +/// Thrown when data trying to decrypt was tempted with +final class CryptoDataMalformed extends CryptoException { + final String? message; + + const CryptoDataMalformed([this.message]); + + @override + String toString() { + if (message != null) return 'CryptoDataMalformed: $message'; + return 'CryptoDataMalformed'; + } +} + +final class CryptoVersionUnsupported extends CryptoException { + final String? message; + + const CryptoVersionUnsupported([this.message]); + + @override + String toString() { + if (message != null) return 'CryptoVersionUnsupported: $message'; + return 'CryptoVersionUnsupported'; + } +} + +final class CryptoAlgorithmUnsupported extends CryptoException { + final String? message; + + const CryptoAlgorithmUnsupported([this.message]); + + @override + String toString() { + if (message != null) return 'CryptoAlgorithmUnsupported: $message'; + return 'CryptoAlgorithmUnsupported'; + } } diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart index 38b7cbc774a..e4a31473ef3 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'dart:convert'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/crypto/aes_crypto_service.dart'; import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; import 'package:catalyst_voices_services/src/storage/storage_string_mixin.dart'; import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; import 'package:catalyst_voices_services/src/storage/vault/vault.dart'; +import 'package:catalyst_voices_services/src/storage/vault/vault_crypto_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -25,7 +25,7 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { FlutterSecureStorage secureStorage = const FlutterSecureStorage(), CryptoService? cryptoService, }) : _secureStorage = secureStorage, - _cryptoService = cryptoService ?? AesCryptoService(); + _cryptoService = cryptoService ?? VaultCryptoService(); Future get _hasLock { final effectiveKey = _buildVaultKey(_lockKey); diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault_crypto_service.dart similarity index 70% rename from catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart rename to catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault_crypto_service.dart index 0d24ab11dae..bc07184a584 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/aes_crypto_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault_crypto_service.dart @@ -6,7 +6,7 @@ import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/foundation.dart'; -final class AesCryptoService implements CryptoService { +final class VaultCryptoService implements CryptoService { /// Salt length for Argon2 key derivation. static const int _saltLength = 16; @@ -17,11 +17,15 @@ final class AesCryptoService implements CryptoService { static const int _keyLength = 16; /// Versioning for future improvements - static const int _version = 1; + static const int _currentVersion = 1; + + /// Algorithm id for future improvements + /// AES-GCM + static const int _currentAlgorithmId = 1; final Random _random; - AesCryptoService({ + VaultCryptoService({ Random? random, }) : _random = random ?? Random.secure(); @@ -74,21 +78,54 @@ final class AesCryptoService implements CryptoService { Uint8List data, { required Uint8List key, }) async { + if (data.length < 2) { + throw const CryptoDataMalformed(); + } + + // Extract the version, algorithm ID + final version = data[0]; + final algorithmId = data[1]; + + if (version != _currentVersion) { + throw CryptoVersionUnsupported('Version $version'); + } + + if (algorithmId != _currentAlgorithmId) { + throw CryptoAlgorithmUnsupported('Algorithm $version'); + } + final algorithm = AesGcm.with256bits(nonceLength: _viLength); final secretKey = SecretKey(key); + final encryptedData = data.sublist(2); + final secretBox = SecretBox.fromConcatenation( - data, + encryptedData, nonceLength: _viLength, macLength: algorithm.macAlgorithm.macLength, ); - return algorithm + final decryptedData = await algorithm .decrypt(secretBox, secretKey: secretKey) .then(Uint8List.fromList) .onError( (_, __) => throw const CryptoAuthenticationException(), ); + + // Verify checksum/marker + final checksum = _checksum; + if (decryptedData.length < checksum.length) { + throw const CryptoDataMalformed('Data is too short'); + } + final originalDataLength = decryptedData.length - checksum.length; + final originalData = decryptedData.sublist(0, originalDataLength); + final extractedChecksum = decryptedData.sublist(originalDataLength); + + if (!listEquals(checksum, extractedChecksum)) { + throw const CryptoDataMalformed('Checksum mismatch'); + } + + return originalData; } @override @@ -112,9 +149,7 @@ final class AesCryptoService implements CryptoService { final concatenation = secretBox.concatenation(); - // Combine version, salt, IV, and encrypted data - // Version 1, Algorithm ID 1 (AES-GCM) - final metadata = Uint8List.fromList([_version, 0x01]); + final metadata = Uint8List.fromList([_currentVersion, _currentAlgorithmId]); final result = Uint8List.fromList([ ...metadata, diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/aes_crypto_service_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/vault_crypto_service_test.dart similarity index 93% rename from catalyst_voices/packages/catalyst_voices_services/test/src/crypto/aes_crypto_service_test.dart rename to catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/vault_crypto_service_test.dart index f342033ba04..56839ac13a3 100644 --- a/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/aes_crypto_service_test.dart +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/vault_crypto_service_test.dart @@ -2,14 +2,14 @@ import 'dart:convert'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_services/catalyst_voices_services.dart'; -import 'package:catalyst_voices_services/src/crypto/aes_crypto_service.dart'; +import 'package:catalyst_voices_services/src/storage/vault/vault_crypto_service.dart'; import 'package:flutter/foundation.dart'; import 'package:test/test.dart'; void main() { - final cryptoService = AesCryptoService(); + final cryptoService = VaultCryptoService(); - group('AesCryptoService key derivation', () { + group('key derivation', () { test( 'derived key matches when verifying with same seed', () async { @@ -66,7 +66,7 @@ void main() { ); }); - group('AesCryptoService crypto', () { + group('crypto', () { test('encrypted and later decrypted data with same key matches', () async { // Given const lockFactor = PasswordLockFactor('admin'); From 054aa4e6829f7e4f724bd3dac4de9f2f58a10fc4 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 14:34:00 +0200 Subject: [PATCH 09/19] test: fix vault tests --- .../storage/vault/secure_storage_vault.dart | 8 ++++-- .../vault/secure_storage_vault_test.dart | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart index e4a31473ef3..488f67a732d 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart @@ -47,7 +47,7 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { } @override - Future get isUnlocked => SynchronousFuture(_isUnlocked); + Future get isUnlocked => Future(() => _isUnlocked); @override Future lock() async { @@ -118,7 +118,8 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { Future _guardedRead({ required String key, }) async { - if (!await isUnlocked) { + final isUnlocked = await this.isUnlocked; + if (!isUnlocked) { throw const VaultLockedException(); } @@ -144,7 +145,8 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { String? value, { required String key, }) async { - if (!await isUnlocked) { + final isUnlocked = await this.isUnlocked; + if (!isUnlocked) { throw const VaultLockedException(); } diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart index cfba41eb733..04f14875d53 100644 --- a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; import 'package:catalyst_voices_services/src/storage/vault/secure_storage_vault.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -28,31 +31,34 @@ void main() { expect(isUnlocked, isFalse); }); - test('read returns null when not unlocked', () async { + test('read when not unlocked throws exception', () async { // Given const key = 'SecureStorageVault.key'; const value = 'username'; // When await flutterSecureStorage.write(key: key, value: value); - final readValue = await vault.readString(key: key); // Then - expect(readValue, isNull); + expect( + () => vault.readString(key: key), + throwsA(isA()), + ); }); - test('write wont happen when is locked', () async { + test('write throws exception when is locked', () async { // Given const key = 'key'; - const fKey = 'SecureStorageVault.$key'; const value = 'username'; // When - await vault.writeString(value, key: key); - final readValue = await flutterSecureStorage.read(key: fKey); + await vault.lock(); // Then - expect(readValue, isNull); + expect( + () => vault.writeString(value, key: key), + throwsA(isA()), + ); }); test('unlock update lock and returns null when locked', () async { @@ -90,9 +96,9 @@ void main() { test('clear removes all vault keys', () async { // Given const lock = PasswordLockFactor('pass1234'); - const vaultKeyValues = { - 'one': 'qqq', - 'two': 'qqq', + final vaultKeyValues = { + 'one': utf8.fuse(base64).encode('qqq'), + 'two': utf8.fuse(base64).encode('qqq'), }; const nonVaultKeyValues = { 'three': 'qqq', From 48a199166455cc8d3731b5788501b5f71b77409a Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 14:43:11 +0200 Subject: [PATCH 10/19] fix: vault singleton registration --- catalyst_voices/lib/dependency/dependencies.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst_voices/lib/dependency/dependencies.dart b/catalyst_voices/lib/dependency/dependencies.dart index eee7f7d3df8..f9754006dd2 100644 --- a/catalyst_voices/lib/dependency/dependencies.dart +++ b/catalyst_voices/lib/dependency/dependencies.dart @@ -55,7 +55,7 @@ final class Dependencies extends DependencyProvider { void _registerServices() { registerLazySingleton(() => const SecureStorage()); - registerLazySingleton(() => const SecureStorageVault()); + registerLazySingleton(SecureStorageVault.new); registerLazySingleton( () => const SecureDummyAuthStorage(), ); From e4022b24a3539d53984614b2e4d158c01294da29 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 15:22:23 +0200 Subject: [PATCH 11/19] fix: keychain adjustments --- .../lib/src/session/session_bloc.dart | 18 +++++++------ .../lib/src/keychain/keychain.dart | 25 ++++++++++--------- .../registration/registration_service.dart | 1 + .../storage/vault/secure_storage_vault.dart | 15 +++++------ .../lib/src/storage/vault/vault.dart | 5 ++-- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/session/session_bloc.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/session/session_bloc.dart index e38d3ad0a0c..30d58759880 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/session/session_bloc.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/session/session_bloc.dart @@ -29,12 +29,6 @@ final class SessionBloc extends Bloc ) async { if (!await _keychain.hasSeedPhrase) { emit(const VisitorSessionState()); - } else if (await _keychain.isUnlocked) { - // TODO(damian-molinski): we shouldn't keep the keychain unlocked - // after leaving the app. In the future once keychain stays locked - // when leaving the app the logic here is not needed. - await _keychain.lock(); - emit(const GuestSessionState()); } else { emit(const GuestSessionState()); } @@ -56,7 +50,9 @@ final class SessionBloc extends Bloc LockSessionEvent event, Emitter emit, ) async { - await _keychain.lock(); + if (await _keychain.hasLock) { + await _keychain.lock(); + } emit(const GuestSessionState()); } @@ -88,6 +84,10 @@ final class SessionBloc extends Bloc GuestSessionEvent event, Emitter emit, ) async { + if (await _keychain.hasLock && !await _keychain.isUnlocked) { + await _keychain.unlock(_dummyUnlockFactor); + } + await _keychain.setLockAndBeginWith( seedPhrase: _dummySeedPhrase, unlockFactor: _dummyUnlockFactor, @@ -101,6 +101,10 @@ final class SessionBloc extends Bloc ActiveUserSessionEvent event, Emitter emit, ) async { + if (await _keychain.hasLock && !await _keychain.isUnlocked) { + await _keychain.unlock(_dummyUnlockFactor); + } + await _keychain.setLockAndBeginWith( seedPhrase: _dummySeedPhrase, unlockFactor: _dummyUnlockFactor, diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart index a85144aba29..47a42129827 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart @@ -21,9 +21,12 @@ final class Keychain { Keychain(this._keyDerivation, this._vault); - /// Returns true if the keychain is unlocked, false otherwise. + /// See [Vault.isUnlocked]. Future get isUnlocked => _vault.isUnlocked; + /// See [Vault.hasLock]. + Future get hasLock => _vault.hasLock; + /// Returns true if the keychain contains the seed phrase, false otherwise. Future get hasSeedPhrase async => _hasSeedPhrase; @@ -37,8 +40,14 @@ final class Keychain { bool unlocked = true, }) async { _logger.info('setLockAndBeginWith, unlocked: $unlocked'); - await _changeLock(unlockFactor, unlocked: unlocked); + + await _changeLock(unlockFactor); + await _vault.unlock(unlockFactor); await _beginWith(seedPhrase: seedPhrase); + + if (!unlocked) { + await _vault.lock(); + } } /// Clears the keychain and all associated data. @@ -47,8 +56,8 @@ final class Keychain { /// from the underlying storage. Future clearAndLock() async { _logger.info('clearAndLock'); - await _ensureUnlocked(); await _vault.clear(); + await _vault.lock(); } /// Unlocks the keychain. @@ -93,16 +102,8 @@ final class Keychain { } } - Future _changeLock( - LockFactor lockFactor, { - bool unlocked = false, - }) async { + Future _changeLock(LockFactor lockFactor) async { await _vault.setLock(lockFactor); - if (unlocked) { - await _vault.unlock(lockFactor); - } else { - await _vault.lock(); - } } Future _beginWith({ diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/registration/registration_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/registration/registration_service.dart index ac4119f9253..411fdf70030 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/registration/registration_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/registration/registration_service.dart @@ -33,6 +33,7 @@ final class RegistrationService { required SeedPhrase seedPhrase, required String unlockPassword, }) async { + await _keychain.clearAndLock(); await _keychain.setLockAndBeginWith( seedPhrase: seedPhrase, unlockFactor: PasswordLockFactor(unlockPassword), diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart index 488f67a732d..380d51bdf21 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart @@ -27,11 +27,6 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { }) : _secureStorage = secureStorage, _cryptoService = cryptoService ?? VaultCryptoService(); - Future get _hasLock { - final effectiveKey = _buildVaultKey(_lockKey); - return _secureStorage.containsKey(key: effectiveKey); - } - Future get _lock async { final effectiveKey = _buildVaultKey(_lockKey); final encodedLock = await _secureStorage.read(key: effectiveKey); @@ -49,6 +44,12 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { @override Future get isUnlocked => Future(() => _isUnlocked); + @override + Future get hasLock async { + final effectiveKey = _buildVaultKey(_lockKey); + return _secureStorage.containsKey(key: effectiveKey); + } + @override Future lock() async { _isUnlocked = false; @@ -56,7 +57,7 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { @override Future unlock(LockFactor unlock) async { - if (!await _hasLock) { + if (!await hasLock) { throw const LockNotFoundException('Set lock before unlocking Vault'); } @@ -72,7 +73,7 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { @override Future setLock(LockFactor lock) async { - if (await _hasLock && !await isUnlocked) { + if (await hasLock && !await isUnlocked) { throw const VaultLockedException(); } diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault.dart index 35717e704b2..e67abc1e69e 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault.dart @@ -6,12 +6,13 @@ import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; /// /// In order to unlock [Vault] sufficient [LockFactor] have to be /// set via [unlock] that can unlock [LockFactor] from [setLock]. -/// -/// See [LockFactor.unlocks] for more details. abstract interface class Vault implements Storage { /// Returns true when have sufficient [LockFactor] from [unlock]. Future get isUnlocked; + /// Returns whether currently have active lock from [setLock]. + Future get hasLock; + /// Deletes unlockFactor if have any. Future lock(); From bc9db8cb4d0154f057b82f84cd6cecd4c361decf Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 16 Oct 2024 16:25:42 +0200 Subject: [PATCH 12/19] chore: benchmark crypto service --- .../lib/src/crypto/vault_crypto_service.dart | 231 ++++++++++++++++++ .../storage/vault/secure_storage_vault.dart | 2 +- .../storage/vault/vault_crypto_service.dart | 174 ------------- .../vault_crypto_service_test.dart | 2 +- 4 files changed, 233 insertions(+), 176 deletions(-) create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart delete mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault_crypto_service.dart rename catalyst_voices/packages/catalyst_voices_services/test/src/{storage/vault => crypto}/vault_crypto_service_test.dart (97%) diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart new file mode 100644 index 00000000000..74d50447647 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart @@ -0,0 +1,231 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +final _logger = Logger('VaultCryptoService'); + +final class VaultCryptoService implements CryptoService { + /// Salt length for Argon2 key derivation. + static const int _saltLength = 16; + + /// Salt length for AesGcm data encryption. + static const int _viLength = 12; + + /// Derived key hash length. + static const int _keyLength = 16; + + /// Versioning for future improvements + static const int _currentVersion = 1; + + /// Algorithm id for future improvements + /// AES-GCM + static const int _currentAlgorithmId = 1; + + final Random _random; + + VaultCryptoService({ + Random? random, + }) : _random = random ?? Random.secure(); + + /// 3-byte marker attached at the end of encrypted data. + Uint8List get _checksum => utf8.encode('CHK'); + + // Note. Argon2id has no browser implementation and is slow > 1s. + @override + Future deriveKey( + Uint8List seed, { + Uint8List? salt, + }) { + Future run() async { + final algorithm = Argon2id( + parallelism: 4, + memory: 10000, // 10 000 x 1kB block = 10 MB + iterations: 1, + hashLength: _keyLength, + ); + + // final algorithm = Pbkdf2( + // macAlgorithm: Hmac.sha256(), + // iterations: 10000, // 20k iterations + // bits: 256, // 256 bits = 32 bytes output + // ); + + final keySalt = salt ?? _generateRandomList(length: _saltLength); + + final secretKey = await algorithm.deriveKey( + secretKey: SecretKey(seed), + nonce: keySalt, + ); + + final keyBytes = await secretKey.extractBytes().then(Uint8List.fromList); + + secretKey.destroy(); + + // Combine salt and hashed password for storage + return Uint8List.fromList([...keySalt, ...keyBytes]); + } + + if (kDebugMode) { + return _benchmark(run(), debugLabel: 'DeriveKey'); + } else { + return run(); + } + } + + @override + Future verifyKey( + Uint8List seed, { + required Uint8List key, + }) { + Future run() async { + final salt = key.sublist(0, _saltLength); + if (salt.length < _saltLength) { + return false; + } + + final derivedKey = await deriveKey(seed, salt: salt); + return listEquals(derivedKey, key); + } + + if (kDebugMode) { + return _benchmark(run(), debugLabel: 'VerifyKey'); + } else { + return run(); + } + } + + @override + Future decrypt( + Uint8List data, { + required Uint8List key, + }) { + Future run() async { + if (data.length < 2) { + throw const CryptoDataMalformed(); + } + + // Extract the version, algorithm ID + final version = data[0]; + final algorithmId = data[1]; + + if (version != _currentVersion) { + throw CryptoVersionUnsupported('Version $version'); + } + + if (algorithmId != _currentAlgorithmId) { + throw CryptoAlgorithmUnsupported('Algorithm $version'); + } + + final algorithm = AesGcm.with256bits(nonceLength: _viLength); + final secretKey = SecretKey(key); + + final encryptedData = data.sublist(2); + + final secretBox = SecretBox.fromConcatenation( + encryptedData, + nonceLength: _viLength, + macLength: algorithm.macAlgorithm.macLength, + ); + + final decryptedData = await algorithm + .decrypt(secretBox, secretKey: secretKey) + .then(Uint8List.fromList) + .onError( + (_, __) => throw const CryptoAuthenticationException(), + ); + + // Verify checksum/marker + final checksum = _checksum; + if (decryptedData.length < checksum.length) { + throw const CryptoDataMalformed('Data is too short'); + } + final originalDataLength = decryptedData.length - checksum.length; + final originalData = decryptedData.sublist(0, originalDataLength); + final extractedChecksum = decryptedData.sublist(originalDataLength); + + if (!listEquals(checksum, extractedChecksum)) { + throw const CryptoDataMalformed('Checksum mismatch'); + } + + return originalData; + } + + if (kDebugMode) { + return _benchmark(run(), debugLabel: 'Decrypt'); + } else { + return run(); + } + } + + @override + Future encrypt( + Uint8List data, { + required Uint8List key, + }) { + Future run() async { + final algorithm = AesGcm.with256bits(nonceLength: _viLength); + final secretKey = SecretKey(key); + + final checksum = _checksum; + final combinedData = Uint8List.fromList([...data, ...checksum]); + + final secretBox = await algorithm.encrypt( + combinedData, + secretKey: secretKey, + nonce: null, + aad: [], + possibleBuffer: null, + ); + + final concatenation = secretBox.concatenation(); + + final metadata = + Uint8List.fromList([_currentVersion, _currentAlgorithmId]); + + final result = Uint8List.fromList([ + ...metadata, + ...concatenation, + ]); + + return result; + } + + if (kDebugMode) { + return _benchmark(run(), debugLabel: 'Encrypt'); + } else { + return run(); + } + } + + /// Builds list with [length] and random bytes in it. + Uint8List _generateRandomList({ + required int length, + }) { + final list = Uint8List(length); + + for (var i = 0; i < length; i++) { + list[i] = _random.nextInt(16); + } + + return list; + } + + Future _benchmark( + Future future, { + required String debugLabel, + }) { + final stopwatch = Stopwatch()..start(); + + return future.whenComplete( + () { + stopwatch.stop(); + _logger.finer('Took[$debugLabel] ${stopwatch.elapsed}'); + }, + ); + } +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart index 380d51bdf21..412254b03f1 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart @@ -3,10 +3,10 @@ import 'dart:convert'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; +import 'package:catalyst_voices_services/src/crypto/vault_crypto_service.dart'; import 'package:catalyst_voices_services/src/storage/storage_string_mixin.dart'; import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; import 'package:catalyst_voices_services/src/storage/vault/vault.dart'; -import 'package:catalyst_voices_services/src/storage/vault/vault_crypto_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault_crypto_service.dart deleted file mode 100644 index bc07184a584..00000000000 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault_crypto_service.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; -import 'package:cryptography/cryptography.dart'; -import 'package:flutter/foundation.dart'; - -final class VaultCryptoService implements CryptoService { - /// Salt length for Argon2 key derivation. - static const int _saltLength = 16; - - /// Salt length for AesGcm data encryption. - static const int _viLength = 12; - - /// Derived key hash length. - static const int _keyLength = 16; - - /// Versioning for future improvements - static const int _currentVersion = 1; - - /// Algorithm id for future improvements - /// AES-GCM - static const int _currentAlgorithmId = 1; - - final Random _random; - - VaultCryptoService({ - Random? random, - }) : _random = random ?? Random.secure(); - - /// 3-byte marker attached at the end of encrypted data. - Uint8List get _checksum => utf8.encode('CHK'); - - @override - Future deriveKey( - Uint8List seed, { - Uint8List? salt, - }) async { - final algorithm = Argon2id( - parallelism: 2, - memory: 10000, // 10 000 x 1kB block = 10 MB - iterations: 1, - hashLength: _keyLength, - ); - - salt ??= _generateRandomList(length: _saltLength); - - final secretKey = await algorithm.deriveKey( - secretKey: SecretKey(seed), - nonce: salt, - ); - - final keyBytes = await secretKey.extractBytes().then(Uint8List.fromList); - - secretKey.destroy(); - - // Combine salt and hashed password for storage - return Uint8List.fromList([...salt, ...keyBytes]); - } - - @override - Future verifyKey( - Uint8List seed, { - required Uint8List key, - }) async { - final salt = key.sublist(0, _saltLength); - if (salt.length < _saltLength) { - return false; - } - - final derivedKey = await deriveKey(seed, salt: salt); - return listEquals(derivedKey, key); - } - - @override - Future decrypt( - Uint8List data, { - required Uint8List key, - }) async { - if (data.length < 2) { - throw const CryptoDataMalformed(); - } - - // Extract the version, algorithm ID - final version = data[0]; - final algorithmId = data[1]; - - if (version != _currentVersion) { - throw CryptoVersionUnsupported('Version $version'); - } - - if (algorithmId != _currentAlgorithmId) { - throw CryptoAlgorithmUnsupported('Algorithm $version'); - } - - final algorithm = AesGcm.with256bits(nonceLength: _viLength); - final secretKey = SecretKey(key); - - final encryptedData = data.sublist(2); - - final secretBox = SecretBox.fromConcatenation( - encryptedData, - nonceLength: _viLength, - macLength: algorithm.macAlgorithm.macLength, - ); - - final decryptedData = await algorithm - .decrypt(secretBox, secretKey: secretKey) - .then(Uint8List.fromList) - .onError( - (_, __) => throw const CryptoAuthenticationException(), - ); - - // Verify checksum/marker - final checksum = _checksum; - if (decryptedData.length < checksum.length) { - throw const CryptoDataMalformed('Data is too short'); - } - final originalDataLength = decryptedData.length - checksum.length; - final originalData = decryptedData.sublist(0, originalDataLength); - final extractedChecksum = decryptedData.sublist(originalDataLength); - - if (!listEquals(checksum, extractedChecksum)) { - throw const CryptoDataMalformed('Checksum mismatch'); - } - - return originalData; - } - - @override - Future encrypt( - Uint8List data, { - required Uint8List key, - }) async { - final algorithm = AesGcm.with256bits(nonceLength: _viLength); - final secretKey = SecretKey(key); - - final checksum = _checksum; - final combinedData = Uint8List.fromList([...data, ...checksum]); - - final secretBox = await algorithm.encrypt( - combinedData, - secretKey: secretKey, - nonce: null, - aad: [], - possibleBuffer: null, - ); - - final concatenation = secretBox.concatenation(); - - final metadata = Uint8List.fromList([_currentVersion, _currentAlgorithmId]); - - final result = Uint8List.fromList([ - ...metadata, - ...concatenation, - ]); - - return result; - } - - /// Builds list with [length] and random bytes in it. - Uint8List _generateRandomList({ - required int length, - }) { - final list = Uint8List(length); - - for (var i = 0; i < length; i++) { - list[i] = _random.nextInt(16); - } - - return list; - } -} diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/vault_crypto_service_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart similarity index 97% rename from catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/vault_crypto_service_test.dart rename to catalyst_voices/packages/catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart index 56839ac13a3..adceba13ae5 100644 --- a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/vault_crypto_service_test.dart +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_services/catalyst_voices_services.dart'; -import 'package:catalyst_voices_services/src/storage/vault/vault_crypto_service.dart'; +import 'package:catalyst_voices_services/src/crypto/vault_crypto_service.dart'; import 'package:flutter/foundation.dart'; import 'package:test/test.dart'; From 0e2251b65b31ecdf8596b581aabe012a306d4201 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 17 Oct 2024 08:22:45 +0200 Subject: [PATCH 13/19] fix: Use Pbkdf2 instead of Argon2id key derivation algorithm --- .config/dictionaries/project.dic | 1 + .../lib/src/crypto/vault_crypto_service.dart | 31 +++++++------------ .../src/crypto/vault_crypto_service_test.dart | 2 +- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index b0f2f0d0729..dc585f17a35 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -298,3 +298,4 @@ xctestrun xcworkspace xvfb yoroi +Pbkdf2 \ No newline at end of file diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart index 74d50447647..107bd8db29a 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart @@ -10,19 +10,16 @@ import 'package:logging/logging.dart'; final _logger = Logger('VaultCryptoService'); final class VaultCryptoService implements CryptoService { - /// Salt length for Argon2 key derivation. + /// Salt length for key derivation. static const int _saltLength = 16; /// Salt length for AesGcm data encryption. static const int _viLength = 12; - /// Derived key hash length. - static const int _keyLength = 16; - - /// Versioning for future improvements + /// Versioning for future improvements. static const int _currentVersion = 1; - /// Algorithm id for future improvements + /// Algorithm id for future improvements. /// AES-GCM static const int _currentAlgorithmId = 1; @@ -35,26 +32,20 @@ final class VaultCryptoService implements CryptoService { /// 3-byte marker attached at the end of encrypted data. Uint8List get _checksum => utf8.encode('CHK'); - // Note. Argon2id has no browser implementation and is slow > 1s. + // Note. Argon2id has no native browser implementation and dart one is + // slow > 1s. That's why Pbkdf2 is used. @override Future deriveKey( Uint8List seed, { Uint8List? salt, }) { Future run() async { - final algorithm = Argon2id( - parallelism: 4, - memory: 10000, // 10 000 x 1kB block = 10 MB - iterations: 1, - hashLength: _keyLength, + final algorithm = Pbkdf2( + macAlgorithm: Hmac.sha256(), + iterations: 10000, // 20k iterations + bits: 256, // 256 bits = 32 bytes output ); - // final algorithm = Pbkdf2( - // macAlgorithm: Hmac.sha256(), - // iterations: 10000, // 20k iterations - // bits: 256, // 256 bits = 32 bytes output - // ); - final keySalt = salt ?? _generateRandomList(length: _saltLength); final secretKey = await algorithm.deriveKey( @@ -122,7 +113,7 @@ final class VaultCryptoService implements CryptoService { } final algorithm = AesGcm.with256bits(nonceLength: _viLength); - final secretKey = SecretKey(key); + final secretKey = SecretKey(key.sublist(_saltLength)); final encryptedData = data.sublist(2); @@ -169,7 +160,7 @@ final class VaultCryptoService implements CryptoService { }) { Future run() async { final algorithm = AesGcm.with256bits(nonceLength: _viLength); - final secretKey = SecretKey(key); + final secretKey = SecretKey(key.sublist(_saltLength)); final checksum = _checksum; final combinedData = Uint8List.fromList([...data, ...checksum]); diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart index adceba13ae5..1c89588cab7 100644 --- a/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart @@ -52,7 +52,7 @@ void main() { () async { // Given const lockFactor = PasswordLockFactor('admin'); - final invalidSeed = Uint8List.fromList([]); + final invalidSeed = Uint8List.fromList([0, 0, 0]); // When final seed = lockFactor.seed; From bd85d1d63ee0f3189a745235c89dd81b7c3694ac Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 17 Oct 2024 08:44:17 +0200 Subject: [PATCH 14/19] docs: improve docs for CryptoService, VaultCryptoService and Keychain --- .../lib/src/crypto/crypto_service.dart | 47 +++++++++++++++++++ .../lib/src/crypto/vault_crypto_service.dart | 11 +++++ .../lib/src/keychain/keychain.dart | 3 ++ 3 files changed, 61 insertions(+) diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart index 8bbb7cc56a8..87708c0e67c 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart @@ -1,21 +1,68 @@ import 'package:flutter/foundation.dart'; +/// An abstract interface that defines cryptographic operations such as +/// key derivation, encryption, decryption, and key verification. abstract interface class CryptoService { + /// Derives a cryptographic key from a given seed, with an optional salt. + /// + /// The derived key is generated based on the provided [seed], which serves + /// as the primary input. Optionally, a [salt] can be used to further + /// randomize the key derivation process, increasing security. + /// + /// - [seed]: The main input data used for key derivation. + /// - [salt]: Optional salt value to randomize the derived key (can be null). + /// + /// Returns a [Future] that completes with the derived key as a [Uint8List]. Future deriveKey( Uint8List seed, { Uint8List? salt, }); + /// Verifies if a given cryptographic key is correctly derived from a seed. + /// + /// This method checks whether the provided [key] matches the key that would + /// be derived from the given [seed]. This can be useful to verify integrity + /// or correctness of the key derivation process. + /// + /// - [seed]: The input data used for key derivation. + /// - [key]: The derived key that needs to be verified. + /// + /// Returns a [Future] that completes with `true` if the [key] is valid and + /// correctly derived from the [seed], or `false` otherwise. Future verifyKey( Uint8List seed, { required Uint8List key, }); + /// Decrypts the provided [data] using the specified cryptographic [key], + /// usually build using [deriveKey]. + /// + /// This method takes encrypted [data] and decrypts it using the provided + /// [key]. The decryption algorithm and the format of the data should be + /// defined by the implementing class. + /// + /// - [data]: The encrypted data to be decrypted. + /// - [key]: The key used for decryption. + /// + /// Returns a [Future] that completes with the decrypted data as a + /// [Uint8List]. Future decrypt( Uint8List data, { required Uint8List key, }); + /// Encrypts the provided [data] using the specified cryptographic [key], + /// usually build using [deriveKey]. + /// + /// This method takes plaintext [data] and encrypts it using the provided + /// [key]. The encryption algorithm and format of the output should be defined + /// by the implementing class. + /// + /// - [data]: The plaintext data to be encrypted. + /// - [key]: The key used for encryption. + /// + /// Returns a [Future] that completes with the encrypted data as a + /// [Uint8List]. Future encrypt( Uint8List data, { required Uint8List key, diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart index 107bd8db29a..52edf9922ab 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart @@ -3,12 +3,23 @@ import 'dart:math'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; +import 'package:catalyst_voices_services/src/storage/vault/vault.dart'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; final _logger = Logger('VaultCryptoService'); +/// [CryptoService] implementation used by default in [Vault]. +/// +/// It uses Pbkdf2 for key derivation as well as +/// AesGcm for encryption/decryption. +/// +/// Only keys build by [VaultCryptoService.deriveKey] should be used +/// for crypto operations are we're adding [VaultCryptoService] specific +/// metadata to them. +/// +/// Supports version for future changes. final class VaultCryptoService implements CryptoService { /// Salt length for key derivation. static const int _saltLength = 16; diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart index 47a42129827..40827992654 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/keychain.dart @@ -13,6 +13,9 @@ const _seedPhraseKey = 'keychain_seed_phrase'; // TODO(dtscalac): in the future when key derivation algorithm spec // will become stable consider to store derived keys instead of deriving // them each time they are needed. + +// TODO(damian-molinski): because we have dummy lock factors vault unlocking +// is dummy too. Any operation on vault require correct lock factor input. final class Keychain { final _logger = Logger('Keychain'); From 593dd2a24eee2cfc173e2fa5fcf470b96c0f6699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20Moli=C5=84ski?= <47773413+damian-molinski@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:23:18 +0200 Subject: [PATCH 15/19] Update catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --- .../catalyst_voices_models/lib/src/errors/crypto_exception.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart index 3853b3486a0..fdd5950483a 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart @@ -15,7 +15,7 @@ final class CryptoAuthenticationException extends CryptoException { String toString() => 'CryptoAuthenticationException'; } -/// Thrown when data trying to decrypt was tempted with +/// Thrown when trying to decrypt tampered data final class CryptoDataMalformed extends CryptoException { final String? message; From fc5a278676c1270077e41ee6ae34be9f38c12e7f Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 17 Oct 2024 11:26:32 +0200 Subject: [PATCH 16/19] fix: missing props in exceptions --- .../lib/src/errors/crypto_exception.dart | 9 +++++++++ .../lib/src/errors/vault_exception.dart | 3 +++ 2 files changed, 12 insertions(+) diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart index 3853b3486a0..efe3c3e1134 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/crypto_exception.dart @@ -26,6 +26,9 @@ final class CryptoDataMalformed extends CryptoException { if (message != null) return 'CryptoDataMalformed: $message'; return 'CryptoDataMalformed'; } + + @override + List get props => [message]; } final class CryptoVersionUnsupported extends CryptoException { @@ -38,6 +41,9 @@ final class CryptoVersionUnsupported extends CryptoException { if (message != null) return 'CryptoVersionUnsupported: $message'; return 'CryptoVersionUnsupported'; } + + @override + List get props => [message]; } final class CryptoAlgorithmUnsupported extends CryptoException { @@ -50,4 +56,7 @@ final class CryptoAlgorithmUnsupported extends CryptoException { if (message != null) return 'CryptoAlgorithmUnsupported: $message'; return 'CryptoAlgorithmUnsupported'; } + + @override + List get props => [message]; } diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/vault_exception.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/vault_exception.dart index ffd17bbaeb5..39282d85b90 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/vault_exception.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/errors/vault_exception.dart @@ -17,6 +17,9 @@ final class LockNotFoundException extends VaultException { if (message != null) return 'LockNotFoundException: $message'; return 'LockNotFoundException'; } + + @override + List get props => [message]; } final class VaultLockedException extends VaultException { From ba99f6eb101e38c4a5f53aba6ce0149eb5b087f1 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 17 Oct 2024 11:34:34 +0200 Subject: [PATCH 17/19] docs: CryptoService and KeyDerivation connection --- .../catalyst_voices_services/lib/src/crypto/crypto_service.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart index 87708c0e67c..edafb8d76a1 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/crypto_service.dart @@ -2,6 +2,8 @@ import 'package:flutter/foundation.dart'; /// An abstract interface that defines cryptographic operations such as /// key derivation, encryption, decryption, and key verification. +// TODO(damian-molinski): Expose KeyDerivation interface and have it +// delegate implementation abstract interface class CryptoService { /// Derives a cryptographic key from a given seed, with an optional salt. /// From dd7542a12cb76e5972b07b3b79b1b4f11979ca08 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 17 Oct 2024 13:10:10 +0200 Subject: [PATCH 18/19] change random number to max 255 --- .../lib/src/crypto/vault_crypto_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart index 52edf9922ab..19a3b6cab71 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart @@ -211,7 +211,7 @@ final class VaultCryptoService implements CryptoService { final list = Uint8List(length); for (var i = 0; i < length; i++) { - list[i] = _random.nextInt(16); + list[i] = _random.nextInt(256); } return list; From 6d68f7c42cab1aed7d72dd5ca40c907a1bbe806e Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 17 Oct 2024 13:10:22 +0200 Subject: [PATCH 19/19] fix: typo --- .../lib/src/crypto/vault_crypto_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart index 19a3b6cab71..0bc0e6505bb 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart @@ -211,7 +211,7 @@ final class VaultCryptoService implements CryptoService { final list = Uint8List(length); for (var i = 0; i < length; i++) { - list[i] = _random.nextInt(256); + list[i] = _random.nextInt(255); } return list;