From 0cbac6dfa166451abcba4ce86d05cdf198bb499c Mon Sep 17 00:00:00 2001 From: Chralu Date: Tue, 4 Jun 2024 14:38:56 +0200 Subject: [PATCH 1/2] Regenerate with build_runner. --- lib/application/oracle/state.g.dart | 2 +- lib/application/session/session.g.dart | 2 +- lib/infrastructure/rpc/dto/rpc_request.g.dart | 2 +- lib/model/blockchain/keychain_secured_infos.g.dart | 6 ++++-- lib/model/keychain_service_keypair.g.dart | 10 ++++++---- lib/model/nft_category.g.dart | 2 +- lib/model/token_transfer_wallet.g.dart | 4 ++-- lib/model/uco_transfer_wallet.g.dart | 2 +- .../layouts/nft_creation_process_sheet.g.dart | 2 +- 9 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/application/oracle/state.g.dart b/lib/application/oracle/state.g.dart index b6073d132..d3cc4a2b0 100644 --- a/lib/application/oracle/state.g.dart +++ b/lib/application/oracle/state.g.dart @@ -9,7 +9,7 @@ part of 'state.dart'; _$ArchethicOracleUCOImpl _$$ArchethicOracleUCOImplFromJson( Map json) => _$ArchethicOracleUCOImpl( - timestamp: json['timestamp'] as int? ?? 0, + timestamp: (json['timestamp'] as num?)?.toInt() ?? 0, eur: (json['eur'] as num?)?.toDouble() ?? 0, usd: (json['usd'] as num?)?.toDouble() ?? 0, ); diff --git a/lib/application/session/session.g.dart b/lib/application/session/session.g.dart index 60ecf10ce..1836a77a3 100644 --- a/lib/application/session/session.g.dart +++ b/lib/application/session/session.g.dart @@ -6,7 +6,7 @@ part of 'session.dart'; // RiverpodGenerator // ************************************************************************** -String _$sessionNotifierHash() => r'2fe58ad2a9c849798604b78ee783099031d16cb8'; +String _$sessionNotifierHash() => r'31eedf6ad75a6f6aef9b160270b055c0908c1d95'; /// See also [_SessionNotifier]. @ProviderFor(_SessionNotifier) diff --git a/lib/infrastructure/rpc/dto/rpc_request.g.dart b/lib/infrastructure/rpc/dto/rpc_request.g.dart index 0fc3a26dc..3ad72a581 100644 --- a/lib/infrastructure/rpc/dto/rpc_request.g.dart +++ b/lib/infrastructure/rpc/dto/rpc_request.g.dart @@ -26,7 +26,7 @@ _$RpcRequestImpl _$$RpcRequestImplFromJson(Map json) => _$RpcRequestImpl( origin: RpcRequestOriginDTO.fromJson(json['origin'] as Map), - version: json['version'] as int, + version: (json['version'] as num).toInt(), payload: json['payload'] as Map, ); diff --git a/lib/model/blockchain/keychain_secured_infos.g.dart b/lib/model/blockchain/keychain_secured_infos.g.dart index a95f2f81c..5abee0cf4 100644 --- a/lib/model/blockchain/keychain_secured_infos.g.dart +++ b/lib/model/blockchain/keychain_secured_infos.g.dart @@ -9,8 +9,10 @@ part of 'keychain_secured_infos.dart'; _$KeychainSecuredInfosImpl _$$KeychainSecuredInfosImplFromJson( Map json) => _$KeychainSecuredInfosImpl( - seed: (json['seed'] as List).map((e) => e as int).toList(), - version: json['version'] as int, + seed: (json['seed'] as List) + .map((e) => (e as num).toInt()) + .toList(), + version: (json['version'] as num).toInt(), services: (json['services'] as Map?)?.map( (k, e) => MapEntry( k, diff --git a/lib/model/keychain_service_keypair.g.dart b/lib/model/keychain_service_keypair.g.dart index c6ec40482..beeb3cee9 100644 --- a/lib/model/keychain_service_keypair.g.dart +++ b/lib/model/keychain_service_keypair.g.dart @@ -9,10 +9,12 @@ part of 'keychain_service_keypair.dart'; _$KeychainServiceKeyPairImpl _$$KeychainServiceKeyPairImplFromJson( Map json) => _$KeychainServiceKeyPairImpl( - privateKey: - (json['privateKey'] as List).map((e) => e as int).toList(), - publicKey: - (json['publicKey'] as List).map((e) => e as int).toList(), + privateKey: (json['privateKey'] as List) + .map((e) => (e as num).toInt()) + .toList(), + publicKey: (json['publicKey'] as List) + .map((e) => (e as num).toInt()) + .toList(), ); Map _$$KeychainServiceKeyPairImplToJson( diff --git a/lib/model/nft_category.g.dart b/lib/model/nft_category.g.dart index 210edaffe..15546f3c6 100644 --- a/lib/model/nft_category.g.dart +++ b/lib/model/nft_category.g.dart @@ -8,7 +8,7 @@ part of 'nft_category.dart'; _$NftCategoryImpl _$$NftCategoryImplFromJson(Map json) => _$NftCategoryImpl( - id: json['id'] as int? ?? 0, + id: (json['id'] as num?)?.toInt() ?? 0, name: json['name'] ?? '', ); diff --git a/lib/model/token_transfer_wallet.g.dart b/lib/model/token_transfer_wallet.g.dart index d0bf7493a..81f019fd4 100644 --- a/lib/model/token_transfer_wallet.g.dart +++ b/lib/model/token_transfer_wallet.g.dart @@ -9,10 +9,10 @@ part of 'token_transfer_wallet.dart'; _$TokenTransferWalletImpl _$$TokenTransferWalletImplFromJson( Map json) => _$TokenTransferWalletImpl( - amount: json['amount'] as int?, + amount: (json['amount'] as num?)?.toInt(), to: json['to'] as String?, tokenAddress: json['tokenAddress'] as String?, - tokenId: json['tokenId'] as int?, + tokenId: (json['tokenId'] as num?)?.toInt(), toContactName: json['toContactName'] as String?, ); diff --git a/lib/model/uco_transfer_wallet.g.dart b/lib/model/uco_transfer_wallet.g.dart index 6938aba1f..91184fa8b 100644 --- a/lib/model/uco_transfer_wallet.g.dart +++ b/lib/model/uco_transfer_wallet.g.dart @@ -9,7 +9,7 @@ part of 'uco_transfer_wallet.dart'; _$UCOTransferWalletImpl _$$UCOTransferWalletImplFromJson( Map json) => _$UCOTransferWalletImpl( - amount: json['amount'] as int?, + amount: (json['amount'] as num?)?.toInt(), to: json['to'] as String?, toContactName: json['toContactName'] as String?, ); diff --git a/lib/ui/views/nft_creation/layouts/nft_creation_process_sheet.g.dart b/lib/ui/views/nft_creation/layouts/nft_creation_process_sheet.g.dart index 7dce35cd5..e1d33054e 100644 --- a/lib/ui/views/nft_creation/layouts/nft_creation_process_sheet.g.dart +++ b/lib/ui/views/nft_creation/layouts/nft_creation_process_sheet.g.dart @@ -9,7 +9,7 @@ part of 'nft_creation_process_sheet.dart'; _$NftCreationSheetParamsImpl _$$NftCreationSheetParamsImplFromJson( Map json) => _$NftCreationSheetParamsImpl( - currentNftCategoryIndex: json['currentNftCategoryIndex'] as int, + currentNftCategoryIndex: (json['currentNftCategoryIndex'] as num).toInt(), ); Map _$$NftCreationSheetParamsImplToJson( From 2c2f68fce7163243229f1bd2df54f2228e778c50 Mon Sep 17 00:00:00 2001 From: Chralu Date: Tue, 4 Jun 2024 14:41:26 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20Test=20Vault.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .../vault/lib/password_vault_cipher.dart | 8 +- .../datasources/vault/vault.dart | 86 ++++++---- lib/main.dart | 8 +- .../views/authenticate/auto_lock_guard.dart | 4 +- pubspec.lock | 8 + pubspec.yaml | 2 + ...st.dart => vault_secure_storage_test.dart} | 2 +- test/vault_test.dart | 151 ++++++++++++++++++ test/vault_test.mocks.dart | 52 ++++++ 10 files changed, 285 insertions(+), 38 deletions(-) rename test/{secure_storage_test.dart => vault_secure_storage_test.dart} (99%) create mode 100644 test/vault_test.dart create mode 100644 test/vault_test.mocks.dart diff --git a/.gitignore b/.gitignore index 4b7a9528a..eddcb138b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ .svn/ dist/ +# Test +test/tmp_data # Fastlane diff --git a/lib/infrastructure/datasources/vault/lib/password_vault_cipher.dart b/lib/infrastructure/datasources/vault/lib/password_vault_cipher.dart index 8cb5f2b8e..5aff67f54 100644 --- a/lib/infrastructure/datasources/vault/lib/password_vault_cipher.dart +++ b/lib/infrastructure/datasources/vault/lib/password_vault_cipher.dart @@ -2,9 +2,9 @@ part of '../vault.dart'; /// Encryption key is AES encrypted before storage class PasswordVaultCipher implements VaultCipher { - PasswordVaultCipher({required this.password}); + PasswordVaultCipher({required this.passphrase}); - final String password; + final String passphrase; Uint8List? _key; @@ -24,11 +24,11 @@ class PasswordVaultCipher implements VaultCipher { final encryptionKey = await Hive.readEncryptedSecureKey( secureStorage, - password, + passphrase, ) ?? await Hive.generateAndStoreEncryptedSecureKey( secureStorage, - password, + passphrase, ); return encryptionKey; diff --git a/lib/infrastructure/datasources/vault/vault.dart b/lib/infrastructure/datasources/vault/vault.dart index efbe3117a..677fb76f5 100644 --- a/lib/infrastructure/datasources/vault/vault.dart +++ b/lib/infrastructure/datasources/vault/vault.dart @@ -15,20 +15,6 @@ part 'lib/vault.encrypted_securedkey_extension.dart'; part 'lib/vault.raw_securedkey_extension.dart'; abstract class VaultCipher { - factory VaultCipher(String password) { - return kIsWeb - ? PasswordVaultCipher(password: password) - : SimpleVaultCipher(); - } - - static Future get isSetup async { - return kIsWeb ? PasswordVaultCipher.isSetup : SimpleVaultCipher.isSetup; - } - - static Future clear() async { - return kIsWeb ? PasswordVaultCipher.clear() : SimpleVaultCipher.clear(); - } - Future get(); Future updateSecureKey( @@ -36,22 +22,67 @@ abstract class VaultCipher { ); } -typedef VaultPasswordDelegate = Future Function(); +abstract class VaultCipherFactory { + factory VaultCipherFactory() => + kIsWeb ? PasswordVaultCipherFactory() : SimpleVaultCipherFactory(); + + VaultCipher build(String password); + + Future get isSetup; + + Future clear(); +} + +class PasswordVaultCipherFactory implements VaultCipherFactory { + @override + VaultCipher build(String password) => PasswordVaultCipher( + passphrase: password, + ); + + @override + Future get isSetup => PasswordVaultCipher.isSetup; + + @override + Future clear() => PasswordVaultCipher.clear(); +} + +class SimpleVaultCipherFactory implements VaultCipherFactory { + @override + VaultCipher build(String password) => SimpleVaultCipher(); + + @override + Future clear() => SimpleVaultCipher.clear(); + + @override + Future get isSetup => SimpleVaultCipher.isSetup; +} + +typedef VaultPassphraseDelegate = Future Function(); typedef VaultAutolockDelegate = Future Function(); class Vault { - Vault._(); + Vault._({VaultCipherFactory? cipherFactory}) { + _cipherFactory = cipherFactory ?? VaultCipherFactory(); + } - factory Vault.instance() { - Vault._instance ??= Vault._(); + factory Vault.instance({VaultCipherFactory? cipherFactory}) { + Vault._instance ??= Vault._(cipherFactory: cipherFactory); return Vault._instance!; } + @visibleForTesting + static Future reset() async { + Vault._instance = null; + await Hive.deleteFromDisk(); + } + + late final VaultCipherFactory _cipherFactory; + static const _logName = 'Vault'; static Vault? _instance; - VaultPasswordDelegate? passwordDelegate; + VaultPassphraseDelegate? passphraseDelegate; VaultAutolockDelegate? shouldBeLocked; VaultCipher? _vaultCipher; @@ -74,7 +105,7 @@ class Vault { 'Unlocking vault', name: _logName, ); - _vaultCipher = VaultCipher(password); + _vaultCipher = _cipherFactory.build(password); // Ensures we are able to retrieve the encryption key await _vaultCipher!.get(); @@ -85,7 +116,7 @@ class Vault { } Future get isSetup async { - return VaultCipher.isSetup; + return _cipherFactory.isSetup; } Future boxExists(String name) { @@ -109,11 +140,12 @@ class Vault { 'Clearing vault secure key', name: _logName, ); - await VaultCipher.clear(); + await _cipherFactory.clear(); + await lock(); } Future updateSecureKey( - String newPassword, + String passphrase, ) async { log( 'Updating vault secure key', @@ -122,7 +154,7 @@ class Vault { if (_vaultCipher == null) { throw const Failure.locked(); } - await _vaultCipher!.updateSecureKey(newPassword); + await _vaultCipher!.updateSecureKey(passphrase); } Future> openBox( @@ -205,7 +237,7 @@ class Vault { return; } - if (passwordDelegate == null) { + if (passphraseDelegate == null) { throw Exception( 'Vault.passwordDelegate must be set before opening Boxes.', ); @@ -214,8 +246,8 @@ class Vault { 'Requesting user action to unlock', name: _logName, ); - final password = await passwordDelegate!(); + final passphrase = await passphraseDelegate!(); - await unlock(password); + await unlock(passphrase); } } diff --git a/lib/main.dart b/lib/main.dart index 0546afe1f..92d73fc1b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -316,7 +316,7 @@ class SplashState extends ConsumerState with WidgetsBindingObserver { .addListener(removeNativeSplash); } - VaultPasswordDelegate? _passwordDelegate; + VaultPassphraseDelegate? _passwordDelegate; @override void initState() { @@ -337,7 +337,7 @@ class SplashState extends ConsumerState with WidgetsBindingObserver { ), canCancel: false, ); - Vault.instance().passwordDelegate = _passwordDelegate; + Vault.instance().passphraseDelegate = _passwordDelegate; WidgetsBinding.instance.addPostFrameCallback((_) async { await initializeProviders(); @@ -349,8 +349,8 @@ class SplashState extends ConsumerState with WidgetsBindingObserver { void dispose() { /// If some other screen updated the passwordDelegate, /// then we should not reset it. - if (Vault.instance().passwordDelegate == _passwordDelegate) { - Vault.instance().passwordDelegate = null; + if (Vault.instance().passphraseDelegate == _passwordDelegate) { + Vault.instance().passphraseDelegate = null; } super.dispose(); diff --git a/lib/ui/views/authenticate/auto_lock_guard.dart b/lib/ui/views/authenticate/auto_lock_guard.dart index b6ee9c567..04c46d79b 100644 --- a/lib/ui/views/authenticate/auto_lock_guard.dart +++ b/lib/ui/views/authenticate/auto_lock_guard.dart @@ -62,7 +62,7 @@ class _AutoLockGuardState extends ConsumerState WidgetsBinding.instance.addObserver(this); Vault.instance() - ..passwordDelegate = _forceAuthent + ..passphraseDelegate = _forceAuthent ..shouldBeLocked = _shouldBeLocked; } @@ -72,7 +72,7 @@ class _AutoLockGuardState extends ConsumerState WidgetsBinding.instance.removeObserver(this); LockMaskOverlay.instance().hide(); Vault.instance() - ..passwordDelegate = null + ..passphraseDelegate = null ..shouldBeLocked = null; super.dispose(); diff --git a/pubspec.lock b/pubspec.lock index b1ebdc890..198164495 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1374,6 +1374,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" msix: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index c22f35586..280868802 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -293,6 +293,8 @@ dev_dependencies: hive_generator: ^2.0.1 # Automatically generate code for converting to and from JSON by annotating Dart classes. json_serializable: ^6.7.0 + # Mocking library (tests) + mockito: ^5.4.4 # A command-line tool that create Msix installer from your flutter windows-build files. msix: ^3.16.7 # Simple yet powerful Flutter-native UI testing framework eliminating limitations of flutter_test, integration_test, and flutter_driver. diff --git a/test/secure_storage_test.dart b/test/vault_secure_storage_test.dart similarity index 99% rename from test/secure_storage_test.dart rename to test/vault_secure_storage_test.dart index a387b322a..6dd61d8ef 100644 --- a/test/secure_storage_test.dart +++ b/test/vault_secure_storage_test.dart @@ -9,7 +9,7 @@ import 'package:pointycastle/pointycastle.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group( - 'Vault', + 'Vault - SecureStorage', () { setUp(() async { FlutterSecureStorage.setMockInitialValues({}); diff --git a/test/vault_test.dart b/test/vault_test.dart new file mode 100644 index 000000000..a4c4f4183 --- /dev/null +++ b/test/vault_test.dart @@ -0,0 +1,151 @@ +import 'dart:io'; + +import 'package:aewallet/infrastructure/datasources/vault/vault.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:pointycastle/export.dart'; + +@GenerateNiceMocks([MockSpec()]) +import 'vault_test.mocks.dart'; + +class VaultDelegate { + VaultDelegate(); + + Future passwordDelegate() async { + throw UnimplementedError(); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + /// Enforce usage of the [PasswordVaultCipherFactory]. + /// That way, we actually test the vault key encryption. + Vault vault() => Vault.instance(cipherFactory: PasswordVaultCipherFactory()); + + group( + 'Vault', + () { + late MockVaultDelegate vaultDelegate; + setUp(() async { + Hive.init('${Directory.current.path}/test/tmp_data'); + await Vault.reset(); + FlutterSecureStorage.setMockInitialValues({}); + }); + + void _setupUserInputPassphrase(String passphrase) { + vaultDelegate = MockVaultDelegate(); + when(vaultDelegate.passwordDelegate()) + .thenAnswer((_) async => passphrase); + vault().passphraseDelegate = vaultDelegate.passwordDelegate; + } + + test( + 'Should ask for passphrase when creating the vault [box]', + () async { + // GIVEN + _setupUserInputPassphrase('passphrase'); + const boxName = 'abox'; + + // WHEN + final box = await vault().openBox(boxName); + + // THEN + expect(box, const TypeMatcher>()); + verify(vaultDelegate.passwordDelegate()).called(1); + }, + ); + + test( + 'Should ask for passphrase when creating the vault [lazybox]', + () async { + // GIVEN + _setupUserInputPassphrase('passphrase'); + const boxName = 'abox'; + + // WHEN + final box = await vault().openLazyBox(boxName); + + // THEN + expect(box, const TypeMatcher>()); + verify(vaultDelegate.passwordDelegate()).called(1); + }, + ); + + test( + 'Should not ask for passphrase when Vault already unlocked', + () async { + // GIVEN + _setupUserInputPassphrase('passphrase'); + await vault().unlock('passphrase'); + const boxName = 'abox'; + + // WHEN + final box = await vault().openBox(boxName); + + // THEN + expect(box, const TypeMatcher>()); + verifyNever(vaultDelegate.passwordDelegate()); + }, + ); + + test( + 'Should ask for passphrase when Vault locked', + () async { + // GIVEN + _setupUserInputPassphrase('passphrase'); + await vault().unlock('passphrase'); + await vault().lock(); + + // WHEN + final box = await vault().openBox('aBox'); + + // THEN + expect(box, const TypeMatcher>()); + verify(vaultDelegate.passwordDelegate()).called(1); + }, + ); + + test( + 'Should ask for passphrase after clearing secure key', + () async { + // GIVEN + _setupUserInputPassphrase('passphrase'); + await vault().unlock('passphrase'); + await vault().clearSecureKey(); + + // WHEN + final box = await vault().openBox('aBox'); + + // THEN + expect(box, const TypeMatcher>()); + verify(vaultDelegate.passwordDelegate()).called(1); + }, + ); + + test( + 'Should reject data reading with wrong passphrase', + () async { + // GIVEN + _setupUserInputPassphrase('oldPassphrase'); + + const boxName = 'box'; + final box = await vault().openBox(boxName); + await box.put('aKey', 'aValue'); + + await vault().updateSecureKey('newPassphrase'); + await vault().lock(); + + // WHEN + expect( + () => vault().openBox(boxName), + throwsA(isA()), + ); + }, + ); + }, + ); +} diff --git a/test/vault_test.mocks.dart b/test/vault_test.mocks.dart new file mode 100644 index 000000000..f981bee38 --- /dev/null +++ b/test/vault_test.mocks.dart @@ -0,0 +1,52 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in aewallet/test/vault_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i4; + +import 'vault_test.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [VaultDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockVaultDelegate extends _i1.Mock implements _i2.VaultDelegate { + @override + _i3.Future passwordDelegate() => (super.noSuchMethod( + Invocation.method( + #passwordDelegate, + [], + ), + returnValue: _i3.Future.value(_i4.dummyValue( + this, + Invocation.method( + #passwordDelegate, + [], + ), + )), + returnValueForMissingStub: + _i3.Future.value(_i4.dummyValue( + this, + Invocation.method( + #passwordDelegate, + [], + ), + )), + ) as _i3.Future); +}