From 25a54021f0055fa4dad1cea682c08c93e953f776 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:19:44 +0200 Subject: [PATCH 01/11] tests --- android/app/src/main/AndroidManifest.xml | 2 - coverage/lcov.info | 13723 +++++++++++++--- integration_test/copy_to_clipboard_test.dart | 2 +- integration_test/rename_and_delete_test.dart | 2 +- integration_test/two_step_rollout_test.dart | 2 +- integration_test/views_test.dart | 2 +- lib/model/encryption/aes_encrypted.dart | 218 +- lib/model/encryption/token_encryption.dart | 11 +- ...dart => day_password_token_view_mode.dart} | 0 lib/model/states/settings_state.dart | 2 +- lib/model/token_import/token_origin_data.dart | 2 +- lib/model/tokens/day_password_token.dart | 2 +- lib/{utils => model}/version.dart | 0 lib/{utils => model}/version.g.dart | 0 lib/repo/preference_settings_repository.dart | 2 +- .../home_widget_state_notifier.dart | 114 - lib/state_notifiers/settings_notifier.dart | 2 +- lib/utils/app_info_utils.dart | 2 +- lib/utils/globals.dart | 2 +- lib/utils/patch_notes_utils.dart | 2 +- .../day_password_token_widget_tile.dart | 2 +- .../dialog_widgets/patch_notes_dialog.dart | 2 +- .../model/encryption/aes_encrypted_test.dart | 150 + .../encryption/token_encryption_test.dart | 32 + .../model/encryption/uint_8_buffer_test.dart | 0 .../model/enums/algorithms_test.dart | 1 + .../model/enums/app_feature_test.dart | 1 + .../day_passoword_token_view_mode_test.dart | 1 + .../unit_test/model/enums/encodings_test.dart | 1 + .../model/enums/introduction_test.dart | 1 + .../model/enums/patch_note_type_test.dart | 1 + .../enums/push_token_rollout_state_test.dart | 0 .../model/enums/token_import_type_test.dart | 0 .../enums/token_origin_source_type_test.dart | 1 + .../model/enums/token_types_test.dart | 1 + .../extensions/color_extension_test.dart | 1 + .../model/extensions/enum_extension_test.dart | 1 + .../model/extensions/int_extension_test.dart | 1 + .../extensions/theme_mode_extension_test.dart | 1 + .../model/mixins/sortable_mixin_test.dart | 0 .../model/processor_result_test.dart | 0 .../model/push_request_queue_test.dart | 0 test/unit_test/model/push_request_test.dart | 0 .../serializable_RSA_private_key_test.dart | 0 .../serializable_RSA_public_key_test.dart | 0 .../introduction_state_test.dart | 0 .../settings_state_test.dart | 0 .../token_folder_state_test.dart | 0 .../token_state_test.dart | 0 .../day_password_test.dart | 2 +- .../hotp_token_test.dart | 0 .../push_token_test.dart | 0 .../totp_token_test.dart | 0 .../token_import_origin_test.dart | 1 + .../token_import/token_origin_data_test.dart | 1 + test/unit_test/model/version_test.dart | 1 + .../mixins/token_import_processor_test.dart | 1 + .../home_widget_processor_test.dart | 1 + .../home_widget_navigate_processor_test.dart | 1 + ...ation_scheme_processor_interface_test.dart | 1 + .../scheme_processor_interface_test.dart | 1 + .../free_otp_plus_qr_processor_test.dart | 1 + ...oogle_authenticator_qr_processor_test.dart | 1 + .../otp_auth_processor_test.dart | 1 + ...yidea_authenticator_qr_processor_test.dart | 1 + ...mport_scheme_processor_interface_test.dart | 1 + .../aegis_import_file_processor_test.dart | 1 + ...icator_pro_import_file_processor_test.dart | 1 + .../free_otp_plus_file_processor_test.dart | 1 + ...henticator_import_file_processor_test.dart | 1 + ..._import_file_processor_interface_test.dart | 1 + .../two_fas_import_file_processor_test.dart | 1 + ...eference_introduction_repository_test.dart | 1 + .../preference_settings_repository_test.dart | 1 + ...eference_token_folder_repository_test.dart | 1 + .../secure_push_request_repository_test.dart | 1 + .../repo/secure_token_repository_test.dart | 1 + .../completed_introduction_notifier_test.dart | 1 + .../deeplink_notifier_test.dart | 1 + test/unit_test/utils/app_customizer_test.dart | 1 + test/unit_test/utils/app_info_utils_test.dart | 1 + test/unit_test/utils/firebase_utils_test.dart | 1 + test/unit_test/utils/globals_test.dart | 1 + .../utils/home_widget_utils_test.dart | 1 + test/unit_test/utils/identifiers_test.dart | 76 + .../unit_test/utils/image_converter_test.dart | 268 + test/unit_test/utils/license_utils_test.dart | 629 + test/unit_test/utils/lock_auth_test.dart | 1 + test/unit_test/utils/logger_test.dart | 1 + test/unit_test/utils/network_utils_test.dart | 182 + .../utils/patch_notes_utils_test.dart | 1 + test/unit_test/utils/pi_mailer_test.dart | 1 + .../utils/pi_notifications_test.dart | 42 + test/unit_test/utils/push_provider_test.dart | 1 + .../utils/riverpod_providers_test.dart | 1 + .../utils/riverpod_state_listener_test.dart | 1 + .../utils/token_import_origins_test.dart | 1 + test/unit_test/utils/view_utils.dart | 1 + 98 files changed, 12660 insertions(+), 2869 deletions(-) rename lib/model/enums/{day_passoword_token_view_mode.dart => day_password_token_view_mode.dart} (100%) rename lib/{utils => model}/version.dart (100%) rename lib/{utils => model}/version.g.dart (100%) delete mode 100644 lib/state_notifiers/home_widget_state_notifier.dart create mode 100644 test/unit_test/model/encryption/aes_encrypted_test.dart create mode 100644 test/unit_test/model/encryption/token_encryption_test.dart create mode 100644 test/unit_test/model/encryption/uint_8_buffer_test.dart create mode 100644 test/unit_test/model/enums/algorithms_test.dart create mode 100644 test/unit_test/model/enums/app_feature_test.dart create mode 100644 test/unit_test/model/enums/day_passoword_token_view_mode_test.dart create mode 100644 test/unit_test/model/enums/encodings_test.dart create mode 100644 test/unit_test/model/enums/introduction_test.dart create mode 100644 test/unit_test/model/enums/patch_note_type_test.dart create mode 100644 test/unit_test/model/enums/push_token_rollout_state_test.dart create mode 100644 test/unit_test/model/enums/token_import_type_test.dart create mode 100644 test/unit_test/model/enums/token_origin_source_type_test.dart create mode 100644 test/unit_test/model/enums/token_types_test.dart create mode 100644 test/unit_test/model/extensions/color_extension_test.dart create mode 100644 test/unit_test/model/extensions/enum_extension_test.dart create mode 100644 test/unit_test/model/extensions/int_extension_test.dart create mode 100644 test/unit_test/model/extensions/theme_mode_extension_test.dart create mode 100644 test/unit_test/model/mixins/sortable_mixin_test.dart create mode 100644 test/unit_test/model/processor_result_test.dart create mode 100644 test/unit_test/model/push_request_queue_test.dart create mode 100644 test/unit_test/model/push_request_test.dart create mode 100644 test/unit_test/model/serializable_RSA_private_key_test.dart create mode 100644 test/unit_test/model/serializable_RSA_public_key_test.dart rename test/unit_test/model/{states_test => states}/introduction_state_test.dart (100%) rename test/unit_test/model/{states_test => states}/settings_state_test.dart (100%) rename test/unit_test/model/{states_test => states}/token_folder_state_test.dart (100%) rename test/unit_test/model/{states_test => states}/token_state_test.dart (100%) rename test/unit_test/model/{token_test => token}/day_password_test.dart (99%) rename test/unit_test/model/{token_test => token}/hotp_token_test.dart (100%) rename test/unit_test/model/{token_test => token}/push_token_test.dart (100%) rename test/unit_test/model/{token_test => token}/totp_token_test.dart (100%) create mode 100644 test/unit_test/model/token_import/token_import_origin_test.dart create mode 100644 test/unit_test/model/token_import/token_origin_data_test.dart create mode 100644 test/unit_test/model/version_test.dart create mode 100644 test/unit_test/processors/mixins/token_import_processor_test.dart create mode 100644 test/unit_test/processors/scheme_processors/home_widget_processor_test.dart create mode 100644 test/unit_test/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor_test.dart create mode 100644 test/unit_test/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface_test.dart create mode 100644 test/unit_test/processors/scheme_processors/scheme_processor_interface_test.dart create mode 100644 test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart create mode 100644 test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart create mode 100644 test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart create mode 100644 test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart create mode 100644 test/unit_test/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface_test.dart create mode 100644 test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart create mode 100644 test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart create mode 100644 test/unit_test/processors/token_import_file_processor/free_otp_plus_file_processor_test.dart create mode 100644 test/unit_test/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor_test.dart create mode 100644 test/unit_test/processors/token_import_file_processor/token_import_file_processor_interface_test.dart create mode 100644 test/unit_test/processors/token_import_file_processor/two_fas_import_file_processor_test.dart create mode 100644 test/unit_test/repo/preference_introduction_repository_test.dart create mode 100644 test/unit_test/repo/preference_settings_repository_test.dart create mode 100644 test/unit_test/repo/preference_token_folder_repository_test.dart create mode 100644 test/unit_test/repo/secure_push_request_repository_test.dart create mode 100644 test/unit_test/repo/secure_token_repository_test.dart create mode 100644 test/unit_test/state_notifiers/completed_introduction_notifier_test.dart create mode 100644 test/unit_test/state_notifiers/deeplink_notifier_test.dart create mode 100644 test/unit_test/utils/app_customizer_test.dart create mode 100644 test/unit_test/utils/app_info_utils_test.dart create mode 100644 test/unit_test/utils/firebase_utils_test.dart create mode 100644 test/unit_test/utils/globals_test.dart create mode 100644 test/unit_test/utils/home_widget_utils_test.dart create mode 100644 test/unit_test/utils/identifiers_test.dart create mode 100644 test/unit_test/utils/image_converter_test.dart create mode 100644 test/unit_test/utils/license_utils_test.dart create mode 100644 test/unit_test/utils/lock_auth_test.dart create mode 100644 test/unit_test/utils/logger_test.dart create mode 100644 test/unit_test/utils/network_utils_test.dart create mode 100644 test/unit_test/utils/patch_notes_utils_test.dart create mode 100644 test/unit_test/utils/pi_mailer_test.dart create mode 100644 test/unit_test/utils/pi_notifications_test.dart create mode 100644 test/unit_test/utils/push_provider_test.dart create mode 100644 test/unit_test/utils/riverpod_providers_test.dart create mode 100644 test/unit_test/utils/riverpod_state_listener_test.dart create mode 100644 test/unit_test/utils/token_import_origins_test.dart create mode 100644 test/unit_test/utils/view_utils.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b7a7b150b..e784db90e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,8 +9,6 @@ - - defaultKdfSettings = { + 'algorithm': 'PBKDF2', + 'macAlgorithm': 'HmacSHA256', + 'iterations': 100000, + 'bits': 256, + }; static final defaultMacAlgorithm = Hmac.sha256(); static const defaultIterations = 100000; static const defaultBits = 256; @@ -29,9 +38,35 @@ class AesEncrypted { required this.cypher, }); + /// If mac is not provided, it will be extracted from the last 16 bytes of the data + /// This is the default of AES encryption. + factory AesEncrypted({ + required Uint8List data, + required Uint8List salt, + required Uint8List iv, + Mac? mac, + required KdfAlgorithm kdf, + required Cipher cypher, + }) { + if (mac == null) { + mac = Mac(data.sublist(data.length - 16, data.length)); + data = data.sublist(0, data.length - 16); + } + return AesEncrypted._(mac: mac, kdf: kdf, cypher: cypher, salt: salt, iv: iv, data: data); + } + + /// Encrypts the data using AES-GCM with 256 bits. + /// Iterations are set to 100,000 (one hundred thousand). + /// The password is used to derive the key using PBKDF2. + /// When the salt or iv is not provided, it is generated randomly. (16 bytes each) + /// The mac is calculated by the cypher. + /// The data is concatenated with the iv and mac. + /// The result is returned as an AesEncrypted object. static Future encrypt({ required String data, required String password, + Uint8List? salt, + Uint8List? iv, }) async { final plainBytes = utf8.encode(data); final cypher = AesGcm.with256bits(); @@ -40,8 +75,8 @@ class AesEncrypted { iterations: defaultIterations, bits: defaultBits, ); - final salt = Uint8List.fromList(List.generate(16, (index) => index)); - final iv = Uint8List.fromList(List.generate(16, (index) => index)); + salt ??= Uint8List.fromList(List.generate(16, (index) => Random.secure().nextInt(256))); + iv ??= Uint8List.fromList(List.generate(16, (index) => Random.secure().nextInt(256))); final secretKey = await kdf.deriveKeyFromPassword(password: password, nonce: salt); final secretBox = await cypher.encrypt(plainBytes, secretKey: secretKey, nonce: iv); final encryptedData = secretBox.concatenation(nonce: false, mac: false); @@ -55,23 +90,6 @@ class AesEncrypted { ); } - /// If mac is not provided, it will be extracted from the last 16 bytes of the data - /// This is the default of AES encryption. - factory AesEncrypted({ - required Uint8List data, - required Uint8List salt, - required Uint8List iv, - Mac? mac, - required KdfAlgorithm kdf, - required Cipher cypher, - }) { - if (mac == null) { - mac = Mac(data.sublist(data.length - 16, data.length)); - data = data.sublist(0, data.length - 16); - } - return AesEncrypted._(mac: mac, kdf: kdf, cypher: cypher, salt: salt, iv: iv, data: data); - } - Future decrypt(String password) async { final SecretKey secretKey = await _deriveKey(password); final SecretBox secretBox = SecretBox(data, nonce: iv, mac: mac ?? Mac.empty); @@ -94,25 +112,169 @@ class AesEncrypted { salt: base64Decode(json['salt']), iv: base64Decode(json['iv']), mac: Mac(base64Decode(json['mac'])), - kdf: Pbkdf2( - macAlgorithm: defaultMacAlgorithm, - iterations: defaultIterations, - bits: defaultBits, - ), - cypher: AesGcm.with256bits(), + kdf: KdfAlgorithmX.fromJson(json['kdf']), + cypher: CipherX.fromJson(json['cypher']), ); } - static AesEncrypted fromJsonString(String jsonString) => fromJson(jsonDecode(jsonString)); - Map toJson() { return { 'data': base64Encode(data), 'salt': base64Encode(salt), 'iv': base64Encode(iv), 'mac': base64Encode(mac?.bytes ?? Uint8List(0)), + 'kdf': kdf.toJson(), + 'cypher': cypher.toJson(), + }; + } +} + +/* //////////////////////////////////////////////////////////////////////////////////// +///////////////////////////// SERIALIZATION EXTENSIONS //////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// */ + +extension MacAlgorithmX on MacAlgorithm { + static MacAlgorithm fromJson(Map json) { + final algorithm = json['algorithm']; + return switch (algorithm) { + 'Hmac' => HmacX.fromJson(json), + _ => throw UnsupportedError('Unsupported MAC algorithm: $algorithm'), + }; + } + + Map toJson() => switch (runtimeType) { + const (DartHmac) => (this as DartHmac).toJson(), + _ => throw UnsupportedError('Unsupported MAC algorithm: $this'), + }; +} + +extension HmacX on Hmac { + static Hmac fromJson(Map json) { + final hashAlgorithm = HashAlgorithmX.fromJson(json['hashAlgorithm']); + return Hmac(hashAlgorithm); + } + + Map toJson() => { + 'algorithm': 'Hmac', + 'hashAlgorithm': hashAlgorithm.toJson(), + }; +} + +extension HashAlgorithmX on HashAlgorithm { + static HashAlgorithm fromJson(Map json) { + final algorithm = json['algorithm']; + return switch (algorithm) { + 'DartSha256' => const DartSha256(), + 'DartSha512' => const DartSha512(), + _ => throw UnsupportedError('Unsupported hash algorithm: $algorithm'), + }; + } + + Map toJson() => { + 'algorithm': runtimeType.toString(), + }; +} + +extension KdfAlgorithmX on KdfAlgorithm { + static KdfAlgorithm fromJson(Map json) { + final algorithm = json['algorithm']; + return switch (algorithm) { + 'Pbkdf2' => Pbkdf2X.fromJson(json), + _ => throw UnsupportedError('Unsupported KDF algorithm: $algorithm'), }; } - String toJsonString() => jsonEncode(toJson()); + Map toJson() => switch (runtimeType) { + const (DartPbkdf2) => (this as DartPbkdf2).toJson(), + _ => throw UnsupportedError('Unsupported KDF algorithm: $this'), + }; +} + +extension Pbkdf2X on Pbkdf2 { + static Pbkdf2 fromJson(Map json) { + final macAlgorithm = MacAlgorithmX.fromJson(json['macAlgorithm']); + final iterations = json['iterations'] as int; + final bits = json['bits'] as int; + return Pbkdf2( + macAlgorithm: macAlgorithm, + iterations: iterations, + bits: bits, + ); + } + + Map toJson() => { + 'algorithm': 'Pbkdf2', + 'macAlgorithm': macAlgorithm.toJson(), + 'iterations': iterations, + 'bits': bits, + }; +} + +extension CipherX on Cipher { + static Cipher fromJson(Map json) { + final algorithm = json['algorithm']; + return switch (algorithm) { + 'AesGcm' => AesGcmX.fromJson(json), + 'AesCbc' => AesCbcX.fromJson(json), + _ => throw UnsupportedError('Unsupported cipher algorithm: $algorithm'), + }; + } + + Map toJson() => switch (runtimeType) { + const (DartAesGcm) => (this as DartAesGcm).toJson(), + const (DartAesCbc) => (this as DartAesCbc).toJson(), + _ => throw UnsupportedError('Unsupported cipher algorithm: $this'), + }; +} + +extension AesGcmX on AesGcm { + static AesGcm fromJson(Map json) { + final secretKeyLength = json['secretKeyLength']; + return switch (secretKeyLength) { + 16 => AesGcm.with128bits(), + 24 => AesGcm.with192bits(), + 32 => AesGcm.with256bits(), + _ => throw UnsupportedError('Unsupported secret key length: $secretKeyLength'), + }; + } + + Map toJson() { + return { + 'algorithm': 'AesGcm', + 'secretKeyLength': secretKeyLength, + }; + } +} + +extension AesCbcX on AesCbc { + static AesCbc fromJson(Map json) { + final secretKeyLength = json['secretKeyLength']; + final macAlgorithm = json['macAlgorithm'] != null ? MacAlgorithmX.fromJson(json['macAlgorithm']) : Hmac.sha256(); + final paddingAlgorithm = json['paddingAlgorithm'] != null ? PaddingAlgorithmX.fromString(json['paddingAlgorithm']) : PaddingAlgorithm.pkcs7; + return switch (secretKeyLength) { + 16 => AesCbc.with128bits(macAlgorithm: macAlgorithm, paddingAlgorithm: paddingAlgorithm), + 24 => AesCbc.with192bits(macAlgorithm: macAlgorithm, paddingAlgorithm: paddingAlgorithm), + 32 => AesCbc.with256bits(macAlgorithm: macAlgorithm, paddingAlgorithm: paddingAlgorithm), + _ => throw UnsupportedError('Unsupported secret key length: $secretKeyLength'), + }; + } + + Map toJson() { + return { + 'algorithm': 'AesCbc', + 'secretKeyLength': secretKeyLength, + 'macAlgorithm': macAlgorithm.toString(), + 'paddingAlgorithm': paddingAlgorithm.toString(), + }; + } +} + +extension PaddingAlgorithmX on PaddingAlgorithm { + static PaddingAlgorithm fromString(String string) { + return switch (string) { + 'PaddingAlgorithm.pkcs7' => PaddingAlgorithm.pkcs7, + 'PaddingAlgorithm.zero' => PaddingAlgorithm.zero, + _ => throw UnsupportedError('Unsupported padding algorithm: $string') + }; + } } diff --git a/lib/model/encryption/token_encryption.dart b/lib/model/encryption/token_encryption.dart index 973b597a2..1c944641b 100644 --- a/lib/model/encryption/token_encryption.dart +++ b/lib/model/encryption/token_encryption.dart @@ -9,14 +9,15 @@ class TokenEncryption { static Future encrypt({required Iterable tokens, required String password}) async { final jsonsList = tokens.map((e) => e.toJson()).toList(); final encoded = json.encode(jsonsList); - final encrypted = (await AesEncrypted.encrypt(data: encoded, password: password)).toJsonString(); - return encrypted; + final encrypted = (await AesEncrypted.encrypt(data: encoded, password: password)).toJson(); + return jsonEncode(encrypted); } static Future> decrypt({required String encryptedTokens, required String password}) async { - final jsonString = await AesEncrypted.fromJsonString(encryptedTokens).decryptToString(password); - final jsonsList = json.decode(jsonString) as List; - return jsonsList.map((e) => Token.fromJson(e)).toList(); + final json = jsonDecode(encryptedTokens); + final tokenJsonString = await AesEncrypted.fromJson(json).decryptToString(password); + final tokenJsonsList = json.decode(tokenJsonString) as List; + return tokenJsonsList.map((e) => Token.fromJson(e)).toList(); } static Uri generateQrCodeUri({required Token token}) { diff --git a/lib/model/enums/day_passoword_token_view_mode.dart b/lib/model/enums/day_password_token_view_mode.dart similarity index 100% rename from lib/model/enums/day_passoword_token_view_mode.dart rename to lib/model/enums/day_password_token_view_mode.dart diff --git a/lib/model/states/settings_state.dart b/lib/model/states/settings_state.dart index 79ba4273c..1d363e4a4 100644 --- a/lib/model/states/settings_state.dart +++ b/lib/model/states/settings_state.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import '../../l10n/app_localizations.dart'; import '../../utils/identifiers.dart'; -import '../../utils/version.dart'; +import '../version.dart'; /// This class contains all device specific settings. E.g., the language used, whether to show the guide on start, etc. class SettingsState { diff --git a/lib/model/token_import/token_origin_data.dart b/lib/model/token_import/token_origin_data.dart index 7c6d1357e..eeefa7d3c 100644 --- a/lib/model/token_import/token_origin_data.dart +++ b/lib/model/token_import/token_origin_data.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; -import '../../utils/version.dart'; +import '../version.dart'; import '../enums/token_origin_source_type.dart'; part 'token_origin_data.g.dart'; diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart index 44ee5574e..9c29633de 100644 --- a/lib/model/tokens/day_password_token.dart +++ b/lib/model/tokens/day_password_token.dart @@ -4,7 +4,7 @@ import 'package:uuid/uuid.dart'; import '../../utils/identifiers.dart'; import '../enums/algorithms.dart'; -import '../enums/day_passoword_token_view_mode.dart'; +import '../enums/day_password_token_view_mode.dart'; import '../enums/encodings.dart'; import '../enums/token_types.dart'; import '../extensions/enum_extension.dart'; diff --git a/lib/utils/version.dart b/lib/model/version.dart similarity index 100% rename from lib/utils/version.dart rename to lib/model/version.dart diff --git a/lib/utils/version.g.dart b/lib/model/version.g.dart similarity index 100% rename from lib/utils/version.g.dart rename to lib/model/version.g.dart diff --git a/lib/repo/preference_settings_repository.dart b/lib/repo/preference_settings_repository.dart index 00829eda4..16043b027 100644 --- a/lib/repo/preference_settings_repository.dart +++ b/lib/repo/preference_settings_repository.dart @@ -2,7 +2,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../interfaces/repo/settings_repository.dart'; import '../model/states/settings_state.dart'; -import '../utils/version.dart'; +import '../model/version.dart'; class PreferenceSettingsRepository extends SettingsRepository { static const String _isFirstRunKey = 'KEY_IS_FIRST_RUN'; diff --git a/lib/state_notifiers/home_widget_state_notifier.dart b/lib/state_notifiers/home_widget_state_notifier.dart deleted file mode 100644 index dd6014f36..000000000 --- a/lib/state_notifiers/home_widget_state_notifier.dart +++ /dev/null @@ -1,114 +0,0 @@ -// import 'dart:convert'; - -// import 'package:flutter_riverpod/flutter_riverpod.dart'; -// import 'package:mutex/mutex.dart'; -// import 'package:shared_preferences/shared_preferences.dart'; - -// import '../utils/logger.dart'; - -// class HomeWidgetStateNotifier extends StateNotifier { -// final Mutex _m = Mutex(); -// final HomeWidgetStateRepository _repo; - -// HomeWidgetStateNotifier({HomeWidgetState? initState, HomeWidgetStateRepository? repo}) -// : _repo = repo ?? PreferencesHomeWidgetStateRepository(), -// super(initState ?? HomeWidgetState(linkedHomeWidgets: {})); - -// Future saveState(HomeWidgetState state) async { -// await _m.acquire(); -// try { -// final success = await _repo.saveHomeWidgetState(state); -// if (success) { -// state = state; -// } else { -// Logger.warning( -// 'Failed to save HomeWidgetState', -// name: 'HomeWidgetStateNotifier#saveState', -// verbose: true, -// ); -// } -// } finally { -// _m.release(); -// } -// } - -// Future loadState() async { -// await _m.acquire(); -// try { -// final newState = await _repo.loadHomeWidgetState(); -// if (newState != null) { -// state = newState; -// } else { -// Logger.warning( -// 'Failed to load HomeWidgetState', -// name: 'HomeWidgetStateNotifier#loadState', -// verbose: true, -// ); -// } -// } finally { -// _m.release(); -// } -// } - -// void linkHomeWidget(String widgetId, String tokenId) { -// state = HomeWidgetState(linkedHomeWidgets: {...state.linkedHomeWidgets, widgetId: tokenId}); -// } -// } - -// class PreferencesHomeWidgetStateRepository extends HomeWidgetStateRepository { -// static const _prefsKey = 'HOME_WIDGET_STATE'; -// final Future _prefs; - -// PreferencesHomeWidgetStateRepository() : _prefs = SharedPreferences.getInstance(); - -// @override -// Future saveHomeWidgetState(HomeWidgetState state) async { -// try { -// final prefs = await _prefs; -// final encodedState = jsonEncode(state); -// return prefs.setString(_prefsKey, encodedState); -// } catch (e, s) { -// Logger.warning( -// 'Failed to save HomeWidgetState', -// name: 'PreferencesHomeWidgetStateRepository#saveHomeWidgetState', -// error: e, -// stackTrace: s, -// verbose: true, -// ); -// return false; -// } -// } - -// @override -// Future loadHomeWidgetState() async { -// try { -// final prefs = await _prefs; -// final jsonString = prefs.getString(_prefsKey); -// final json = jsonDecode(jsonString!); -// return HomeWidgetState.fromJson(json); -// } catch (e, s) { -// Logger.warning( -// 'Failed to load HomeWidgetState', -// name: 'PreferencesHomeWidgetStateRepository#loadHomeWidgetState', -// error: e, -// stackTrace: s, -// verbose: true, -// ); -// return null; -// } -// } -// } - -// abstract class HomeWidgetStateRepository { -// Future saveHomeWidgetState(HomeWidgetState state); -// Future loadHomeWidgetState(); -// } - -// class HomeWidgetState { -// Map linkedHomeWidgets; - -// HomeWidgetState({required this.linkedHomeWidgets}); - -// Map toJSon() => {'widgetIdToTokenId': linkedHomeWidgets}; -// factory HomeWidgetState.fromJson(Map json) => HomeWidgetState(linkedHomeWidgets: json['widgetIdToTokenId'] as Map); -// } diff --git a/lib/state_notifiers/settings_notifier.dart b/lib/state_notifiers/settings_notifier.dart index fc16358c4..a6a960179 100644 --- a/lib/state_notifiers/settings_notifier.dart +++ b/lib/state_notifiers/settings_notifier.dart @@ -6,7 +6,7 @@ import '../interfaces/repo/settings_repository.dart'; import '../model/states/settings_state.dart'; import '../utils/logger.dart'; import '../utils/push_provider.dart'; -import '../utils/version.dart'; +import '../model/version.dart'; /// This class provies access to the device specific settings. /// It also ensures that the settings are saved to the device. diff --git a/lib/utils/app_info_utils.dart b/lib/utils/app_info_utils.dart index 28747cfb6..ca2bcb1ef 100644 --- a/lib/utils/app_info_utils.dart +++ b/lib/utils/app_info_utils.dart @@ -4,7 +4,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'version.dart'; +import '../model/version.dart'; class AppInfoUtils { static bool isInitialized = false; diff --git a/lib/utils/globals.dart b/lib/utils/globals.dart index b578f1eed..165a807e2 100644 --- a/lib/utils/globals.dart +++ b/lib/utils/globals.dart @@ -24,7 +24,7 @@ import 'package:flutter/material.dart'; import '../l10n/app_localizations.dart'; import '../model/enums/patch_note_type.dart'; -import 'version.dart'; +import '../model/version.dart'; Map>> getLocalizedPatchNotes(AppLocalizations localizations) => { const Version(4, 3, 0): { diff --git a/lib/utils/patch_notes_utils.dart b/lib/utils/patch_notes_utils.dart index ac45040c1..fbf33db0c 100644 --- a/lib/utils/patch_notes_utils.dart +++ b/lib/utils/patch_notes_utils.dart @@ -6,7 +6,7 @@ import '../widgets/dialog_widgets/patch_notes_dialog.dart'; import 'app_info_utils.dart'; import 'globals.dart'; import 'logger.dart'; -import 'version.dart'; +import '../model/version.dart'; class PatchNotesUtils { static Map>> _getNewPatchNotes({required BuildContext context, required Version latestStartedVersion}) { diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart index 6d176fc33..1fa836c5f 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart @@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../../../../../l10n/app_localizations.dart'; -import '../../../../../model/enums/day_passoword_token_view_mode.dart'; +import '../../../../../model/enums/day_password_token_view_mode.dart'; import '../../../../../model/extensions/enum_extension.dart'; import '../../../../../model/tokens/day_password_token.dart'; import '../../../../../utils/riverpod_providers.dart'; diff --git a/lib/widgets/dialog_widgets/patch_notes_dialog.dart b/lib/widgets/dialog_widgets/patch_notes_dialog.dart index 3094c5742..2acbae7c6 100644 --- a/lib/widgets/dialog_widgets/patch_notes_dialog.dart +++ b/lib/widgets/dialog_widgets/patch_notes_dialog.dart @@ -6,7 +6,7 @@ import '../../l10n/app_localizations.dart'; import '../../model/enums/patch_note_type.dart'; import '../../utils/app_info_utils.dart'; import '../../utils/riverpod_providers.dart'; -import '../../utils/version.dart'; +import '../../model/version.dart'; import 'default_dialog.dart'; class PatchNotesDialog extends StatelessWidget { diff --git a/test/unit_test/model/encryption/aes_encrypted_test.dart b/test/unit_test/model/encryption/aes_encrypted_test.dart new file mode 100644 index 000000000..542668294 --- /dev/null +++ b/test/unit_test/model/encryption/aes_encrypted_test.dart @@ -0,0 +1,150 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:cryptography/cryptography.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/encryption/aes_encrypted.dart'; + +void main() { + _testAesEncrypted(); +} + +void _testAesEncrypted() { + group('Aes Encrypted', () { + test('constructor', () { + final AesEncrypted aesEncrypted = AesEncrypted( + data: Uint8List.fromList([41, 142, 95, 156]), + salt: Uint8List.fromList(List.generate(16, (index) => index)), + iv: Uint8List.fromList(List.generate(16, (index) => index)), + mac: const Mac([103, 169, 139, 92, 212, 40, 200, 3, 208, 110, 165, 128, 152, 185, 48, 3]), + kdf: Pbkdf2( + macAlgorithm: AesEncrypted.defaultMacAlgorithm, + iterations: AesEncrypted.defaultIterations, + bits: AesEncrypted.defaultBits, + ), + cypher: AesGcm.with256bits(), + ); + expect(aesEncrypted, isNotNull); + expect(aesEncrypted.data, Uint8List.fromList([41, 142, 95, 156])); + expect(aesEncrypted.salt, Uint8List.fromList(List.generate(16, (index) => index))); + expect(aesEncrypted.iv, Uint8List.fromList(List.generate(16, (index) => index))); + expect( + aesEncrypted.kdf, + Pbkdf2( + macAlgorithm: AesEncrypted.defaultMacAlgorithm, + iterations: AesEncrypted.defaultIterations, + bits: AesEncrypted.defaultBits, + ), + ); + expect(aesEncrypted.cypher, AesGcm.with256bits()); + expect(aesEncrypted.mac, Mac.empty); + }); + test('encrypt', () async { + final AesEncrypted aesEncrypted = await AesEncrypted.encrypt( + data: "test", + password: "password", + salt: Uint8List.fromList(List.generate(16, (index) => index)), + iv: Uint8List.fromList(List.generate(16, (index) => index)), + ); + expect(aesEncrypted, isNotNull); + expect(aesEncrypted.data, Uint8List.fromList([41, 142, 95, 156])); + final decrypted = await aesEncrypted.decrypt("password"); + expect(decrypted, Uint8List.fromList([116, 101, 115, 116])); + final decryptedString = await aesEncrypted.decryptToString("password"); + expect(decryptedString, "test"); + }); + test('decrypt', () async { + final AesEncrypted aesEncrypted = AesEncrypted( + data: Uint8List.fromList([41, 142, 95, 156]), + salt: Uint8List.fromList(List.generate(16, (index) => index)), + iv: Uint8List.fromList(List.generate(16, (index) => index)), + mac: const Mac([103, 169, 139, 92, 212, 40, 200, 3, 208, 110, 165, 128, 152, 185, 48, 3]), + kdf: Pbkdf2( + macAlgorithm: AesEncrypted.defaultMacAlgorithm, + iterations: AesEncrypted.defaultIterations, + bits: AesEncrypted.defaultBits, + ), + cypher: AesGcm.with256bits(), + ); + final decrypted = await aesEncrypted.decrypt("password"); + expect(decrypted, Uint8List.fromList([116, 101, 115, 116])); + }); + test('decryptToString', () async { + final AesEncrypted aesEncrypted = AesEncrypted( + data: Uint8List.fromList([41, 142, 95, 156]), + salt: Uint8List.fromList(List.generate(16, (index) => index)), + iv: Uint8List.fromList(List.generate(16, (index) => index)), + mac: const Mac([103, 169, 139, 92, 212, 40, 200, 3, 208, 110, 165, 128, 152, 185, 48, 3]), + kdf: Pbkdf2( + macAlgorithm: AesEncrypted.defaultMacAlgorithm, + iterations: AesEncrypted.defaultIterations, + bits: AesEncrypted.defaultBits, + ), + cypher: AesGcm.with256bits(), + ); + final decrypted = await aesEncrypted.decryptToString("password"); + final jsonEncoded = jsonEncode(aesEncrypted.toJson()); + print(jsonEncoded); + expect(decrypted, "test"); + }); + test('toJson', () { + final AesEncrypted aesEncrypted = AesEncrypted( + data: Uint8List.fromList([41, 142, 95, 156]), + salt: Uint8List.fromList(List.generate(16, (index) => index)), + iv: Uint8List.fromList(List.generate(16, (index) => index)), + mac: Mac.empty, + kdf: Pbkdf2( + macAlgorithm: AesEncrypted.defaultMacAlgorithm, + iterations: AesEncrypted.defaultIterations, + bits: AesEncrypted.defaultBits, + ), + cypher: AesGcm.with256bits(), + ); + expect( + jsonEncode(aesEncrypted.toJson()), + '{"data":"KY5fnA==","salt":"AAECAwQFBgcICQoLDA0ODw==","iv":"AAECAwQFBgcICQoLDA0ODw==","mac":"","kdf":{"algorithm":"Pbkdf2","macAlgorithm":{"algorithm":"Hmac","hashAlgorithm":{"algorithm":"DartSha256"}},"iterations":100000,"bits":256},"cypher":{"algorithm":"AesGcm","secretKeyLength":32}}', + ); + }); + + test('toJson 2', () { +// TODO: implement test + }); + test('toJson 3', () { +// TODO: implement test + }); + test('toJson 4', () { +// TODO: implement test + }); + + test('fromJson', () async { + final json = { + "data": "KY5fnA==", + "salt": "AAECAwQFBgcICQoLDA0ODw==", + "iv": "AAECAwQFBgcICQoLDA0ODw==", + "mac": "Z6mLXNQoyAPQbqWAmLkwAw==", + "kdf": { + "algorithm": "Pbkdf2", + "macAlgorithm": { + "algorithm": "Hmac", + "hashAlgorithm": {"algorithm": "DartSha256"} + }, + "iterations": 100000, + "bits": 256 + }, + "cypher": {"algorithm": "AesGcm", "secretKeyLength": 32} + }; + final aesEncrypted = AesEncrypted.fromJson(json); + final decrypted = await aesEncrypted.decryptToString("password"); + expect(decrypted, "test"); + }); + }); + test('fromJson 2', () { + // TODO: implement test + }); + test('fromJson 3', () { + // TODO: implement test + }); + test('fromJson 4', () { + // TODO: implement test + }); +} diff --git a/test/unit_test/model/encryption/token_encryption_test.dart b/test/unit_test/model/encryption/token_encryption_test.dart new file mode 100644 index 000000000..341956c5b --- /dev/null +++ b/test/unit_test/model/encryption/token_encryption_test.dart @@ -0,0 +1,32 @@ +import 'package:cryptography/cryptography.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/encryption/token_encryption.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/tokens/day_password_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/steam_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; + +void main() { + _testTokenEncryption(); +} + +void _testTokenEncryption() { + group('Token Encryption', () { + test('encrypt', () async { + final tokensList = [ + HOTPToken(id: 'id1', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret1'), + TOTPToken(period: 30, id: 'id2', algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2'), + SteamToken(period: 30, id: 'id3', algorithm: Algorithms.SHA512, secret: 'secret3'), + DayPasswordToken(period: const Duration(hours: 24), id: 'id4', algorithm: Algorithms.SHA512, digits: 10, secret: 'secret4'), + PushToken(serial: 'serial', id: 'id5'), + ]; + final encrypted = await TokenEncryption.encrypt(tokens: tokensList, password: 'password'); + print(encrypted); + }); + test('decrypt', () {}); + test('generateQrCodeUri', () {}); + test('fromQrCodeUri', () {}); + }); +} diff --git a/test/unit_test/model/encryption/uint_8_buffer_test.dart b/test/unit_test/model/encryption/uint_8_buffer_test.dart new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit_test/model/enums/algorithms_test.dart b/test/unit_test/model/enums/algorithms_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/enums/algorithms_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/enums/app_feature_test.dart b/test/unit_test/model/enums/app_feature_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/enums/app_feature_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/enums/day_passoword_token_view_mode_test.dart b/test/unit_test/model/enums/day_passoword_token_view_mode_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/enums/day_passoword_token_view_mode_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/enums/encodings_test.dart b/test/unit_test/model/enums/encodings_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/enums/encodings_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/enums/introduction_test.dart b/test/unit_test/model/enums/introduction_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/enums/introduction_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/enums/patch_note_type_test.dart b/test/unit_test/model/enums/patch_note_type_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/enums/patch_note_type_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/enums/push_token_rollout_state_test.dart b/test/unit_test/model/enums/push_token_rollout_state_test.dart new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit_test/model/enums/token_import_type_test.dart b/test/unit_test/model/enums/token_import_type_test.dart new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit_test/model/enums/token_origin_source_type_test.dart b/test/unit_test/model/enums/token_origin_source_type_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/enums/token_origin_source_type_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/enums/token_types_test.dart b/test/unit_test/model/enums/token_types_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/enums/token_types_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/extensions/color_extension_test.dart b/test/unit_test/model/extensions/color_extension_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/extensions/color_extension_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/extensions/enum_extension_test.dart b/test/unit_test/model/extensions/enum_extension_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/extensions/enum_extension_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/extensions/int_extension_test.dart b/test/unit_test/model/extensions/int_extension_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/extensions/int_extension_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/extensions/theme_mode_extension_test.dart b/test/unit_test/model/extensions/theme_mode_extension_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/extensions/theme_mode_extension_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/mixins/sortable_mixin_test.dart b/test/unit_test/model/mixins/sortable_mixin_test.dart new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit_test/model/processor_result_test.dart b/test/unit_test/model/processor_result_test.dart new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit_test/model/push_request_queue_test.dart b/test/unit_test/model/push_request_queue_test.dart new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit_test/model/push_request_test.dart b/test/unit_test/model/push_request_test.dart new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit_test/model/serializable_RSA_private_key_test.dart b/test/unit_test/model/serializable_RSA_private_key_test.dart new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit_test/model/serializable_RSA_public_key_test.dart b/test/unit_test/model/serializable_RSA_public_key_test.dart new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit_test/model/states_test/introduction_state_test.dart b/test/unit_test/model/states/introduction_state_test.dart similarity index 100% rename from test/unit_test/model/states_test/introduction_state_test.dart rename to test/unit_test/model/states/introduction_state_test.dart diff --git a/test/unit_test/model/states_test/settings_state_test.dart b/test/unit_test/model/states/settings_state_test.dart similarity index 100% rename from test/unit_test/model/states_test/settings_state_test.dart rename to test/unit_test/model/states/settings_state_test.dart diff --git a/test/unit_test/model/states_test/token_folder_state_test.dart b/test/unit_test/model/states/token_folder_state_test.dart similarity index 100% rename from test/unit_test/model/states_test/token_folder_state_test.dart rename to test/unit_test/model/states/token_folder_state_test.dart diff --git a/test/unit_test/model/states_test/token_state_test.dart b/test/unit_test/model/states/token_state_test.dart similarity index 100% rename from test/unit_test/model/states_test/token_state_test.dart rename to test/unit_test/model/states/token_state_test.dart diff --git a/test/unit_test/model/token_test/day_password_test.dart b/test/unit_test/model/token/day_password_test.dart similarity index 99% rename from test/unit_test/model/token_test/day_password_test.dart rename to test/unit_test/model/token/day_password_test.dart index d77607826..ac1acd263 100644 --- a/test/unit_test/model/token_test/day_password_test.dart +++ b/test/unit_test/model/token/day_password_test.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; -import 'package:privacyidea_authenticator/model/enums/day_passoword_token_view_mode.dart'; +import 'package:privacyidea_authenticator/model/enums/day_password_token_view_mode.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; import 'package:privacyidea_authenticator/model/tokens/day_password_token.dart'; import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; diff --git a/test/unit_test/model/token_test/hotp_token_test.dart b/test/unit_test/model/token/hotp_token_test.dart similarity index 100% rename from test/unit_test/model/token_test/hotp_token_test.dart rename to test/unit_test/model/token/hotp_token_test.dart diff --git a/test/unit_test/model/token_test/push_token_test.dart b/test/unit_test/model/token/push_token_test.dart similarity index 100% rename from test/unit_test/model/token_test/push_token_test.dart rename to test/unit_test/model/token/push_token_test.dart diff --git a/test/unit_test/model/token_test/totp_token_test.dart b/test/unit_test/model/token/totp_token_test.dart similarity index 100% rename from test/unit_test/model/token_test/totp_token_test.dart rename to test/unit_test/model/token/totp_token_test.dart diff --git a/test/unit_test/model/token_import/token_import_origin_test.dart b/test/unit_test/model/token_import/token_import_origin_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/token_import/token_import_origin_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/token_import/token_origin_data_test.dart b/test/unit_test/model/token_import/token_origin_data_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/token_import/token_origin_data_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/model/version_test.dart b/test/unit_test/model/version_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/model/version_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/mixins/token_import_processor_test.dart b/test/unit_test/processors/mixins/token_import_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/mixins/token_import_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/scheme_processors/home_widget_processor_test.dart b/test/unit_test/processors/scheme_processors/home_widget_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/home_widget_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor_test.dart b/test/unit_test/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface_test.dart b/test/unit_test/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/scheme_processors/scheme_processor_interface_test.dart b/test/unit_test/processors/scheme_processors/scheme_processor_interface_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/scheme_processor_interface_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/token_import_file_processor/free_otp_plus_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/free_otp_plus_file_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/free_otp_plus_file_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/token_import_file_processor/token_import_file_processor_interface_test.dart b/test/unit_test/processors/token_import_file_processor/token_import_file_processor_interface_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/token_import_file_processor_interface_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/processors/token_import_file_processor/two_fas_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/two_fas_import_file_processor_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/two_fas_import_file_processor_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/repo/preference_introduction_repository_test.dart b/test/unit_test/repo/preference_introduction_repository_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/repo/preference_introduction_repository_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/repo/preference_settings_repository_test.dart b/test/unit_test/repo/preference_settings_repository_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/repo/preference_settings_repository_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/repo/preference_token_folder_repository_test.dart b/test/unit_test/repo/preference_token_folder_repository_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/repo/preference_token_folder_repository_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/repo/secure_push_request_repository_test.dart b/test/unit_test/repo/secure_push_request_repository_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/repo/secure_push_request_repository_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/repo/secure_token_repository_test.dart b/test/unit_test/repo/secure_token_repository_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/repo/secure_token_repository_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/state_notifiers/completed_introduction_notifier_test.dart b/test/unit_test/state_notifiers/completed_introduction_notifier_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/state_notifiers/completed_introduction_notifier_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/state_notifiers/deeplink_notifier_test.dart b/test/unit_test/state_notifiers/deeplink_notifier_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/state_notifiers/deeplink_notifier_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/app_customizer_test.dart b/test/unit_test/utils/app_customizer_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/app_customizer_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/app_info_utils_test.dart b/test/unit_test/utils/app_info_utils_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/app_info_utils_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/firebase_utils_test.dart b/test/unit_test/utils/firebase_utils_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/firebase_utils_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/globals_test.dart b/test/unit_test/utils/globals_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/globals_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/home_widget_utils_test.dart b/test/unit_test/utils/home_widget_utils_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/home_widget_utils_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/identifiers_test.dart b/test/unit_test/utils/identifiers_test.dart new file mode 100644 index 000000000..e56679f64 --- /dev/null +++ b/test/unit_test/utils/identifiers_test.dart @@ -0,0 +1,76 @@ +// ignore_for_file: constant_identifier_names + +/* + privacyIDEA Authenticator + + Authors: Timo Sturm + Frank Merkel + Copyright (c) 2017-2023 NetKnights GmbH + + Licensed under the Apache License, Version 2.0 (the 'License'); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an 'AS IS' BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// default email address for crash reports + +const defaultCrashReportRecipient = 'app-crash@netknights.it'; + +// qr codes: +const String URI_TYPE = 'URI_TYPE'; +const String URI_LABEL = 'URI_LABEL'; +const String URI_ALGORITHM = 'URI_ALGORITHM'; +const String URI_DIGITS = 'URI_DIGITS'; +const String URI_SECRET = 'URI_SECRET'; // Should be base32 encoded +const String URI_COUNTER = 'URI_COUNTER'; +const String URI_PERIOD = 'URI_PERIOD'; +const String URI_ISSUER = 'URI_ISSUER'; +const String URI_PIN = 'URI_PIN'; +const String URI_IMAGE = 'URI_IMAGE'; +const String URI_ORIGIN = 'URI_ORIGIN'; + +// 2 step: +const String URI_SALT_LENGTH = 'URI_SALT_LENGTH'; +const String URI_OUTPUT_LENGTH_IN_BYTES = 'URI_OUTPUT_LENGTH_IN_BYTES'; +const String URI_ITERATIONS = 'URI_ITERATIONS'; + +// push token: +const String URI_SERIAL = 'URI_SERIAL'; +const String URI_ROLLOUT_URL = 'URI_ROLLOUT_URL'; +const String URI_TTL = 'URI_TTL'; +const String URI_ENROLLMENT_CREDENTIAL = 'URI_ENROLLMENT_CREDENTIAL'; +const String URI_SSL_VERIFY = 'URI_SSL_VERIFY'; + +// Crypto stuff: +const String SIGNING_ALGORITHM = 'SHA-256/RSA'; + +// Custom error identifiers +const String FIREBASE_TOKEN_ERROR_CODE = 'FIREBASE_TOKEN_ERROR_CODE'; + +// Push request: +const String PUSH_REQUEST_NONCE = 'nonce'; // 1. +const String PUSH_REQUEST_URL = 'url'; // 2. +const String PUSH_REQUEST_SERIAL = 'serial'; // 3. +const String PUSH_REQUEST_QUESTION = 'question'; // 4. +const String PUSH_REQUEST_TITLE = 'title'; // 5. +const String PUSH_REQUEST_SSL_VERIFY = 'sslverify'; // 6. +const String PUSH_REQUEST_SIGNATURE = 'signature'; // 7. + +const String GLOBAL_SECURE_REPO_PREFIX = 'app_v3_'; + +bool validateMap(Map map, List keys) { + for (String key in keys) { + if (!map.containsKey(key)) { + return false; + } + } + return true; +} diff --git a/test/unit_test/utils/image_converter_test.dart b/test/unit_test/utils/image_converter_test.dart new file mode 100644 index 000000000..2639ea5c5 --- /dev/null +++ b/test/unit_test/utils/image_converter_test.dart @@ -0,0 +1,268 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:image/image.dart' as imglib; + +class ImageConverter { + final imglib.Image image; + final Size size; + + ImageConverter({ + required this.image, + }) : size = Size(image.width.toDouble(), image.height.toDouble()); + + factory ImageConverter.fromCameraImage(CameraImage image, int rotation, + {bool isFrontCamera = false, int? chropLeft, int? chropRight, int? chropTop, int? chropBottom}) { + return switch (image.format.group) { + ImageFormatGroup.yuv420 => ImageConverter._fromYUV420(image, rotation, isFrontCamera, chropLeft ?? 0, chropRight ?? 0, chropTop ?? 0, chropBottom ?? 0), + ImageFormatGroup.bgra8888 => + ImageConverter._fromBGRA8888(image, rotation, isFrontCamera, chropLeft ?? 0, chropRight ?? 0, chropTop ?? 0, chropBottom ?? 0), + ImageFormatGroup.jpeg => ImageConverter._fromJPEG(image), + ImageFormatGroup.nv21 => ImageConverter._fromNV21(image), + ImageFormatGroup.unknown => throw ArgumentError('Unknown image format'), + }; + } + + factory ImageConverter._fromNV21(CameraImage image) { + final width = image.width.toInt(); + final height = image.height.toInt(); + Uint8List yuv420sp = image.planes[0].bytes; + final convertedImage = imglib.Image(height: height, width: width); + final int frameSize = width * height; + + for (int j = 0, yp = 0; j < height; j++) { + int uvp = frameSize + (j >> 1) * width, u = 0, v = 0; + for (int i = 0; i < width; i++, yp++) { + int y = (0xff & yuv420sp[yp]) - 16; + if (y < 0) y = 0; + if ((i & 1) == 0) { + v = (0xff & yuv420sp[uvp++]) - 128; + u = (0xff & yuv420sp[uvp++]) - 128; + } + int y1192 = 1192 * y; + int r = (y1192 + 1634 * v).clamp(0, 262143); + int g = (y1192 - 833 * v - 400 * u).clamp(0, 262143); + int b = (y1192 + 2066 * u).clamp(0, 262143); + + // getting their 8-bit values. + convertedImage.setPixelRgba( + i, + j, + ((r << 6) & 0xff0000) >> 16, + ((g >> 2) & 0xff00) >> 8, + (b >> 10) & 0xff, + 0xff, + ); + } + } + + return ImageConverter( + image: convertedImage, + ); + } + + factory ImageConverter._fromJPEG(CameraImage image) { + return ImageConverter(image: imglib.decodeJpg(image.planes[0].bytes)!); + } + + factory ImageConverter._fromBGRA8888(CameraImage image, int rotation, bool mirror, int cropLeft, int cropRight, int cropTop, int cropBottom) { + rotation = 360 - (rotation % 360); // if the image is rotated by 90, we need to rotate by another 270 to get the correct rotation (0/360) + const numChannels = 4; // 1 for alpha, 3 for RGB + var img = imglib.Image.fromBytes( + width: image.width, + height: image.height, + rowStride: image.planes[0].bytesPerRow, + numChannels: numChannels, + bytesOffset: numChannels * 7, // i don't know why 7 pixels, but it works + bytes: (image.planes[0].bytes).buffer, + ); + img = imglib.copyRotate(img, angle: rotation); + if (mirror) { + img = imglib.flip(img, direction: imglib.FlipDirection.horizontal); + } + img = imglib.copyCrop( + img, + x: cropLeft, + y: cropTop, + width: img.width - cropLeft - cropRight, + height: img.height - cropTop - cropBottom, + ); + return ImageConverter(image: img); + } + + factory ImageConverter._fromYUV420( + CameraImage image, + int rotation, + bool mirror, [ + int chropLeft = 0, + int chropRight = 0, + int chropTop = 0, + int chropBottom = 0, + ]) { + rotation = 360 - (rotation % 360); // if the rotation is 90, we need to rotate by 270 to get the correct rotation + + const alpha = 0xFF; + final height = image.height; + final width = image.width; + final yPlane = image.planes[0]; + final uPlane = image.planes[1]; + final vPlane = image.planes[2]; + + final int outputWidth; + final int outputHeight; + final int rotatedChropLeft; + final int rotatedChropRight; + final int rotatedChropTop; + final int rotatedChropBottom; + + final int uvRowStride = uPlane.bytesPerRow; + final int uvPixelStride = uPlane.bytesPerPixel!; + Function(int x, int y) getNewX; + Function(int x, int y) getNewY; + + switch (rotation) { + case 90: + outputWidth = height; + outputHeight = width; + if (mirror) { + // rotate by 90 and flip horizontally + getNewX = (x, y) => height - y - 1; + getNewY = (x, y) => width - x - 1; + rotatedChropRight = chropBottom; + rotatedChropBottom = chropRight; + rotatedChropLeft = chropTop; + rotatedChropTop = chropLeft; + } else { + getNewX = (x, y) => y; + getNewY = (x, y) => width - x - 1; + rotatedChropRight = chropTop; + rotatedChropBottom = chropRight; + rotatedChropLeft = chropBottom; + rotatedChropTop = chropLeft; + } + break; + case 180: + outputWidth = width; + outputHeight = height; + if (mirror) { + // rotate by 180 and flip horizontally + getNewX = (x, y) => x; + getNewY = (x, y) => height - y - 1; + + rotatedChropBottom = chropTop; + rotatedChropLeft = chropLeft; + rotatedChropTop = chropBottom; + rotatedChropRight = chropRight; + } else { + getNewX = (x, y) => width - x - 1; + getNewY = (x, y) => height - y - 1; + rotatedChropBottom = chropTop; + rotatedChropLeft = chropRight; + rotatedChropTop = chropBottom; + rotatedChropRight = chropLeft; + } + break; + case 270: + outputWidth = height; + outputHeight = width; + if (mirror) { + // rotate by 270 and flip horizontally + getNewX = (x, y) => y; + getNewY = (x, y) => height - x; + + rotatedChropLeft = chropBottom; + rotatedChropTop = chropRight; + rotatedChropRight = chropTop; + rotatedChropBottom = chropLeft; + } else { + getNewX = (x, y) => height - y - 1; + getNewY = (x, y) => x; + rotatedChropLeft = chropTop; + rotatedChropTop = chropRight; + rotatedChropRight = chropBottom; + rotatedChropBottom = chropLeft; + } + break; + + default: + outputWidth = width; + outputHeight = height; + if (mirror) { + // flip horizontally + getNewX = (x, y) => x; + getNewY = (x, y) => height - y - 1; + rotatedChropTop = chropTop; + rotatedChropRight = chropLeft; + rotatedChropBottom = chropBottom; + rotatedChropLeft = chropRight; + } else { + getNewX = (x, y) => x; + getNewY = (x, y) => y; + rotatedChropTop = chropTop; + rotatedChropRight = chropRight; + rotatedChropBottom = chropBottom; + rotatedChropLeft = chropLeft; + } + break; + } + + // imgLib -> Image package from https://pub.dartlang.org/packages/image + var img = imglib.Image(width: outputWidth, height: outputHeight); // Create Image buffer + + // Fill image buffer with plane[0] from YUV420_888 + + for (int y = chropTop; y < height - chropBottom; y++) { + for (int x = chropLeft; x < width - chropRight; x++) { + // if (x % 100 == 0) log("x: $x, y: $y"); + final int uvIndex = uvPixelStride * (x / 2).floor() + uvRowStride * (y / 2).floor(); + final int index = (y * width + x); + + final yp = yPlane.bytes[index]; + final up = uPlane.bytes[uvIndex]; + final vp = vPlane.bytes[uvIndex]; + // Calculate pixel color + + final int r = (yp + vp * 1436 / 1024 - 179).round().clamp(0, 255); + final int g = (yp - up * 46549 / 131072 + 44 - vp * 93604 / 131072 + 91).round().clamp(0, 255); + final int b = (yp + up * 1814 / 1024 - 227).round().clamp(0, 255); + // color: 0x FF FF FF FF + // A B G R + final newX = getNewX(x, y); + final newY = getNewY(x, y); + + if ((img.isBoundsSafe(newX, newY))) { + img.setPixelRgba(newX, newY, r, g, b, alpha); + } + } + } + final chropedImg = imglib.copyCrop( + img, + x: rotatedChropLeft, + y: rotatedChropTop, + width: img.width - rotatedChropLeft - rotatedChropRight, + height: img.height - rotatedChropTop - rotatedChropBottom, + ); + return ImageConverter(image: chropedImg); + } + + factory ImageConverter.fromFile(String path) { + final img = imglib.decodeImage(File(path).readAsBytesSync())!; + return ImageConverter(image: img); + } + + factory ImageConverter.fromBytes(Uint8List bytes) { + final img = imglib.decodeImage(bytes)!; + return ImageConverter(image: img); + } + + Uint8List toBytes() { + return Uint8List.fromList(imglib.encodePng(image)); + } + + imglib.Image toImage() { + return image; + } +} diff --git a/test/unit_test/utils/license_utils_test.dart b/test/unit_test/utils/license_utils_test.dart new file mode 100644 index 000000000..684f134ca --- /dev/null +++ b/test/unit_test/utils/license_utils_test.dart @@ -0,0 +1,629 @@ +// ignore_for_file: constant_identifier_names + +/* + privacyIDEA Authenticator + + Authors: Timo Sturm + Frank Merkel + Copyright (c) 2017-2023 NetKnights GmbH + + Licensed under the Apache License, Version 2.0 (the 'License'); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an 'AS IS' BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import 'package:flutter/foundation.dart'; + +/// This method removes all licenses from the LicenseRegistry. +/// It can be used for testing purposes, if one wishes to inspect a specifically +/// added license. +clearLicenses() { + // ignore: invalid_use_of_visible_for_testing_member + LicenseRegistry.reset(); +} + +addAllLicenses() { + _addNewLicense('privacyIDEA Authenticator', _PI_AUTHENTICATOR_LICENSE); + _addNewLicense('dart-hex', _DART_HEX_LICENSE); + _addNewLicense('dart-base32', _DART_BASE32_LICENSE); + _addNewLicense('otp', _DART_OTP_LICENSE); + _addNewLicense('dart-uuid', _DART_UUID_LICENSE); + _addNewLicense('json_serializabel', _JSON_SERIALIZABLE_LICENSE); + _addNewLicense('flutter_secure_storage', _FLUTTER_SECURE_STORAGE_LICENSE); + _addNewLicense('flutter_slidable', _FLUTTER_SLIDABLE_LICENSE); + _addNewLicense('intl', _INTL_LICENSE); + _addNewLicense('package_info', _PACKAGE_INFO_LICENSE); + _addNewLicense('pointycastle', _POINTYCASTLE_LICENSE); + _addNewLicense('dynamic_theme', _DYNAMIC_THEME_LICENSE); + _addNewLicense('flutterfire', _FLUTTERFIRE_LICENSE); + _addNewLicense('firebase_core', _FIREBASE_CORE_LICENSE); + _addNewLicense('asn1lib', _ASN1LIB_LICENSE); + _addNewLicense('http', _HTTP_LICENSE); + _addNewLicense('flutter_local_notifications#', _FLUTTER_LOCAL_NOTIFICATIONS); + _addNewLicense('dart-mutex', _DART_MUTEX_LICENSE); +} + +_addNewLicense(String packageName, String licenseText) { + LicenseRegistry.addLicense(() async* { + yield LicenseEntryWithLineBreaks([packageName], licenseText); + }); +} + +const String _DART_MUTEX_LICENSE = ''' +Copyright (c) 2016, Hoylen Sue. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + '''; + +const String _FLUTTER_LOCAL_NOTIFICATIONS = ''' +Copyright 2018 Michael Bui. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + +const String _HTTP_LICENSE = ''' +Copyright 2014, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + +const String _ASN1LIB_LICENSE = ''' +http://opensource.org/licenses/BSD-3-Clause +Copyright (c) 2015, Warren Strange +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + +const String _FIREBASE_CORE_LICENSE = ''' +// Copyright 2017 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; +const String _FLUTTERFIRE_LICENSE = ''' +Copyright 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; +const String _DYNAMIC_THEME_LICENSE = ''' +MIT License + +Copyright (c) 2019 Norbert Kozsir + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the 'Software'), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +'''; +const String _PI_AUTHENTICATOR_LICENSE = ''' + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + 'License' shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + 'Licensor' shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + 'Legal Entity' shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + 'control' means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + 'You' (or 'Your') shall mean an individual or Legal Entity + exercising permissions granted by this License. + + 'Source' form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + 'Object' form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + 'Work' shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + 'Derivative Works' shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + 'Contribution' shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, 'submitted' + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as 'Not a Contribution.' + + 'Contributor' shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a 'NOTICE' text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an 'AS IS' BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS'''; +const String _DART_HEX_LICENSE = ''' + +The MIT License (MIT) + +Copyright (c) 2016 Dartcoin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the 'Software'), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +'''; +const String _DART_BASE32_LICENSE = ''' Copyright (c) 2012 Yulian Kuncheff + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.'''; +const String _DART_OTP_LICENSE = ''' Copyright (c) 2012 Yulian Kuncheff + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.'''; +const String _DART_UUID_LICENSE = ''' Copyright (c) 2012 Yulian Kuncheff + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.'''; +const String _JSON_SERIALIZABLE_LICENSE = ''' +Copyright 2017, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''; +const String _FLUTTER_SECURE_STORAGE_LICENSE = ''' +// Copyright 2017 Your Company. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Your Company nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''; +const String _FLUTTER_SLIDABLE_LICENSE = ''' +MIT License + +Copyright (c) 2018 Romain Rastel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the 'Software'), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.'''; +const String _INTL_LICENSE = ''' +Copyright 2013, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; +const String _PACKAGE_INFO_LICENSE = ''' Copyright 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''; +const String _POINTYCASTLE_LICENSE = ''' +Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +'''; diff --git a/test/unit_test/utils/lock_auth_test.dart b/test/unit_test/utils/lock_auth_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/lock_auth_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/logger_test.dart b/test/unit_test/utils/logger_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/logger_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/network_utils_test.dart b/test/unit_test/utils/network_utils_test.dart new file mode 100644 index 000000000..9d63c4103 --- /dev/null +++ b/test/unit_test/utils/network_utils_test.dart @@ -0,0 +1,182 @@ +/* + privacyIDEA Authenticator + + Authors: Timo Sturm + Frank Merkel + Copyright (c) 2017-2023 NetKnights GmbH + + Licensed under the Apache License, Version 2.0 (the 'License'); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an 'AS IS' BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; +import 'package:privacyidea_authenticator/utils/globals.dart'; +import 'package:privacyidea_authenticator/utils/logger.dart'; +import 'package:privacyidea_authenticator/utils/riverpod_providers.dart'; +import 'package:privacyidea_authenticator/utils/view_utils.dart'; + +class PrivacyIdeaIOClient { + const PrivacyIdeaIOClient(); + + /// Dummy network request can be used to trigger the network access permission + /// on iOS devices. Doing this at an appropriate place in the code can prevent + /// SocketExceptions. + Future triggerNetworkAccessPermission({required Uri url, bool sslVerify = true, bool isRetry = false}) async { + if (kIsWeb) return false; + HttpClient httpClient = HttpClient(); + httpClient.badCertificateCallback = ((X509Certificate cert, String host, int port) => !sslVerify); + httpClient.userAgent = 'privacyIDEA-App' + '/${(await PackageInfo.fromPlatform()).version}' + ' ${Platform.operatingSystem}' + '/${Platform.operatingSystemVersion}'; + + IOClient ioClient = IOClient(httpClient); + + try { + await ioClient.post(url, body: '').timeout(const Duration(seconds: 15)); + } on ClientException { + Logger.warning('ClientException', name: 'utils.dart#triggerNetworkAccessPermission'); + ioClient.close(); + if (globalNavigatorKey.currentState?.context == null) return false; + globalRef?.read(statusMessageProvider.notifier).state = ( + AppLocalizations.of(await globalContext)!.connectionFailed, + AppLocalizations.of(await globalContext)!.checkYourNetwork, + ); + return false; + } catch (e, _) { + if (e is! SocketException && e is! TimeoutException) { + rethrow; + } + if (isRetry) { + Logger.warning('SocketException while retrying', name: 'utils.dart#triggerNetworkAccessPermission'); + if (globalNavigatorKey.currentState?.context != null) { + globalRef?.read(statusMessageProvider.notifier).state = ( + AppLocalizations.of(await globalContext)!.connectionFailed, + AppLocalizations.of(await globalContext)!.checkYourNetwork, + ); + } + ioClient.close(); + return false; + } + ioClient.close(); + return Future.delayed( + const Duration(seconds: 10), + () => triggerNetworkAccessPermission(url: url, sslVerify: sslVerify, isRetry: true), + ); + } finally { + ioClient.close(); + } + return true; + } + + /// Custom POST request allows to not verify certificates. + Future doPost({required Uri url, required Map body, bool sslVerify = true}) async { + if (kIsWeb) return Response('Platform not supported', 405); + Logger.info('Sending post request (SSLVerify: $sslVerify)', name: 'utils.dart#doPost'); + + List entries = body.entries.where((element) => element.value == null).toList(); + if (entries.isNotEmpty) { + List nullEntries = []; + for (MapEntry entry in entries) { + nullEntries.add(entry.key); + } + throw ArgumentError('Can not send request because the argument [body] contains a null values' + ' at entries $nullEntries, this is not permitted.'); + } + + HttpClient httpClient = HttpClient(); + httpClient.badCertificateCallback = ((_, __, ___) => !sslVerify); + httpClient.userAgent = 'privacyIDEA-App' + '/${(await PackageInfo.fromPlatform()).version}' + ' ${Platform.operatingSystem}' + '/${Platform.operatingSystemVersion}'; + + IOClient ioClient = IOClient(httpClient); + + Response response; + try { + response = await ioClient.post(url, body: body).timeout(const Duration(seconds: 15)); + } on HandshakeException catch (e, s) { + response = Response('${e.runtimeType} : $s', 525); + } catch (e, s) { + if (e is! TimeoutException && e is! SocketException) rethrow; + response = Response('${e.runtimeType} : $s', 404); + } + + if (response.statusCode != 200) { + Logger.warning( + 'Received unexpected response', + name: 'utils.dart#doPost', + error: 'Status code: ${response.statusCode}' '\nPosted body: $body' '\nResponse: ${response.body}\n', + ); + } + ioClient.close(); + + return response; + } + + Future doGet({required Uri url, required Map parameters, bool sslVerify = true}) async { + if (kIsWeb) return Response('', 405); + Logger.info('Sending get request (SSLVerify: $sslVerify)', name: 'utils.dart#doGet'); + List entries = parameters.entries.where((element) => element.value == null).toList(); + if (entries.isNotEmpty) { + List nullEntries = []; + for (MapEntry entry in entries) { + nullEntries.add(entry.key); + } + throw ArgumentError("Can not send request because the argument [parameters] contains " + "null values at entries $nullEntries, this is not permitted."); + } + + HttpClient httpClient = HttpClient(); + httpClient.badCertificateCallback = ((X509Certificate cert, String host, int port) => !sslVerify); + httpClient.userAgent = 'privacyIDEA-App /' + ' ${Platform.operatingSystem}' + ' ${(await PackageInfo.fromPlatform()).version}'; + + IOClient ioClient = IOClient(httpClient); + + StringBuffer buffer = StringBuffer(url); + + if (parameters.isNotEmpty) { + buffer.write('?'); + buffer.writeAll(parameters.entries.map((e) => '${e.key}=${e.value}'), '&'); + } + + Response response; + Uri uri = Uri.parse(buffer.toString()); + try { + response = await ioClient.get(uri).timeout(const Duration(seconds: 15)); + } on HandshakeException catch (e, s) { + Logger.warning('Handshake failed. sslVerify: $sslVerify', name: 'utils.dart#doGet', error: e, stackTrace: s); + showMessage(message: 'Handshake failed, please check the server certificate and try again.'); + rethrow; + } catch (e, s) { + if (e is! TimeoutException && e is! SocketException) rethrow; + response = Response('${e.runtimeType} : $s', 404); + } + + if (response.statusCode != 200) { + Logger.warning('Received unexpected response: ${response.statusCode}', name: 'utils.dart#doGet'); + } + + ioClient.close(); + return response; + } +} diff --git a/test/unit_test/utils/patch_notes_utils_test.dart b/test/unit_test/utils/patch_notes_utils_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/patch_notes_utils_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/pi_mailer_test.dart b/test/unit_test/utils/pi_mailer_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/pi_mailer_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/pi_notifications_test.dart b/test/unit_test/utils/pi_notifications_test.dart new file mode 100644 index 000000000..8a98113a7 --- /dev/null +++ b/test/unit_test/utils/pi_notifications_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class PiNotifications { + static PiNotifications? _instance; + int id = 0; + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + late NotificationDetails notificationDetails; + + PiNotifications._(); + + static Future show(String title, String body) async => (await _getInstance)._show(title, body); + + static Future get _getInstance async { + if (_instance == null) { + _instance = PiNotifications._(); + await _instance!._initialize(); + } + return _instance!; + } + + Future _initialize() async { + var initializationSettingsAndroid = const AndroidInitializationSettings('@mipmap/ic_launcher'); // <- default icon name is @mipmap/ic_launcher + // var initializationSettingsIOS = IOSInitializationSettings(onDidReceiveLocalNotification: onDidReceiveLocalNotification); + var initializationSettings = InitializationSettings(android: initializationSettingsAndroid); + flutterLocalNotificationsPlugin.initialize(initializationSettings); + AndroidNotificationDetails androidNotificationDetails = const AndroidNotificationDetails( + 'PiNotifications', + 'PiNotifications', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker', + ); + + notificationDetails = NotificationDetails(android: androidNotificationDetails); + } + + Future _show(String title, String body) async { + final id = this.id++; + await flutterLocalNotificationsPlugin.show(id, title, body, notificationDetails); + return id; + } +} diff --git a/test/unit_test/utils/push_provider_test.dart b/test/unit_test/utils/push_provider_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/push_provider_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/riverpod_providers_test.dart b/test/unit_test/utils/riverpod_providers_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/riverpod_providers_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/riverpod_state_listener_test.dart b/test/unit_test/utils/riverpod_state_listener_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/riverpod_state_listener_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/token_import_origins_test.dart b/test/unit_test/utils/token_import_origins_test.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/token_import_origins_test.dart @@ -0,0 +1 @@ + diff --git a/test/unit_test/utils/view_utils.dart b/test/unit_test/utils/view_utils.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/unit_test/utils/view_utils.dart @@ -0,0 +1 @@ + From a9ca139dd7d86724cff2edb03dbd3ab79acf3b56 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:28:53 +0200 Subject: [PATCH 02/11] added more tests --- lib/model/encryption/token_encryption.dart | 4 +- lib/model/encryption/uint_8_buffer.dart | 21 ++- lib/model/enums/algorithms.dart | 105 ------------ lib/model/enums/app_feature.dart | 17 -- lib/model/enums/encodings.dart | 75 -------- lib/model/enums/introduction.dart | 21 +-- lib/model/enums/patch_note_type.dart | 2 +- lib/model/enums/token_import_type.dart | 3 +- lib/model/extensions/enum_extension.dart | 13 +- .../enums/algorithms_extension.dart | 33 ++++ .../extensions/enums/encodings_extension.dart | 57 ++++++ lib/model/states/introduction_state.g.dart | 2 +- lib/model/tokens/day_password_token.dart | 11 +- lib/model/tokens/hotp_token.dart | 9 +- lib/model/tokens/push_token.dart | 3 +- lib/model/tokens/steam_token.dart | 9 +- lib/model/tokens/steam_token.g.dart | 2 + lib/model/tokens/token.dart | 20 +-- lib/model/tokens/totp_token.dart | 7 +- .../otp_auth_processor.dart | 12 +- .../aegis_import_file_processor.dart | 3 +- ...thenticator_pro_import_file_processor.dart | 14 +- .../free_otp_plus_file_processor.dart | 2 +- .../two_fas_import_file_processor.dart | 3 +- lib/utils/app_customizer.dart | 2 +- lib/utils/crypto_utils.dart | 1 + lib/utils/errors.dart | 53 ++++++ .../add_token_manually_view.dart | 6 +- .../labeled_dropdown_button.dart | 4 +- .../pages/import_start_page.dart | 2 +- .../main_view_navigation_bar.dart | 4 +- .../edit_day_password_token_action.dart | 3 +- .../day_password_token_widget_tile.dart | 3 +- .../actions/edit_hotp_token_action.dart | 3 +- .../hotp_token_widget_tile.dart | 3 +- .../actions/edit_totp_token_action.dart | 3 +- .../totp_token_widget_tile.dart | 3 +- .../dialog_widgets/patch_notes_dialog.dart | 2 +- .../encryption/token_encryption_test.dart | 162 +++++++++++++++++- .../model/encryption/uint_8_buffer_test.dart | 108 ++++++++++++ .../model/enums/app_feature_test.dart | 16 ++ .../model/states/introduction_state_test.dart | 14 +- .../model/token/day_password_test.dart | 1 + .../model/token/hotp_token_test.dart | 1 + .../model/token/totp_token_test.dart | 1 + test/unit_test/utils/crypto_utils_test.dart | 1 + test/unit_test/utils/utils_test.dart | 21 --- 47 files changed, 533 insertions(+), 332 deletions(-) create mode 100644 lib/model/extensions/enums/algorithms_extension.dart create mode 100644 lib/model/extensions/enums/encodings_extension.dart create mode 100644 lib/utils/errors.dart diff --git a/lib/model/encryption/token_encryption.dart b/lib/model/encryption/token_encryption.dart index 1c944641b..898d6b442 100644 --- a/lib/model/encryption/token_encryption.dart +++ b/lib/model/encryption/token_encryption.dart @@ -16,7 +16,7 @@ class TokenEncryption { static Future> decrypt({required String encryptedTokens, required String password}) async { final json = jsonDecode(encryptedTokens); final tokenJsonString = await AesEncrypted.fromJson(json).decryptToString(password); - final tokenJsonsList = json.decode(tokenJsonString) as List; + final tokenJsonsList = jsonDecode(tokenJsonString) as List; return tokenJsonsList.map((e) => Token.fromJson(e)).toList(); } @@ -29,7 +29,7 @@ class TokenEncryption { return uri; } - static Future fromQrCodeUri(Uri uri) async { + static Token fromQrCodeUri(Uri uri) { final base64String = uri.queryParameters['data']; final zip = base64Url.decode(base64String!); final jsonString = utf8.decode(gzip.decode(zip)); diff --git a/lib/model/encryption/uint_8_buffer.dart b/lib/model/encryption/uint_8_buffer.dart index ddac5ea6a..804717f31 100644 --- a/lib/model/encryption/uint_8_buffer.dart +++ b/lib/model/encryption/uint_8_buffer.dart @@ -15,21 +15,36 @@ class Uint8Buffer { /// Reads [length] bytes from the current position /// and moves the position forward + /// If [length] is out of bounds, it will return the rest of the buffer Uint8List readBytes(int length) { - final bytes = data.sublist(currentPos, currentPos + length); - currentPos += length; + var nextPos = currentPos + length; + if (nextPos > data.length) nextPos = data.length; + final bytes = data.sublist(currentPos, nextPos); + currentPos = nextPos; return bytes; } /// Reads all bytes from the current position to the end of the buffer /// If [left] is provided, it will leave [left] bytes at the end /// and return the rest + /// If [left] is out of bounds, it will return an empty list Uint8List readBytesToEnd({int left = 0}) { + if (left < 0) left = 0; + var nextPos = data.length - left; + if (nextPos < currentPos) nextPos = currentPos; final bytes = data.sublist(currentPos, data.length - left); currentPos = data.length - left; return bytes; } /// Moves the current position to [pos] - void moveCurrentPos(int pos) => currentPos = pos; + /// If [pos] is out of bounds, it will move to the closest bound + void moveCurrentPos(int pos) { + if (pos > data.length) { + pos = data.length; + } else if (pos < 0) { + pos = 0; + } + currentPos = pos; + } } diff --git a/lib/model/enums/algorithms.dart b/lib/model/enums/algorithms.dart index c189d9774..b0a468d12 100644 --- a/lib/model/enums/algorithms.dart +++ b/lib/model/enums/algorithms.dart @@ -1,111 +1,6 @@ // ignore_for_file: constant_identifier_names - -import 'package:otp/otp.dart' as otp_library; -import 'package:privacyidea_authenticator/model/extensions/enum_extension.dart'; - -import '../../l10n/app_localizations.dart'; - enum Algorithms { SHA1, SHA256, SHA512, } - -extension AlgorithmsX on Algorithms { - String generateTOTPCodeString({ - required String secret, - required DateTime time, - required int length, - required Duration interval, - required bool isGoogle, - }) => - switch (this) { - Algorithms.SHA1 => otp_library.OTP.generateTOTPCodeString(secret, time.millisecondsSinceEpoch, - length: length, interval: interval.inSeconds, algorithm: otp_library.Algorithm.SHA1, isGoogle: isGoogle), - Algorithms.SHA256 => otp_library.OTP.generateTOTPCodeString(secret, time.millisecondsSinceEpoch, - length: length, interval: interval.inSeconds, algorithm: otp_library.Algorithm.SHA256, isGoogle: isGoogle), - Algorithms.SHA512 => otp_library.OTP.generateTOTPCodeString(secret, time.millisecondsSinceEpoch, - length: length, interval: interval.inSeconds, algorithm: otp_library.Algorithm.SHA512, isGoogle: isGoogle), - }; - - String generateHOTPCodeString({ - required String secret, - required int counter, - required int length, - required bool isGoogle, - }) => - switch (this) { - Algorithms.SHA1 => otp_library.OTP.generateHOTPCodeString(secret, counter, length: length, algorithm: otp_library.Algorithm.SHA1, isGoogle: isGoogle), - Algorithms.SHA256 => - otp_library.OTP.generateHOTPCodeString(secret, counter, length: length, algorithm: otp_library.Algorithm.SHA256, isGoogle: isGoogle), - Algorithms.SHA512 => - otp_library.OTP.generateHOTPCodeString(secret, counter, length: length, algorithm: otp_library.Algorithm.SHA512, isGoogle: isGoogle), - }; - - bool isString(String algoAsString) { - return algoAsString == asString; - } - - static Algorithms fromString(String algoAsString) => switch (algoAsString) { - 'SHA1' => Algorithms.SHA1, - 'SHA256' => Algorithms.SHA256, - 'SHA512' => Algorithms.SHA512, - _ => throw LocalizedArgumentError( - localizedMessage: (l, algo, name) => l.algorithmUnsupported(algo), - unlocalizedMessage: 'The algorithm [$algoAsString] is not supported', - invalidValue: algoAsString, - name: 'Algorithm'), - }; -} - -class LocalizedArgumentError extends LocalizedException implements ArgumentError { - final T _invalidValue; - final String? _name; - final StackTrace? _stackTrace; - - factory LocalizedArgumentError({ - required String Function(AppLocalizations localizations, T value, String name) localizedMessage, - required String unlocalizedMessage, - required T invalidValue, - required String name, - StackTrace? stackTrace, - }) => - LocalizedArgumentError._( - unlocalizedMessage: unlocalizedMessage, - localizedMessage: (localizations) => localizedMessage(localizations, invalidValue, name), - invalidValue: invalidValue, - name: name, - stackTrace: stackTrace, - ); - - const LocalizedArgumentError._({ - required super.unlocalizedMessage, - required super.localizedMessage, - required dynamic invalidValue, - String? name, - StackTrace? stackTrace, - }) : _invalidValue = invalidValue, - _name = name, - _stackTrace = stackTrace; - - @override - dynamic get invalidValue => _invalidValue; - @override - dynamic get message => super.unlocalizedMessage; - @override - String? get name => _name; - @override - StackTrace? get stackTrace => _stackTrace; - @override - String toString() => 'ArgumentError: $message'; -} - -class LocalizedException implements Exception { - final String Function(AppLocalizations localizations) localizedMessage; - final String unlocalizedMessage; - - const LocalizedException({required this.localizedMessage, required this.unlocalizedMessage}); - - @override - String toString() => 'Exception: $unlocalizedMessage'; -} diff --git a/lib/model/enums/app_feature.dart b/lib/model/enums/app_feature.dart index 05a4af8aa..45db738fa 100644 --- a/lib/model/enums/app_feature.dart +++ b/lib/model/enums/app_feature.dart @@ -1,20 +1,3 @@ -import 'algorithms.dart'; - enum AppFeature { patchNotes, } - -extension AppFeatureX on AppFeature { - String get name => switch (this) { - AppFeature.patchNotes => 'patchNotes', - }; - - static AppFeature fromName(String featureString) => switch (featureString) { - 'patchNotes' => AppFeature.patchNotes, - _ => throw LocalizedArgumentError( - localizedMessage: (localizations, feature, type) => localizations.invalidArgument(feature, type), - unlocalizedMessage: 'Invalid AppFeature name: $featureString', - invalidValue: featureString, - name: 'AppFeature'), - }; -} diff --git a/lib/model/enums/encodings.dart b/lib/model/enums/encodings.dart index f065f8736..7b8333cd8 100644 --- a/lib/model/enums/encodings.dart +++ b/lib/model/enums/encodings.dart @@ -1,80 +1,5 @@ -import 'dart:convert'; - -import 'package:base32/base32.dart'; -import 'package:flutter/foundation.dart'; -import 'package:hex/hex.dart'; - enum Encodings { none, base32, hex, } - -extension EncodingsX on Encodings { - String encode(Uint8List data) => switch (this) { - Encodings.none => utf8.decode(data), - Encodings.base32 => base32.encode(data), - Encodings.hex => HEX.encode(data), - }; - - String encodeStringTo(Encodings encoding, String data) => encoding.encode(decode(data)); - - Uint8List decode(String string) => switch (this) { - Encodings.none => utf8.encode(string), - Encodings.base32 => Uint8List.fromList(base32.decode(string)), - Encodings.hex => Uint8List.fromList(HEX.decode(string)), - }; - - bool isValidEncoding(String string) { - try { - decode(string); - return true; - } catch (_) { - return false; - } - } - - bool isInvalidEncoding(String string) { - try { - decode(string); - return false; - } catch (_) { - return true; - } - } - - Uint8List? tryDecode(String string) { - try { - return decode(string); - } catch (_) { - return null; - } - } - - String? tryEncode(Uint8List data) { - try { - return encode(data); - } catch (_) { - return null; - } - } - - bool isString(String value) { - return value == name; - } - - String get name => switch (this) { - Encodings.none => 'none', - Encodings.base32 => 'base32', - Encodings.hex => 'hex', - }; - - static Encodings fromString(String value) { - return switch (value) { - 'none' => Encodings.none, - 'base32' => Encodings.base32, - 'hex' => Encodings.hex, - _ => throw ArgumentError('Unknown encoding: $value'), - }; - } -} diff --git a/lib/model/enums/introduction.dart b/lib/model/enums/introduction.dart index cbb370f34..67126f2c9 100644 --- a/lib/model/enums/introduction.dart +++ b/lib/model/enums/introduction.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:json_annotation/json_annotation.dart'; import '../../l10n/app_localizations.dart'; import '../../utils/riverpod_providers.dart'; @@ -8,35 +7,27 @@ import '../states/introduction_state.dart'; // Do not rename or remove JsonValue values, they are used for serialization. Only add new values. enum Introduction { - @JsonValue('introductionScreen') introductionScreen, // 1st start - @JsonValue('scanQrCode') scanQrCode, // 1st start && introductionScreen - @JsonValue('addManually') - addTokenManually, // 1st start && scanQrCode - @JsonValue('tokenSwipe') + addManually, // 1st start && scanQrCode tokenSwipe, // 1st token - @JsonValue('editToken') editToken, // 1st token && tokenSwipe - @JsonValue('lockToken') lockToken, // 1st token && editToken - @JsonValue('dragToken') dragToken, // 2nd token && tokenSwipe - @JsonValue('addFolder') addFolder, // 3 tokens && 0 groups - @JsonValue('pollForChallenges') pollForChallenges, // 1st push token && lockToken - @JsonValue('hidePushTokens') hidePushTokens, // hiding is enabled } extension IntroductionX on Introduction { + /// Checks if the condition for the given state is fulfilled. + /// Given ref might be watched to acces the state of different providers. bool isConditionFulfilled(WidgetRef ref, IntroductionState state) => switch (this) { Introduction.introductionScreen => state.isUncompleted(Introduction.introductionScreen), Introduction.scanQrCode => state.isUncompleted(Introduction.scanQrCode), - Introduction.addTokenManually => state.isCompleted(Introduction.scanQrCode) && state.isUncompleted(Introduction.addTokenManually), + Introduction.addManually => state.isCompleted(Introduction.scanQrCode) && state.isUncompleted(Introduction.addManually), Introduction.tokenSwipe => - ref.watch(tokenProvider).tokens.isNotEmpty && state.isCompleted(Introduction.addTokenManually) && state.isUncompleted(Introduction.tokenSwipe), + ref.watch(tokenProvider).tokens.isNotEmpty && state.isCompleted(Introduction.addManually) && state.isUncompleted(Introduction.tokenSwipe), Introduction.editToken => state.isCompleted(Introduction.tokenSwipe) && state.isUncompleted(Introduction.editToken), Introduction.lockToken => state.isCompleted(Introduction.editToken) && state.isUncompleted(Introduction.lockToken), Introduction.dragToken => @@ -57,7 +48,7 @@ extension IntroductionX on Introduction { String hintText(BuildContext context) => switch (this) { Introduction.introductionScreen => '', Introduction.scanQrCode => AppLocalizations.of(context)!.introScanQrCode, - Introduction.addTokenManually => AppLocalizations.of(context)!.introAddTokenManually, + Introduction.addManually => AppLocalizations.of(context)!.introAddTokenManually, Introduction.tokenSwipe => AppLocalizations.of(context)!.introTokenSwipe, Introduction.editToken => AppLocalizations.of(context)!.introEditToken, Introduction.lockToken => AppLocalizations.of(context)!.introLockToken, diff --git a/lib/model/enums/patch_note_type.dart b/lib/model/enums/patch_note_type.dart index b3dc8cbda..3f6b1af07 100644 --- a/lib/model/enums/patch_note_type.dart +++ b/lib/model/enums/patch_note_type.dart @@ -7,7 +7,7 @@ enum PatchNoteType { } extension PatchNoteTypeX on PatchNoteType { - String getName(AppLocalizations localizations) => switch (this) { + String localizedName(AppLocalizations localizations) => switch (this) { PatchNoteType.newFeature => localizations.patchNotesNewFeatures, PatchNoteType.improvement => localizations.patchNotesImprovements, PatchNoteType.bugFix => localizations.patchNotesBugFixes, diff --git a/lib/model/enums/token_import_type.dart b/lib/model/enums/token_import_type.dart index 47ea211aa..521ab7131 100644 --- a/lib/model/enums/token_import_type.dart +++ b/lib/model/enums/token_import_type.dart @@ -10,7 +10,6 @@ enum TokenImportType { } extension TokenImportTypeExtension on TokenImportType { - String get name => toString().split('.').last; IconData get icon => switch (this) { const (TokenImportType.backupFile) => Icons.file_present, const (TokenImportType.qrScan) => Icons.qr_code_scanner, @@ -18,7 +17,7 @@ extension TokenImportTypeExtension on TokenImportType { const (TokenImportType.link) => Icons.link, }; - String getButtonText(BuildContext context) => switch (this) { + String buttonText(BuildContext context) => switch (this) { const (TokenImportType.backupFile) => AppLocalizations.of(context)!.selectFile, const (TokenImportType.qrScan) => AppLocalizations.of(context)!.scanQrCode, const (TokenImportType.qrFile) => AppLocalizations.of(context)!.selectFile, diff --git a/lib/model/extensions/enum_extension.dart b/lib/model/extensions/enum_extension.dart index 2dba81809..3e4d1461d 100644 --- a/lib/model/extensions/enum_extension.dart +++ b/lib/model/extensions/enum_extension.dart @@ -1,14 +1,3 @@ extension EnumExtension on Enum { - String get asString => toString().split('.').last; - - static Enum fromString(String string, List values) { - for (var value in values) { - if (value.asString.toLowerCase() == string.toLowerCase()) { - return value; - } - } - throw ArgumentError('Invalid token source type string'); - } - - bool isString(String encoding) => encoding.toLowerCase() == asString.toLowerCase(); + bool isName(String enumName) => enumName == name; } diff --git a/lib/model/extensions/enums/algorithms_extension.dart b/lib/model/extensions/enums/algorithms_extension.dart new file mode 100644 index 000000000..93910ce5e --- /dev/null +++ b/lib/model/extensions/enums/algorithms_extension.dart @@ -0,0 +1,33 @@ +import 'package:otp/otp.dart'; + +import '../../enums/algorithms.dart'; + +extension AlgorithmsX on Algorithms { + String generateTOTPCodeString({ + required String secret, + required DateTime time, + required int length, + required Duration interval, + required bool isGoogle, + }) => + switch (this) { + Algorithms.SHA1 => OTP.generateTOTPCodeString(secret, time.millisecondsSinceEpoch, + length: length, interval: interval.inSeconds, algorithm: Algorithm.SHA1, isGoogle: isGoogle), + Algorithms.SHA256 => OTP.generateTOTPCodeString(secret, time.millisecondsSinceEpoch, + length: length, interval: interval.inSeconds, algorithm: Algorithm.SHA256, isGoogle: isGoogle), + Algorithms.SHA512 => OTP.generateTOTPCodeString(secret, time.millisecondsSinceEpoch, + length: length, interval: interval.inSeconds, algorithm: Algorithm.SHA512, isGoogle: isGoogle), + }; + + String generateHOTPCodeString({ + required String secret, + required int counter, + required int length, + required bool isGoogle, + }) => + switch (this) { + Algorithms.SHA1 => OTP.generateHOTPCodeString(secret, counter, length: length, algorithm: Algorithm.SHA1, isGoogle: isGoogle), + Algorithms.SHA256 => OTP.generateHOTPCodeString(secret, counter, length: length, algorithm: Algorithm.SHA256, isGoogle: isGoogle), + Algorithms.SHA512 => OTP.generateHOTPCodeString(secret, counter, length: length, algorithm: Algorithm.SHA512, isGoogle: isGoogle), + }; +} diff --git a/lib/model/extensions/enums/encodings_extension.dart b/lib/model/extensions/enums/encodings_extension.dart new file mode 100644 index 000000000..e176c9ccf --- /dev/null +++ b/lib/model/extensions/enums/encodings_extension.dart @@ -0,0 +1,57 @@ +import 'dart:convert'; + +import 'package:base32/base32.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hex/hex.dart'; + +import '../../enums/encodings.dart'; + +extension EncodingsX on Encodings { + String encode(Uint8List data) => switch (this) { + Encodings.none => utf8.decode(data), + Encodings.base32 => base32.encode(data), + Encodings.hex => HEX.encode(data), + }; + + String encodeStringTo(Encodings encoding, String data) => encoding.encode(decode(data)); + + Uint8List decode(String string) => switch (this) { + Encodings.none => utf8.encode(string), + Encodings.base32 => Uint8List.fromList(base32.decode(string)), + Encodings.hex => Uint8List.fromList(HEX.decode(string)), + }; + + bool isValidEncoding(String string) { + try { + decode(string); + return true; + } catch (_) { + return false; + } + } + + bool isInvalidEncoding(String string) { + try { + decode(string); + return false; + } catch (_) { + return true; + } + } + + Uint8List? tryDecode(String string) { + try { + return decode(string); + } catch (_) { + return null; + } + } + + String? tryEncode(Uint8List data) { + try { + return encode(data); + } catch (_) { + return null; + } + } +} diff --git a/lib/model/states/introduction_state.g.dart b/lib/model/states/introduction_state.g.dart index 433334f91..b1095d878 100644 --- a/lib/model/states/introduction_state.g.dart +++ b/lib/model/states/introduction_state.g.dart @@ -24,7 +24,7 @@ Map _$IntroductionStateToJson(IntroductionState instance) => const _$IntroductionEnumMap = { Introduction.introductionScreen: 'introductionScreen', Introduction.scanQrCode: 'scanQrCode', - Introduction.addTokenManually: 'addManually', + Introduction.addManually: 'addManually', Introduction.tokenSwipe: 'tokenSwipe', Introduction.editToken: 'editToken', Introduction.lockToken: 'lockToken', diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart index 9c29633de..55386b9dc 100644 --- a/lib/model/tokens/day_password_token.dart +++ b/lib/model/tokens/day_password_token.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/algorithms_extension.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:uuid/uuid.dart'; import '../../utils/identifiers.dart'; @@ -7,7 +9,6 @@ import '../enums/algorithms.dart'; import '../enums/day_password_token_view_mode.dart'; import '../enums/encodings.dart'; import '../enums/token_types.dart'; -import '../extensions/enum_extension.dart'; import '../token_import/token_origin_data.dart'; import 'otp_token.dart'; import 'token.dart'; @@ -17,7 +18,7 @@ part 'day_password_token.g.dart'; @JsonSerializable() @immutable class DayPasswordToken extends OTPToken { - static String get tokenType => TokenTypes.DAYPASSWORD.asString; + static String get tokenType => TokenTypes.DAYPASSWORD.name; final DayPasswordTokenViewMode viewMode; final Duration period; @@ -39,7 +40,7 @@ class DayPasswordToken extends OTPToken { super.label = '', super.issuer = '', }) : period = period.inSeconds > 0 ? period : const Duration(hours: 24), - super(type: TokenTypes.DAYPASSWORD.asString); + super(type: TokenTypes.DAYPASSWORD.name); @override // Only the viewMode can be changed even if its the same token @@ -85,7 +86,7 @@ class DayPasswordToken extends OTPToken { label: label ?? this.label, issuer: issuer ?? this.issuer, id: id ?? this.id, - type: TokenTypes.DAYPASSWORD.asString, + type: TokenTypes.DAYPASSWORD.name, algorithm: algorithm ?? this.algorithm, digits: digits ?? this.digits, secret: secret ?? this.secret, @@ -128,7 +129,7 @@ class DayPasswordToken extends OTPToken { label: uriMap[URI_LABEL] ?? '', issuer: uriMap[URI_ISSUER] ?? '', id: const Uuid().v4(), - algorithm: AlgorithmsX.fromString(uriMap[URI_ALGORITHM] ?? 'SHA1'), + algorithm: Algorithms.values.byName(uriMap[URI_ALGORITHM] ?? 'SHA1'), digits: uriMap[URI_DIGITS] ?? 6, secret: Encodings.base32.encode(uriMap[URI_SECRET]), period: Duration(seconds: uriMap[URI_PERIOD]), diff --git a/lib/model/tokens/hotp_token.dart b/lib/model/tokens/hotp_token.dart index 7d7566211..81e2b2a43 100644 --- a/lib/model/tokens/hotp_token.dart +++ b/lib/model/tokens/hotp_token.dart @@ -1,11 +1,12 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/algorithms_extension.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:uuid/uuid.dart'; import '../../utils/identifiers.dart'; import '../enums/algorithms.dart'; import '../enums/encodings.dart'; import '../enums/token_types.dart'; -import '../extensions/enum_extension.dart'; import '../token_import/token_origin_data.dart'; import 'otp_token.dart'; import 'token.dart'; @@ -14,7 +15,7 @@ part 'hotp_token.g.dart'; @JsonSerializable() class HOTPToken extends OTPToken { - static String get tokenType => TokenTypes.HOTP.asString; + static String get tokenType => TokenTypes.HOTP.name; final int counter; // this value is used to calculate the current otp value @override @@ -36,7 +37,7 @@ class HOTPToken extends OTPToken { super.origin, super.label = '', super.issuer = '', - }) : super(type: TokenTypes.HOTP.asString); + }) : super(type: TokenTypes.HOTP.name); @override bool sameValuesAs(Token other) => super.sameValuesAs(other) && other is HOTPToken && other.counter == counter; @@ -101,7 +102,7 @@ class HOTPToken extends OTPToken { label: uriMap[URI_LABEL] ?? '', issuer: uriMap[URI_ISSUER] ?? '', id: const Uuid().v4(), - algorithm: AlgorithmsX.fromString(uriMap[URI_ALGORITHM] ?? 'SHA1'), + algorithm: Algorithms.values.byName(uriMap[URI_ALGORITHM] ?? 'SHA1'), digits: uriMap[URI_DIGITS] ?? 6, secret: Encodings.base32.encode(uriMap[URI_SECRET]), counter: uriMap[URI_COUNTER] ?? 0, diff --git a/lib/model/tokens/push_token.dart b/lib/model/tokens/push_token.dart index 550b77e9c..93d2a2f25 100644 --- a/lib/model/tokens/push_token.dart +++ b/lib/model/tokens/push_token.dart @@ -7,7 +7,6 @@ import '../../utils/identifiers.dart'; import '../../utils/rsa_utils.dart'; import '../enums/push_token_rollout_state.dart'; import '../enums/token_types.dart'; -import '../extensions/enum_extension.dart'; import '../token_import/token_origin_data.dart'; import 'token.dart'; @@ -69,7 +68,7 @@ class PushToken extends Token { }) : isRolledOut = isRolledOut ?? false, sslVerify = sslVerify ?? false, rolloutState = rolloutState ?? PushTokenRollOutState.rolloutNotStarted, - super(type: TokenTypes.PIPUSH.asString); + super(type: TokenTypes.PIPUSH.name); @override bool sameValuesAs(Token other) { diff --git a/lib/model/tokens/steam_token.dart b/lib/model/tokens/steam_token.dart index b29ef84fe..6c88f93da 100644 --- a/lib/model/tokens/steam_token.dart +++ b/lib/model/tokens/steam_token.dart @@ -1,13 +1,13 @@ import 'package:base32/base32.dart'; import 'package:crypto/crypto.dart' show Hmac, sha1; import 'package:json_annotation/json_annotation.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:uuid/uuid.dart'; import '../../utils/identifiers.dart'; import '../enums/algorithms.dart'; import '../enums/encodings.dart'; import '../enums/token_types.dart'; -import '../extensions/enum_extension.dart'; import '../extensions/int_extension.dart'; import '../token_import/token_origin_data.dart'; import 'token.dart'; @@ -19,7 +19,7 @@ part 'steam_token.g.dart'; class SteamToken extends TOTPToken { @override bool get isPrivacyIdeaToken => false; - static String get tokenType => TokenTypes.STEAM.asString; + static String get tokenType => TokenTypes.STEAM.name; static const String steamAlphabet = "23456789BCDFGHJKMNPQRTVWXY"; SteamToken({ @@ -27,6 +27,7 @@ class SteamToken extends TOTPToken { required super.id, required super.algorithm, required super.secret, + String? type, super.tokenImage, super.sortIndex, super.pin, @@ -37,7 +38,7 @@ class SteamToken extends TOTPToken { super.label = '', super.issuer = '', }) : super( - type: tokenType, + type: type ?? tokenType, digits: 5, ); @@ -105,7 +106,7 @@ class SteamToken extends TOTPToken { label: uriMap[URI_LABEL] as String, issuer: uriMap[URI_ISSUER] as String, id: const Uuid().v4(), - algorithm: AlgorithmsX.fromString(uriMap[URI_ALGORITHM] ?? 'SHA1'), + algorithm: Algorithms.values.byName(uriMap[URI_ALGORITHM] ?? 'SHA1'), secret: Encodings.base32.encode(uriMap[URI_SECRET]), tokenImage: uriMap[URI_IMAGE] as String?, pin: uriMap[URI_PIN] as bool?, diff --git a/lib/model/tokens/steam_token.g.dart b/lib/model/tokens/steam_token.g.dart index 971f72250..f8a72b8ff 100644 --- a/lib/model/tokens/steam_token.g.dart +++ b/lib/model/tokens/steam_token.g.dart @@ -11,6 +11,7 @@ SteamToken _$SteamTokenFromJson(Map json) => SteamToken( id: json['id'] as String, algorithm: $enumDecode(_$AlgorithmsEnumMap, json['algorithm']), secret: json['secret'] as String, + type: json['type'] as String?, tokenImage: json['tokenImage'] as String?, sortIndex: json['sortIndex'] as int?, pin: json['pin'] as bool?, @@ -36,6 +37,7 @@ Map _$SteamTokenToJson(SteamToken instance) => 'folderId': instance.folderId, 'sortIndex': instance.sortIndex, 'origin': instance.origin, + 'type': instance.type, 'algorithm': _$AlgorithmsEnumMap[instance.algorithm]!, 'secret': instance.secret, 'period': instance.period, diff --git a/lib/model/tokens/token.dart b/lib/model/tokens/token.dart index e0985e18b..821719608 100644 --- a/lib/model/tokens/token.dart +++ b/lib/model/tokens/token.dart @@ -33,22 +33,22 @@ abstract class Token with SortableMixin { factory Token.fromJson(Map json) { String type = json['type']; - if (TokenTypes.HOTP.isString(type)) return HOTPToken.fromJson(json); - if (TokenTypes.TOTP.isString(type)) return TOTPToken.fromJson(json); - if (TokenTypes.PIPUSH.isString(type)) return PushToken.fromJson(json); - if (TokenTypes.DAYPASSWORD.isString(type)) return DayPasswordToken.fromJson(json); - if (TokenTypes.STEAM.isString(type)) return SteamToken.fromJson(json); + if (TokenTypes.HOTP.isName(type)) return HOTPToken.fromJson(json); + if (TokenTypes.TOTP.isName(type)) return TOTPToken.fromJson(json); + if (TokenTypes.PIPUSH.isName(type)) return PushToken.fromJson(json); + if (TokenTypes.DAYPASSWORD.isName(type)) return DayPasswordToken.fromJson(json); + if (TokenTypes.STEAM.isName(type)) return SteamToken.fromJson(json); throw ArgumentError.value(json, 'Token#fromJson', 'Token type [$type] is not a supported'); } factory Token.fromUriMap( Map uriMap, ) { String type = uriMap[URI_TYPE]; - if (TokenTypes.HOTP.isString(type)) return HOTPToken.fromUriMap(uriMap); - if (TokenTypes.TOTP.isString(type)) return TOTPToken.fromUriMap(uriMap); - if (TokenTypes.PIPUSH.isString(type)) return PushToken.fromUriMap(uriMap); - if (TokenTypes.DAYPASSWORD.isString(type)) return DayPasswordToken.fromUriMap(uriMap); - if (TokenTypes.STEAM.isString(type)) return SteamToken.fromUriMap(uriMap); + if (TokenTypes.HOTP.isName(type)) return HOTPToken.fromUriMap(uriMap); + if (TokenTypes.TOTP.isName(type)) return TOTPToken.fromUriMap(uriMap); + if (TokenTypes.PIPUSH.isName(type)) return PushToken.fromUriMap(uriMap); + if (TokenTypes.DAYPASSWORD.isName(type)) return DayPasswordToken.fromUriMap(uriMap); + if (TokenTypes.STEAM.isName(type)) return SteamToken.fromUriMap(uriMap); throw ArgumentError.value(uriMap, 'Token#fromUriMap', 'Token type [$type] is not a supported'); } diff --git a/lib/model/tokens/totp_token.dart b/lib/model/tokens/totp_token.dart index 9132b6df5..dc8bbfb77 100644 --- a/lib/model/tokens/totp_token.dart +++ b/lib/model/tokens/totp_token.dart @@ -1,4 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/algorithms_extension.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:uuid/uuid.dart'; import '../../utils/identifiers.dart'; @@ -6,7 +8,6 @@ import '../../utils/logger.dart'; import '../enums/algorithms.dart'; import '../enums/encodings.dart'; import '../enums/token_types.dart'; -import '../extensions/enum_extension.dart'; import '../token_import/token_origin_data.dart'; import 'otp_token.dart'; import 'token.dart'; @@ -15,7 +16,7 @@ part 'totp_token.g.dart'; @JsonSerializable() class TOTPToken extends OTPToken { - static String get tokenType => TokenTypes.TOTP.asString; + static String get tokenType => TokenTypes.TOTP.name; // this value is used to calculate the current 'counter' of this token // based on the UNIX systemtime), the counter is used to calculate the // current otp value @@ -112,7 +113,7 @@ class TOTPToken extends OTPToken { label: uriMap[URI_LABEL] ?? '', issuer: uriMap[URI_ISSUER] ?? '', id: const Uuid().v4(), - algorithm: AlgorithmsX.fromString((uriMap[URI_ALGORITHM] ?? 'SHA1')), + algorithm: Algorithms.values.byName((uriMap[URI_ALGORITHM] ?? 'SHA1')), digits: uriMap[URI_DIGITS] ?? 6, tokenImage: uriMap[URI_IMAGE], secret: Encodings.base32.encode(uriMap[URI_SECRET]), diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart index ce32642a5..531b6d4ad 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import '../../../l10n/app_localizations.dart'; import '../../../model/enums/algorithms.dart'; @@ -9,6 +10,7 @@ import '../../../model/enums/token_types.dart'; import '../../../model/extensions/enum_extension.dart'; import '../../../model/processor_result.dart'; import '../../../model/tokens/token.dart'; +import '../../../utils/errors.dart'; import '../../../utils/globals.dart'; import '../../../utils/identifiers.dart'; import '../../../utils/logger.dart'; @@ -71,11 +73,11 @@ class OtpAuthProcessor extends TokenImportSchemeProcessor { /// to https://github.com/google/google-authenticator/wiki/Key-Uri-Format. Map _parseOtpToken(Uri uri) { final type = uri.host; - if (TokenTypes.PIPUSH.isString(type)) { + if (TokenTypes.PIPUSH.isName(type)) { // otpauth://pipush/LABEL?PARAMETERS return _parsePiPushToken(uri); } - if (TokenTypes.values.firstWhereOrNull((element) => element.isString(type)) != null) { + if (TokenTypes.values.firstWhereOrNull((element) => element.isName(type)) != null) { return _parseOtpAuth(uri); } throw ArgumentError.value( @@ -118,8 +120,8 @@ Map _parseOtpAuth(Uri uri) { uriMap[URI_IMAGE] = uri.queryParameters['image']; } - String algorithm = uri.queryParameters['algorithm'] ?? Algorithms.SHA1.asString; // Optional parameter - algorithm = AlgorithmsX.fromString(algorithm).asString; // Validate algorithm, throw error if not supported. + String algorithm = uri.queryParameters['algorithm'] ?? Algorithms.SHA1.name; // Optional parameter + algorithm = Algorithms.values.byName(algorithm).name; // Validate algorithm, throw error if not supported. uriMap[URI_ALGORITHM] = algorithm; @@ -155,7 +157,7 @@ Map _parseOtpAuth(Uri uri) { throw ArgumentError.value( uri, 'uri', - '[${Encodings.base32.asString}] is not a valid encoding for [$secretAsString].', + '[${Encodings.base32.name}] is not a valid encoding for [$secretAsString].', ); } diff --git a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart index 28996176c..c377114fe 100644 --- a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart @@ -10,14 +10,15 @@ import 'package:file_selector/file_selector.dart'; import 'package:pointycastle/export.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:privacyidea_authenticator/utils/identifiers.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; import 'package:privacyidea_authenticator/utils/token_import_origins.dart'; import '../../l10n/app_localizations.dart'; -import '../../model/enums/algorithms.dart'; import '../../model/processor_result.dart'; +import '../../utils/errors.dart'; import '../../utils/globals.dart'; import 'token_import_file_processor_interface.dart'; import 'two_fas_import_file_processor.dart'; diff --git a/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart index 3824db135..79d8ed805 100644 --- a/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart @@ -6,7 +6,6 @@ import 'package:cryptography/cryptography.dart'; import 'package:file_selector/file_selector.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/token_types.dart'; -import 'package:privacyidea_authenticator/model/extensions/enum_extension.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart'; import 'package:privacyidea_authenticator/processors/token_import_file_processor/two_fas_import_file_processor.dart'; @@ -19,6 +18,7 @@ import '../../model/encryption/aes_encrypted.dart'; import '../../model/encryption/uint_8_buffer.dart'; import '../../model/enums/token_origin_source_type.dart'; import '../../model/processor_result.dart'; +import '../../utils/errors.dart'; import '../../utils/globals.dart'; import 'token_import_file_processor_interface.dart'; @@ -43,17 +43,17 @@ class AuthenticatorProImportFileProcessor extends TokenImportFileProcessor { */ static final typeMap = { - 1: TokenTypes.HOTP.asString, - 2: TokenTypes.TOTP.asString, + 1: TokenTypes.HOTP.name, + 2: TokenTypes.TOTP.name, // 3: 'mOTP', // Not supported - 4: TokenTypes.STEAM.asString, + 4: TokenTypes.STEAM.name, // 5: 'Yandex', // Not supported }; static final algorithmMap = { - 0: Algorithms.SHA1.asString, - 1: Algorithms.SHA256.asString, - 2: Algorithms.SHA512.asString, + 0: Algorithms.SHA1.name, + 1: Algorithms.SHA256.name, + 2: Algorithms.SHA512.name, }; const AuthenticatorProImportFileProcessor(); diff --git a/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart b/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart index d9c83ad68..ee27ec04f 100644 --- a/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart +++ b/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart @@ -5,13 +5,13 @@ import 'dart:typed_data'; import 'package:file_selector/file_selector.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; -import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/model/processor_result.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:privacyidea_authenticator/utils/globals.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; +import '../../utils/errors.dart'; import '../../utils/identifiers.dart'; import '../../utils/token_import_origins.dart'; import '../scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart'; diff --git a/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart b/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart index a54e84338..d89e934c6 100644 --- a/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart @@ -4,15 +4,16 @@ import 'dart:convert'; import 'package:cryptography/cryptography.dart'; import 'package:file_selector/file_selector.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/utils/token_import_origins.dart'; import '../../l10n/app_localizations.dart'; import '../../model/encryption/aes_encrypted.dart'; -import '../../model/enums/algorithms.dart'; import '../../model/enums/encodings.dart'; import '../../model/enums/token_origin_source_type.dart'; import '../../model/processor_result.dart'; import '../../model/tokens/token.dart'; +import '../../utils/errors.dart'; import '../../utils/globals.dart'; import '../../utils/identifiers.dart'; import '../../utils/logger.dart'; diff --git a/lib/utils/app_customizer.dart b/lib/utils/app_customizer.dart index a335d2c76..e0c40fbf0 100644 --- a/lib/utils/app_customizer.dart +++ b/lib/utils/app_customizer.dart @@ -513,7 +513,7 @@ class ApplicationCustomization { appImageUint8List: json['appImageBASE64'] != null ? base64Decode(json['appImageBASE64'] as String) : null, lightTheme: ThemeCustomization.fromJson(json['lightTheme'] as Map), darkTheme: ThemeCustomization.fromJson(json['darkTheme'] as Map), - disabledFeatures: (json['disabledFeatures'] as List).map((e) => AppFeatureX.fromName(e as String)).toSet(), + disabledFeatures: (json['disabledFeatures'] as List).map((e) => AppFeature.values.byName(e as String)).toSet(), ); Map toJson() { diff --git a/lib/utils/crypto_utils.dart b/lib/utils/crypto_utils.dart index c083c0f8e..5915edd25 100644 --- a/lib/utils/crypto_utils.dart +++ b/lib/utils/crypto_utils.dart @@ -25,6 +25,7 @@ import 'dart:math' as math; import 'package:base32/base32.dart'; import 'package:flutter/foundation.dart'; import 'package:pointycastle/export.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import '../model/enums/encodings.dart'; diff --git a/lib/utils/errors.dart b/lib/utils/errors.dart new file mode 100644 index 000000000..5bc7e3de6 --- /dev/null +++ b/lib/utils/errors.dart @@ -0,0 +1,53 @@ +import '../l10n/app_localizations.dart'; + +class LocalizedArgumentError extends LocalizedException implements ArgumentError { + final T _invalidValue; + final String? _name; + final StackTrace? _stackTrace; + + factory LocalizedArgumentError({ + required String Function(AppLocalizations localizations, T value, String name) localizedMessage, + required String unlocalizedMessage, + required T invalidValue, + required String name, + StackTrace? stackTrace, + }) => + LocalizedArgumentError._( + unlocalizedMessage: unlocalizedMessage, + localizedMessage: (localizations) => localizedMessage(localizations, invalidValue, name), + invalidValue: invalidValue, + name: name, + stackTrace: stackTrace, + ); + + const LocalizedArgumentError._({ + required super.unlocalizedMessage, + required super.localizedMessage, + required dynamic invalidValue, + String? name, + StackTrace? stackTrace, + }) : _invalidValue = invalidValue, + _name = name, + _stackTrace = stackTrace; + + @override + dynamic get invalidValue => _invalidValue; + @override + dynamic get message => super.unlocalizedMessage; + @override + String? get name => _name; + @override + StackTrace? get stackTrace => _stackTrace; + @override + String toString() => 'ArgumentError: $message'; +} + +class LocalizedException implements Exception { + final String Function(AppLocalizations localizations) localizedMessage; + final String unlocalizedMessage; + + const LocalizedException({required this.localizedMessage, required this.unlocalizedMessage}); + + @override + String toString() => 'Exception: $unlocalizedMessage'; +} diff --git a/lib/views/add_token_manually_view/add_token_manually_view.dart b/lib/views/add_token_manually_view/add_token_manually_view.dart index 1ad044ec9..80c2367d9 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import '../../l10n/app_localizations.dart'; import '../../mains/main_netknights.dart'; @@ -9,7 +10,6 @@ import '../../model/enums/algorithms.dart'; import '../../model/enums/encodings.dart'; import '../../model/enums/token_origin_source_type.dart'; import '../../model/enums/token_types.dart'; -import '../../model/extensions/enum_extension.dart'; import '../../model/tokens/token.dart'; import '../../utils/identifiers.dart'; import '../../utils/logger.dart'; @@ -183,10 +183,10 @@ class _AddTokenManuallyViewState extends ConsumerState { Logger.info('Input is valid, building token'); final uriMap = { - URI_TYPE: _typeNotifier.value.asString, + URI_TYPE: _typeNotifier.value.name, URI_LABEL: _labelController.text, URI_ISSUER: '', - URI_ALGORITHM: _algorithmNotifier.value.asString, + URI_ALGORITHM: _algorithmNotifier.value.name, URI_DIGITS: _digitsNotifier.value, URI_SECRET: _encodingNotifier.value.decode(_secretController.text), URI_COUNTER: 0, diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart index 83b52a957..58cfd8625 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; - -import '../../../model/extensions/enum_extension.dart'; import '../../../utils/logger.dart'; class LabeledDropdownButton extends StatefulWidget { @@ -49,7 +47,7 @@ class _LabeledDropdownButtonState extends State> { return DropdownMenuItem( value: value, child: Text( - '${value is Enum ? value.asString : value}' + '${value is Enum ? value.name : value}' '${widget.postFix}', style: Theme.of(context).textTheme.titleMedium, overflow: TextOverflow.fade, diff --git a/lib/views/import_tokens_view/pages/import_start_page.dart b/lib/views/import_tokens_view/pages/import_start_page.dart index 10e2d25ee..d6b0dec7d 100644 --- a/lib/views/import_tokens_view/pages/import_start_page.dart +++ b/lib/views/import_tokens_view/pages/import_start_page.dart @@ -113,7 +113,7 @@ class _ImportStartPageState extends State { width: double.infinity, child: ElevatedButton( child: Text( - widget.selectedSource.type.getButtonText(context), + widget.selectedSource.type.buttonText(context), style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimary), overflow: TextOverflow.fade, softWrap: false, diff --git a/lib/views/main_view/main_view_widgets/main_view_navigation_bar.dart b/lib/views/main_view/main_view_widgets/main_view_navigation_bar.dart index e8e50c368..74bfdd2c3 100644 --- a/lib/views/main_view/main_view_widgets/main_view_navigation_bar.dart +++ b/lib/views/main_view/main_view_widgets/main_view_navigation_bar.dart @@ -74,9 +74,9 @@ class MainViewNavigationBar extends ConsumerWidget { }, icon: FocusedItemAsOverlay( onComplete: () { - ref.read(introductionProvider.notifier).complete(Introduction.addTokenManually); + ref.read(introductionProvider.notifier).complete(Introduction.addManually); }, - isFocused: ref.watch(introductionProvider).isConditionFulfilled(ref, Introduction.addTokenManually), + isFocused: ref.watch(introductionProvider).isConditionFulfilled(ref, Introduction.addManually), tooltipWhenFocused: AppLocalizations.of(context)!.introAddTokenManually, child: FittedBox( child: Icon( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart index 63da3f365..d9acac56e 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart @@ -5,7 +5,6 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../../l10n/app_localizations.dart'; import '../../../../../../model/enums/introduction.dart'; -import '../../../../../../model/extensions/enum_extension.dart'; import '../../../../../../model/tokens/day_password_token.dart'; import '../../../../../../utils/app_customizer.dart'; import '../../../../../../utils/globals.dart'; @@ -124,7 +123,7 @@ class EditDayPassowrdTokenAction extends TokenAction { }, ), TextFormField( - initialValue: algorithm.asString, + initialValue: algorithm.name, decoration: InputDecoration(labelText: AppLocalizations.of(context)!.algorithm), enabled: false, ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart index 1fa836c5f..4bc649f67 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart @@ -7,7 +7,6 @@ import 'package:intl/intl.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/enums/day_password_token_view_mode.dart'; -import '../../../../../model/extensions/enum_extension.dart'; import '../../../../../model/tokens/day_password_token.dart'; import '../../../../../utils/riverpod_providers.dart'; import '../../../../../utils/utils.dart'; @@ -104,7 +103,7 @@ class _DayPasswordTokenWidgetTileState extends ConsumerState { (widget.token.label.isNotEmpty && widget.token.issuer.isNotEmpty) ? '${widget.token.issuer}: ${widget.token.label}' : '${widget.token.issuer}${widget.token.label}', - 'Algorithm: ${widget.token.algorithm.asString}', + 'Algorithm: ${widget.token.algorithm.name}', 'Counter: ${widget.token.counter}', ] : [ diff --git a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart index ac8de49ec..b4da8dab3 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart @@ -3,7 +3,6 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../../l10n/app_localizations.dart'; import '../../../../../../model/enums/introduction.dart'; -import '../../../../../../model/extensions/enum_extension.dart'; import '../../../../../../model/tokens/totp_token.dart'; import '../../../../../../utils/app_customizer.dart'; import '../../../../../../utils/globals.dart'; @@ -120,7 +119,7 @@ class EditTOTPTokenAction extends TokenAction { }, ), TextFormField( - initialValue: algorithm.asString, + initialValue: algorithm.name, decoration: InputDecoration(labelText: AppLocalizations.of(context)!.algorithm), enabled: false, ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart index c32add5d4..dcd1a6242 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutterlifecyclehooks/flutterlifecyclehooks.dart'; import '../../../../../l10n/app_localizations.dart'; -import '../../../../../model/extensions/enum_extension.dart'; import '../../../../../model/tokens/totp_token.dart'; import '../../../../../utils/riverpod_providers.dart'; import '../../../../../utils/utils.dart'; @@ -125,7 +124,7 @@ class _TOTPTokenWidgetTileState extends ConsumerState with (widget.token.label.isNotEmpty && widget.token.issuer.isNotEmpty) ? '${widget.token.issuer}: ${widget.token.label}' : widget.token.issuer + widget.token.label, - 'Algorithm: ${widget.token.algorithm.asString}', + 'Algorithm: ${widget.token.algorithm.name}', 'Period: ${widget.token.period} seconds', ] : [ diff --git a/lib/widgets/dialog_widgets/patch_notes_dialog.dart b/lib/widgets/dialog_widgets/patch_notes_dialog.dart index 2acbae7c6..c472fd934 100644 --- a/lib/widgets/dialog_widgets/patch_notes_dialog.dart +++ b/lib/widgets/dialog_widgets/patch_notes_dialog.dart @@ -47,7 +47,7 @@ class PatchNotesDialog extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - entry.key.getName(localizations), + entry.key.localizedName(localizations), style: Theme.of(context).textTheme.titleSmall?.copyWith(color: theme.primaryColor), ), const SizedBox(height: 8), diff --git a/test/unit_test/model/encryption/token_encryption_test.dart b/test/unit_test/model/encryption/token_encryption_test.dart index 341956c5b..6a2fff294 100644 --- a/test/unit_test/model/encryption/token_encryption_test.dart +++ b/test/unit_test/model/encryption/token_encryption_test.dart @@ -1,4 +1,3 @@ -import 'package:cryptography/cryptography.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/encryption/token_encryption.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; @@ -7,6 +6,7 @@ import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; import 'package:privacyidea_authenticator/model/tokens/steam_token.dart'; import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart'; void main() { _testTokenEncryption(); @@ -23,10 +23,162 @@ void _testTokenEncryption() { PushToken(serial: 'serial', id: 'id5'), ]; final encrypted = await TokenEncryption.encrypt(tokens: tokensList, password: 'password'); - print(encrypted); + expect(encrypted.isNotEmpty, true); + expect(encrypted.contains('"data":"'), true); + }); + test('decrypt', () async { + const encrypted = + '{"data":"jW5TJIY5dApfjZwYxJO7U5TYoV8JDbSHqlD2iPVDri8KrrisYRFy0ewg+YmU8XH9SS+TzEppAc4tbC69ZLXt5FLbQFprnJgP3eHEIw3ok1aHAaALtClyLnCNW265IjSrdqYdXm4DSHGG3Ol+9SyuCNjKwgdmkRO4Oqa2PimL0oOyjMLwVp908PY65lckBPAvX9CeAuLwglMCmg36tr2u0lKiPDqmYexPlpuriZOuzpBN4x+hWU75hBeo8hAJNIpnEBLCBufnOFCfFxgpr2mx4AsMh79AIeTENSTE2k327CKPpnJYXKfCdTVwVKtreeWyp4tN++9ACjmDx7QCRzAuDLHucyP4cE4gQ3uDkhhLtAOhaBlkTHWfQ0KP0dq3O5zQE6IwXRaMhN8kBiwqkQALjEtwhbWqtJPVK6fTYpGFb+gNg5dqwig4jx5h90drUUtlWWWvHCtAxFxNVgLtJIoAcHTrJy1rHU3gO85EaUClLYOQIx17gyA3FhO97VwRkk+8b8+kurjnEk+CVH3CTsBSEOKHMQDr2euoTlLukADm9qrJcXkprPfHLUnSCKAJ+9cDMvD13+Fa+xK7ybBnGnG13PkeNJplpwxNprITrvzq8QDpLBmAIaTeEbev9+qpuUOkS1UsDiXaYw/0tsRmsI0vc+864amfXMHiKl1VaAdL58/GjkveCu+nteers2Mubk48qWVyiw1MFR8c1gxDrL+V0WFD/YACNOjFUnUVP43XosbdM+7DRtW5m08uIcrap2SF7+Fzg9ye3WLSLCzAg5v6oNijHnaxNiWNaaX88vjLbCJAj00OX3xZGqefVMF4hV5l2SkTICEBh9Q21ZMJvA1WVs0LsYK2i9DHKVQohvhpjqCyn9xEGEvEOHOOYWNiBhLdEEQojfkdzmGOAw17Qi/7Ttd5bboMmUg6lIbkiDlfnkB6B2XtEmj/tASQJkWcWtamds+5VYu1j7L12Yk+133CeBXRYzHtUj4Ks7OCBilHS67kEJxJc2fcJvuQhJ7i1fZh4BB1/wCAjhRhoEmB8BXlD6xQeLcqSk/bvs4wbTf7AejfQpb4+yOW4sn6v00QrSDN52OFuTB0cDnFlNMQEAwaPgynkWafP5ibLerXd0EHzPpgioT70scgAV0WTVSItyAhuxixmp3Zr90g3hx0GfL3knCfHX3OwPOb7LGhqKQYcqG6MewDucHVftCAaUt6xg8tHTci9Zvv4d1mF/XZ8JLw/5IhRw4VxkqSsHWPQMGRNGFttHCCjwje4jEd9PZISK4dSA1TybTCvNek9dfrSLFDhpEXN9zrLHFYsYfHOhegFxdnFr9f8wZPeP1z1agoQXL9tKjrADPD0HmEBxBQtq/ihGRAggDK89BBufApj7IqSayBvS7JA/On22FGtIqKcnMeozNXGFGKeTRlQd7Rb+nBQuubNVx4qNjPrGRU5pZS1qAUNM4viK+8iZE1ZhObMf6hkFYOn8YcJx+PYsW83i6m9XqA/LbBUCKZOYhx101xLwsid1U2lftlwfVbmEyw095UnTLLSM5QDub0gZOpGWZ3YSPg6eteBBwlkiAnmmuT4li37BDxCDOGtCHY6c+LXOELZxTcTkwH7B7ODJxR5RS1+f+3AOekaNGaTBgN/7B6wKq6SG5y/BUrXebfAyyMofXFReLUHImJWxwKF1oVgf69ioN57xvbjbmLmeySlkZaIehrx5AEmMxW6PRzPbyEctOKesDBvlLT4LO7YBqYRLb9V0Ul0U1Gecbd4Uxi","salt":"68nMAFVeqzS5L9zaK3Rfrw==","iv":"z/3ZYNKTiwuDLzW9dfn9Kg==","mac":"Neo3ZresLNiEiM3Zs0F+tg==","kdf":{"algorithm":"Pbkdf2","macAlgorithm":{"algorithm":"Hmac","hashAlgorithm":{"algorithm":"DartSha256"}},"iterations":100000,"bits":256},"cypher":{"algorithm":"AesGcm","secretKeyLength":32}}'; + final decrypted = await TokenEncryption.decrypt(encryptedTokens: encrypted, password: 'password'); + + expect(decrypted.isNotEmpty, true); + expect(decrypted.length, 5); + expect(decrypted.whereType().length, 1); + expect(decrypted.whereType().length, 2); // TOTP and Steam + expect(decrypted.whereType().length, 1); + expect(decrypted.whereType().length, 1); + expect(decrypted.whereType().length, 1); + }); + test('generateQrCodeUri', () { + final tokensList = [ + HOTPToken(id: 'id1', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret1'), + TOTPToken(period: 30, id: 'id2', algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2'), + SteamToken(period: 30, id: 'id3', algorithm: Algorithms.SHA512, secret: 'secret3'), + DayPasswordToken(period: const Duration(hours: 24), id: 'id4', algorithm: Algorithms.SHA512, digits: 10, secret: 'secret4'), + PushToken(serial: 'serial', id: 'id5'), + ]; + const compressedTokensBase64 = [ + 'H4sIAAAAAAAACk1PMQ7CMAz8i-cOsDBkY2slJJDgA2nshqhuUiWuBEL8HUelgsnnu7PPfgHbnhgMQAOhlIXyF6PWgHuFc4hgBsuFquWU3Ej4R7QBkX4OSSPFbrKewMSFuYEhMVLucOtLytJFpMdGpBx8zVg7ec46Cu35dtFwy15luU9KXdtjvQfVLQXMQVeRyyQqraCqLi1R6he79wc_WDvI3QAAAA==', + 'H4sIAAAAAAAACk1OzQrCMAx-l5x3kIkivXlbQVDYXqAuWS3L2tF2oIjvbuYcesr3l3x5ApsrMSiAAlxKE8UvRpkOS4Gj86A6w4nmyCm0PeGfUDlE-iVy6MnrwVgC5SfmArrASFHjylOIWXuk-yqE6OzcsbD8GGUVmnNzkXLDVux8G0Sqq2O524uIks8J1EGOURspi7mAz78UXZC27eb1Bgi4xPffAAAA', + 'H4sIAAAAAAAACk2OsQ7CMAxE_8VzB6BiydYBqZFggh8ItRuiukmVpBII8e84lAom3z2fdX4CmysxKIAKXEozxa9GmQ5rkZPzoHrDiUrkGLqB8A-0DpF-iRwG8no0lkD5mbmCPjBS1Lj6FGLWHum-ghCdLR2Ly49JTuF8OTQnaTdsZZ9vY2Fts9_uBCbqImUhi_h8SdEF6ag3rzcEg5Vm1QAAAA==', + 'H4sIAAAAAAAACk1PTQvCMAz9Lznv4GSK7DYY4mAy2UDxOE2cZV072s4PxP9uyhyaS15eXpKXF8j6RBJigACEtQOZL0bOAiOGvVAQX2ppyUtyfW4J_4iNQKSfwumWVNbVDUGsBikDuGiJZDKcaquNyxTSYyK0EY2_MVbu2fMopMlxl1TVoShT9lDLhlXu2nGn2iSLcM4k8pizEIczXkpnQ467I_C-b4LuW41-2T7Js3RdlP4bMkKzl9Uymn3j_QHdQYgrBgEAAA==', + 'H4sIAAAAAAAAClVQwW7CMAz9F5-57tIrO1ANMbSy3VPiIguTVI6DqKb9-5xCOnbK88t7fk_-BnY9MjQAK6CUMsoDe3vJvxgcKUAzOE5YJNt4PKN_IjbkPf4pNJ4xtBd3QmhCZl7BENmjtL7OKYq2weOtElHoVDLuk06jWWHf7j-7jcXjbSRxSjG8Ol2WJhRypfcDWEx_KNGLIPGXfQ3T0gyDROYLBl0LWmU1X6ryLFwhpQ_ToX_PuniLM2btdK5Qx10sjKjdw86Ue6Zjh3JFecOpbhuFrmaauz3Ts_o_-_ML-WVvco4BAAA=', + ]; + for (var i = 0; tokensList.length > i; i++) { + final token = tokensList[i]; + final compressed = compressedTokensBase64[i]; + final qrCodeUri = TokenEncryption.generateQrCodeUri(token: token); + final uriString = qrCodeUri.toString(); + expect(uriString.isNotEmpty, true); + expect(uriString, '${PrivacyIDEAAuthenticatorQrProcessor.scheme}://${PrivacyIDEAAuthenticatorQrProcessor.host}?data=$compressed'); + } + }); + test('fromQrCodeUri', () { + final tokensList = [ + HOTPToken(id: 'id1', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret1'), + TOTPToken(period: 30, id: 'id2', algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2'), + SteamToken(period: 30, id: 'id3', algorithm: Algorithms.SHA512, secret: 'secret3'), + DayPasswordToken(period: const Duration(hours: 24), id: 'id4', algorithm: Algorithms.SHA512, digits: 10, secret: 'secret4'), + PushToken(serial: 'serial', id: 'id5'), + ]; + const uriStrings = [ + 'pia://qrbackup?data=H4sIAAAAAAAACk1PMQ7CMAz8i-cOsDBkY2slJJDgA2nshqhuUiWuBEL8HUelgsnnu7PPfgHbnhgMQAOhlIXyF6PWgHuFc4hgBsuFquWU3Ej4R7QBkX4OSSPFbrKewMSFuYEhMVLucOtLytJFpMdGpBx8zVg7ec46Cu35dtFwy15luU9KXdtjvQfVLQXMQVeRyyQqraCqLi1R6he79wc_WDvI3QAAAA==', + 'pia://qrbackup?data=H4sIAAAAAAAACk1OzQrCMAx-l5x3kIkivXlbQVDYXqAuWS3L2tF2oIjvbuYcesr3l3x5ApsrMSiAAlxKE8UvRpkOS4Gj86A6w4nmyCm0PeGfUDlE-iVy6MnrwVgC5SfmArrASFHjylOIWXuk-yqE6OzcsbD8GGUVmnNzkXLDVux8G0Sqq2O524uIks8J1EGOURspi7mAz78UXZC27eb1Bgi4xPffAAAA', + 'pia://qrbackup?data=H4sIAAAAAAAACk2OsQ7CMAxE_8VzB6BiydYBqZFggh8ItRuiukmVpBII8e84lAom3z2fdX4CmysxKIAKXEozxa9GmQ5rkZPzoHrDiUrkGLqB8A-0DpF-iRwG8no0lkD5mbmCPjBS1Lj6FGLWHum-ghCdLR2Ly49JTuF8OTQnaTdsZZ9vY2Fts9_uBCbqImUhi_h8SdEF6ag3rzcEg5Vm1QAAAA==', + 'pia://qrbackup?data=H4sIAAAAAAAACk1PTQvCMAz9Lznv4GSK7DYY4mAy2UDxOE2cZV072s4PxP9uyhyaS15eXpKXF8j6RBJigACEtQOZL0bOAiOGvVAQX2ppyUtyfW4J_4iNQKSfwumWVNbVDUGsBikDuGiJZDKcaquNyxTSYyK0EY2_MVbu2fMopMlxl1TVoShT9lDLhlXu2nGn2iSLcM4k8pizEIczXkpnQ467I_C-b4LuW41-2T7Js3RdlP4bMkKzl9Uymn3j_QHdQYgrBgEAAA==', + 'pia://qrbackup?data=H4sIAAAAAAAAClVQwW7CMAz9F5-57tIrO1ANMbSy3VPiIguTVI6DqKb9-5xCOnbK88t7fk_-BnY9MjQAK6CUMsoDe3vJvxgcKUAzOE5YJNt4PKN_IjbkPf4pNJ4xtBd3QmhCZl7BENmjtL7OKYq2weOtElHoVDLuk06jWWHf7j-7jcXjbSRxSjG8Ol2WJhRypfcDWEx_KNGLIPGXfQ3T0gyDROYLBl0LWmU1X6ryLFwhpQ_ToX_PuniLM2btdK5Qx10sjKjdw86Ue6Zjh3JFecOpbhuFrmaauz3Ts_o_-_ML-WVvco4BAAA=', + ]; + for (var i = 0; uriStrings.length > i; i++) { + final uri = Uri.parse(uriStrings[i]); + final token = tokensList[i]; + final decrypted = TokenEncryption.fromQrCodeUri(uri); + expect(decrypted, token); + } }); - test('decrypt', () {}); - test('generateQrCodeUri', () {}); - test('fromQrCodeUri', () {}); }); } + +// const asd = [ +// { +// "label": "", +// "issuer": "", +// "id": "id1", +// "pin": false, +// "isLocked": false, +// "isHidden": false, +// "tokenImage": null, +// "folderId": null, +// "sortIndex": null, +// "origin": null, +// "type": "HOTP", +// "algorithm": "SHA1", +// "digits": 6, +// "secret": "secret1", +// "counter": 0 +// }, +// { +// "label": "", +// "issuer": "", +// "id": "id2", +// "pin": false, +// "isLocked": false, +// "isHidden": false, +// "tokenImage": null, +// "folderId": null, +// "sortIndex": null, +// "origin": null, +// "type": "TOTP", +// "algorithm": "SHA256", +// "digits": 8, +// "secret": "secret2", +// "period": 30 +// }, +// { +// "label": "", +// "issuer": "", +// "id": "id3", +// "pin": false, +// "isLocked": false, +// "isHidden": false, +// "tokenImage": null, +// "folderId": null, +// "sortIndex": null, +// "origin": null, +// "type": "STEAM", +// "algorithm": "SHA512", +// "secret": "secret3", +// "period": 30 +// }, +// { +// "label": "", +// "issuer": "", +// "id": "id4", +// "pin": false, +// "isLocked": false, +// "isHidden": false, +// "tokenImage": null, +// "folderId": null, +// "sortIndex": null, +// "origin": null, +// "type": "DAYPASSWORD", +// "algorithm": "SHA512", +// "digits": 10, +// "secret": "secret4", +// "viewMode": "VALIDFOR", +// "period": 86400000000 +// }, +// { +// "label": "", +// "issuer": "", +// "id": "id5", +// "pin": false, +// "isLocked": false, +// "isHidden": false, +// "tokenImage": null, +// "folderId": null, +// "sortIndex": null, +// "origin": null, +// "type": "PIPUSH", +// "expirationDate": null, +// "serial": "serial", +// "fbToken": null, +// "sslVerify": false, +// "enrollmentCredentials": null, +// "url": null, +// "isRolledOut": false, +// "rolloutState": "rolloutNotStarted", +// "publicServerKey": null, +// "privateTokenKey": null, +// "publicTokenKey": null +// } +// ]; diff --git a/test/unit_test/model/encryption/uint_8_buffer_test.dart b/test/unit_test/model/encryption/uint_8_buffer_test.dart index e69de29bb..b6c321c8b 100644 --- a/test/unit_test/model/encryption/uint_8_buffer_test.dart +++ b/test/unit_test/model/encryption/uint_8_buffer_test.dart @@ -0,0 +1,108 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/encryption/uint_8_buffer.dart'; + +void main() { + _testUint8Buffer(); +} + +void _testUint8Buffer() { + group('Uint 8 Buffer', () { + test('fromList', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + expect(buffer.data, equals(Uint8List.fromList(list))); + }); + + test('writeBytes', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + final list2 = [6, 7, 8, 9, 10]; + buffer.writeBytes(Uint8List.fromList(list2)); + expect(buffer.data, equals(Uint8List.fromList([...list, ...list2]))); + }); + + test('readBytes', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + final bytes = buffer.readBytes(3); + expect(bytes, equals(Uint8List.fromList([1, 2, 3]))); + expect(buffer.currentPos, equals(3)); + }); + + test('readBytes more than available', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + final bytes = buffer.readBytes(10); + expect(bytes, equals(Uint8List.fromList([1, 2, 3, 4, 5]))); + expect(buffer.currentPos, equals(5)); + }); + + test('readBytesToEnd with left', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + buffer.readBytes(1); + final bytes = buffer.readBytesToEnd(left: 2); + expect(bytes, equals(Uint8List.fromList([2, 3]))); + expect(buffer.currentPos, equals(3)); + }); + + test('readBytesToEnd without left', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + buffer.readBytes(1); + final bytes = buffer.readBytesToEnd(); + expect(bytes, equals(Uint8List.fromList([2, 3, 4, 5]))); + expect(buffer.currentPos, equals(5)); + }); + + test('moveCurrentPos', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + buffer.moveCurrentPos(3); + expect(buffer.currentPos, equals(3)); + final bytes = buffer.readBytesToEnd(); + expect(bytes, equals(Uint8List.fromList([4, 5]))); + }); + + test('moveCurrentPos to out of bounds', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + buffer.moveCurrentPos(10); + expect(buffer.currentPos, equals(5)); + final bytes = buffer.readBytesToEnd(); + expect(bytes, equals(Uint8List.fromList([]))); + }); + }); +} +/* + factory Uint8Buffer.fromList(List list) { + return Uint8Buffer(data: Uint8List.fromList(list)); + } + + /// Writes [bytes] to the buffer + void writeBytes(Uint8List bytes) { + data = Uint8List.fromList([...data, ...bytes]); + } + + /// Reads [length] bytes from the current position + /// and moves the position forward + Uint8List readBytes(int length) { + final bytes = data.sublist(currentPos, currentPos + length); + currentPos += length; + return bytes; + } + + /// Reads all bytes from the current position to the end of the buffer + /// If [left] is provided, it will leave [left] bytes at the end + /// and return the rest + Uint8List readBytesToEnd({int left = 0}) { + final bytes = data.sublist(currentPos, data.length - left); + currentPos = data.length - left; + return bytes; + } + + /// Moves the current position to [pos] + void moveCurrentPos(int pos) => currentPos = pos; + */ \ No newline at end of file diff --git a/test/unit_test/model/enums/app_feature_test.dart b/test/unit_test/model/enums/app_feature_test.dart index 8b1378917..0b380ae64 100644 --- a/test/unit_test/model/enums/app_feature_test.dart +++ b/test/unit_test/model/enums/app_feature_test.dart @@ -1 +1,17 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/app_feature.dart'; +void main() { + _testAppFeatureX(); +} + +void _testAppFeatureX() { + group('App Feature Extension', () { + test('name', () { + expect((AppFeature.patchNotes.name), equals('patchNotes')); + }); + test('fromName', () { + expect(AppFeature.values.byName('patchNotes'), equals(AppFeature.patchNotes)); + }); + }); +} diff --git a/test/unit_test/model/states/introduction_state_test.dart b/test/unit_test/model/states/introduction_state_test.dart index c78aaeda4..d18bcbf60 100644 --- a/test/unit_test/model/states/introduction_state_test.dart +++ b/test/unit_test/model/states/introduction_state_test.dart @@ -6,20 +6,20 @@ void main() { group('IntroductionState', () { test('withCompletedIntroduction add introduction', () { const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder}); - final updatedState = introductionState.withCompletedIntroduction(Introduction.addTokenManually); - expect(updatedState.completedIntroductions, {Introduction.addFolder, Introduction.addTokenManually}); + final updatedState = introductionState.withCompletedIntroduction(Introduction.addManually); + expect(updatedState.completedIntroductions, {Introduction.addFolder, Introduction.addManually}); }); test('withoutCompletedIntroduction remove introduction', () { - const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder, Introduction.addTokenManually}); - final updatedState = introductionState.withoutCompletedIntroduction(Introduction.addTokenManually); + const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder, Introduction.addManually}); + final updatedState = introductionState.withoutCompletedIntroduction(Introduction.addManually); expect(updatedState.completedIntroductions, {Introduction.addFolder}); }); test('withoutCompletedIntroduction add duplicate introduction', () { - const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder, Introduction.addTokenManually}); - final updatedState = introductionState.withCompletedIntroduction(Introduction.addTokenManually); - expect(updatedState.completedIntroductions, {Introduction.addFolder, Introduction.addTokenManually}); + const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder, Introduction.addManually}); + final updatedState = introductionState.withCompletedIntroduction(Introduction.addManually); + expect(updatedState.completedIntroductions, {Introduction.addFolder, Introduction.addManually}); }); }); } diff --git a/test/unit_test/model/token/day_password_test.dart b/test/unit_test/model/token/day_password_test.dart index ac1acd263..9c78ec64f 100644 --- a/test/unit_test/model/token/day_password_test.dart +++ b/test/unit_test/model/token/day_password_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/day_password_token_view_mode.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/model/tokens/day_password_token.dart'; import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; diff --git a/test/unit_test/model/token/hotp_token_test.dart b/test/unit_test/model/token/hotp_token_test.dart index caddc616a..c064675fc 100644 --- a/test/unit_test/model/token/hotp_token_test.dart +++ b/test/unit_test/model/token/hotp_token_test.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; void main() { diff --git a/test/unit_test/model/token/totp_token_test.dart b/test/unit_test/model/token/totp_token_test.dart index ff33b9274..91cf42b15 100644 --- a/test/unit_test/model/token/totp_token_test.dart +++ b/test/unit_test/model/token/totp_token_test.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; diff --git a/test/unit_test/utils/crypto_utils_test.dart b/test/unit_test/utils/crypto_utils_test.dart index 42861daca..bbf4070ad 100644 --- a/test/unit_test/utils/crypto_utils_test.dart +++ b/test/unit_test/utils/crypto_utils_test.dart @@ -22,6 +22,7 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/utils/crypto_utils.dart'; void main() { diff --git a/test/unit_test/utils/utils_test.dart b/test/unit_test/utils/utils_test.dart index 900c79ac0..53f83c4bb 100644 --- a/test/unit_test/utils/utils_test.dart +++ b/test/unit_test/utils/utils_test.dart @@ -18,15 +18,11 @@ limitations under the License. */ import 'package:flutter_test/flutter_test.dart'; -import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; -import 'package:privacyidea_authenticator/model/extensions/enum_extension.dart'; import 'package:privacyidea_authenticator/utils/utils.dart'; void main() { _testInsertCharAt(); _testSplitPeriodically(); - _testMapStringToAlgorithm(); - _testEnumAsString(); } void _testInsertCharAt() { @@ -54,20 +50,3 @@ void _testSplitPeriodically() { test('Split every 12', () => expect('ABCDEFGHIJKL MNOPQRSTUVWX YZ', splitPeriodically(str, 12))); }); } - -void _testMapStringToAlgorithm() { - group('mapStringToAlgorithm', () { - test('Test SHA1', () => expect(AlgorithmsX.fromString('SHA1'), Algorithms.SHA1)); - test('Test SHA256', () => expect(AlgorithmsX.fromString('SHA256'), Algorithms.SHA256)); - test('Test SHA512', () => expect(AlgorithmsX.fromString('SHA512'), Algorithms.SHA512)); - test('Test invalid', () => expect(() => AlgorithmsX.fromString('invalid'), throwsArgumentError)); - }); -} - -void _testEnumAsString() { - group('enumAsString', () { - test('Test SHA1', () => expect(Algorithms.SHA1.asString, 'SHA1')); - test('Test SHA256', () => expect(Algorithms.SHA256.asString, 'SHA256')); - test('Test SHA512', () => expect(Algorithms.SHA512.asString, 'SHA512')); - }); -} From c51fccbab98957b07c815556297b31910ca80ae7 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:29:01 +0200 Subject: [PATCH 03/11] added more tests --- lib/model/enums/introduction.dart | 49 +------------------ lib/model/enums/patch_note_type.dart | 10 ---- lib/model/enums/push_token_rollout_state.dart | 28 +---------- lib/model/enums/token_import_type.dart | 21 +------- lib/model/enums/token_origin_source_type.dart | 18 +------ lib/model/extensions/enum_extension.dart | 1 + .../enums/introduction_extension.dart | 46 +++++++++++++++++ .../enums/patch_note_type_extension.dart | 10 ++++ .../push_token_rollout_state_extension.dart | 25 ++++++++++ .../enums/token_import_type_extension.dart | 20 ++++++++ .../enums/token_origin_source_type.dart | 17 +++++++ lib/model/states/introduction_state.dart | 1 + lib/model/tokens/token.dart | 20 ++++---- .../free_otp_plus_qr_processor.dart | 2 + .../google_authenticator_qr_processor.dart | 1 + .../otp_auth_processor.dart | 4 +- .../aegis_import_file_processor.dart | 1 + ...thenticator_pro_import_file_processor.dart | 1 + .../free_otp_plus_file_processor.dart | 1 + .../two_fas_import_file_processor.dart | 1 + lib/state_notifiers/token_notifier.dart | 6 ++- .../add_token_manually_view.dart | 1 + .../pages/import_plain_tokens_page.dart | 1 + .../pages/import_start_page.dart | 31 +++++++----- .../pages/select_import_type_page.dart | 12 +++-- .../rollout_failed_widget.dart | 18 ++++--- .../push_token_widgets/rollout_widget.dart | 5 +- .../dialog_widgets/patch_notes_dialog.dart | 1 + .../state_notifiers/token_notifier_test.dart | 1 + 29 files changed, 191 insertions(+), 162 deletions(-) create mode 100644 lib/model/extensions/enums/introduction_extension.dart create mode 100644 lib/model/extensions/enums/patch_note_type_extension.dart create mode 100644 lib/model/extensions/enums/push_token_rollout_state_extension.dart create mode 100644 lib/model/extensions/enums/token_import_type_extension.dart create mode 100644 lib/model/extensions/enums/token_origin_source_type.dart diff --git a/lib/model/enums/introduction.dart b/lib/model/enums/introduction.dart index 67126f2c9..61a3cd8ae 100644 --- a/lib/model/enums/introduction.dart +++ b/lib/model/enums/introduction.dart @@ -1,11 +1,4 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../l10n/app_localizations.dart'; -import '../../utils/riverpod_providers.dart'; -import '../states/introduction_state.dart'; - -// Do not rename or remove JsonValue values, they are used for serialization. Only add new values. +// Do not rename or remove values, they are used for serialization. Only add new values. enum Introduction { introductionScreen, // 1st start scanQrCode, // 1st start && introductionScreen @@ -18,43 +11,3 @@ enum Introduction { pollForChallenges, // 1st push token && lockToken hidePushTokens, // hiding is enabled } - -extension IntroductionX on Introduction { - /// Checks if the condition for the given state is fulfilled. - /// Given ref might be watched to acces the state of different providers. - bool isConditionFulfilled(WidgetRef ref, IntroductionState state) => switch (this) { - Introduction.introductionScreen => state.isUncompleted(Introduction.introductionScreen), - Introduction.scanQrCode => state.isUncompleted(Introduction.scanQrCode), - Introduction.addManually => state.isCompleted(Introduction.scanQrCode) && state.isUncompleted(Introduction.addManually), - Introduction.tokenSwipe => - ref.watch(tokenProvider).tokens.isNotEmpty && state.isCompleted(Introduction.addManually) && state.isUncompleted(Introduction.tokenSwipe), - Introduction.editToken => state.isCompleted(Introduction.tokenSwipe) && state.isUncompleted(Introduction.editToken), - Introduction.lockToken => state.isCompleted(Introduction.editToken) && state.isUncompleted(Introduction.lockToken), - Introduction.dragToken => - ref.watch(tokenProvider).tokens.length >= 2 && state.isCompleted(Introduction.tokenSwipe) && state.isUncompleted(Introduction.dragToken), - Introduction.addFolder => ref.watch(tokenProvider).tokens.length >= 3 && - state.isCompleted(Introduction.dragToken) && - state.isUncompleted(Introduction.addFolder) && - Introduction.dragToken.isConditionFulfilled(ref, state) == false, - Introduction.pollForChallenges => ref.watch(tokenProvider).pushTokens.firstOrNull?.isRolledOut == true && - state.isCompleted(Introduction.tokenSwipe) && - state.isUncompleted(Introduction.pollForChallenges) && - Introduction.dragToken.isConditionFulfilled(ref, state) == false && - Introduction.addFolder.isConditionFulfilled(ref, state) == false, - Introduction.hidePushTokens => - ref.watch(settingsProvider).hidePushTokens && state.isCompleted(Introduction.pollForChallenges) && state.isUncompleted(Introduction.hidePushTokens), - }; - - String hintText(BuildContext context) => switch (this) { - Introduction.introductionScreen => '', - Introduction.scanQrCode => AppLocalizations.of(context)!.introScanQrCode, - Introduction.addManually => AppLocalizations.of(context)!.introAddTokenManually, - Introduction.tokenSwipe => AppLocalizations.of(context)!.introTokenSwipe, - Introduction.editToken => AppLocalizations.of(context)!.introEditToken, - Introduction.lockToken => AppLocalizations.of(context)!.introLockToken, - Introduction.dragToken => AppLocalizations.of(context)!.introDragToken, - Introduction.addFolder => AppLocalizations.of(context)!.introAddFolder, - Introduction.pollForChallenges => AppLocalizations.of(context)!.introPollForChallenges, - Introduction.hidePushTokens => AppLocalizations.of(context)!.introHidePushTokens, - }; -} diff --git a/lib/model/enums/patch_note_type.dart b/lib/model/enums/patch_note_type.dart index 3f6b1af07..7e917bb19 100644 --- a/lib/model/enums/patch_note_type.dart +++ b/lib/model/enums/patch_note_type.dart @@ -1,15 +1,5 @@ -import '../../l10n/app_localizations.dart'; - enum PatchNoteType { newFeature, improvement, bugFix, } - -extension PatchNoteTypeX on PatchNoteType { - String localizedName(AppLocalizations localizations) => switch (this) { - PatchNoteType.newFeature => localizations.patchNotesNewFeatures, - PatchNoteType.improvement => localizations.patchNotesImprovements, - PatchNoteType.bugFix => localizations.patchNotesBugFixes, - }; -} diff --git a/lib/model/enums/push_token_rollout_state.dart b/lib/model/enums/push_token_rollout_state.dart index a81911ce4..e4e34399a 100644 --- a/lib/model/enums/push_token_rollout_state.dart +++ b/lib/model/enums/push_token_rollout_state.dart @@ -1,7 +1,4 @@ -import 'package:flutter/material.dart'; - -import '../../l10n/app_localizations.dart'; - +// Do not rename or remove values, they are used for serialization. Only add new values. enum PushTokenRollOutState { rolloutNotStarted, generatingRSAKeyPair, @@ -12,26 +9,3 @@ enum PushTokenRollOutState { parsingResponseFailed, rolloutComplete, } - -extension PushTokenRollOutStateX on PushTokenRollOutState { - bool get rollOutInProgress => switch (this) { - PushTokenRollOutState.rolloutNotStarted => false, - PushTokenRollOutState.generatingRSAKeyPair => true, - PushTokenRollOutState.generatingRSAKeyPairFailed => false, - PushTokenRollOutState.sendRSAPublicKey => true, - PushTokenRollOutState.sendRSAPublicKeyFailed => false, - PushTokenRollOutState.parsingResponse => true, - PushTokenRollOutState.parsingResponseFailed => false, - PushTokenRollOutState.rolloutComplete => false, - }; - String rolloutMsg(BuildContext context) => switch (this) { - PushTokenRollOutState.rolloutNotStarted => AppLocalizations.of(context)!.rollingOut, - PushTokenRollOutState.generatingRSAKeyPair => AppLocalizations.of(context)!.generatingRSAKeyPair, - PushTokenRollOutState.generatingRSAKeyPairFailed => AppLocalizations.of(context)!.generatingRSAKeyPairFailed, - PushTokenRollOutState.sendRSAPublicKey => AppLocalizations.of(context)!.sendingRSAPublicKey, - PushTokenRollOutState.sendRSAPublicKeyFailed => AppLocalizations.of(context)!.sendingRSAPublicKeyFailed, - PushTokenRollOutState.parsingResponse => AppLocalizations.of(context)!.parsingResponse, - PushTokenRollOutState.parsingResponseFailed => AppLocalizations.of(context)!.parsingResponseFailed, - PushTokenRollOutState.rolloutComplete => AppLocalizations.of(context)!.rolloutCompleted, - }; -} diff --git a/lib/model/enums/token_import_type.dart b/lib/model/enums/token_import_type.dart index 521ab7131..0f92f2e59 100644 --- a/lib/model/enums/token_import_type.dart +++ b/lib/model/enums/token_import_type.dart @@ -1,26 +1,7 @@ -import 'package:flutter/material.dart'; - -import '../../l10n/app_localizations.dart'; - +// Do not rename or remove values, they are used for serialization. Only add new values. enum TokenImportType { backupFile, qrScan, qrFile, link, } - -extension TokenImportTypeExtension on TokenImportType { - IconData get icon => switch (this) { - const (TokenImportType.backupFile) => Icons.file_present, - const (TokenImportType.qrScan) => Icons.qr_code_scanner, - const (TokenImportType.qrFile) => Icons.qr_code_2, - const (TokenImportType.link) => Icons.link, - }; - - String buttonText(BuildContext context) => switch (this) { - const (TokenImportType.backupFile) => AppLocalizations.of(context)!.selectFile, - const (TokenImportType.qrScan) => AppLocalizations.of(context)!.scanQrCode, - const (TokenImportType.qrFile) => AppLocalizations.of(context)!.selectFile, - const (TokenImportType.link) => AppLocalizations.of(context)!.enterLink, - }; -} diff --git a/lib/model/enums/token_origin_source_type.dart b/lib/model/enums/token_origin_source_type.dart index 1fed87159..691aa1a2e 100644 --- a/lib/model/enums/token_origin_source_type.dart +++ b/lib/model/enums/token_origin_source_type.dart @@ -1,7 +1,4 @@ -import '../../mains/main_netknights.dart'; -import '../token_import/token_origin_data.dart'; -import '../tokens/token.dart'; - +// Do not rename any value, only add new values at the end of the list. The order of values must not change. enum TokenOriginSourceType { backupFile, qrScan, @@ -12,16 +9,3 @@ enum TokenOriginSourceType { manually, unknown, } - -extension TokenSourceTypeX on TokenOriginSourceType { - TokenOriginData toTokenOrigin({String data = '', String? appName, bool? isPrivacyIdeaToken, DateTime? createdAt}) => TokenOriginData( - source: this, - data: data, - appName: appName ?? PrivacyIDEAAuthenticator.currentCustomization?.appName, - isPrivacyIdeaToken: isPrivacyIdeaToken, - createdAt: createdAt ?? DateTime.now(), - ); - - Token addOriginToToken({required Token token, required String data, required bool? isPrivacyIdeaToken, String? appName, DateTime? createdAt}) => - token.copyWith(origin: toTokenOrigin(data: data, appName: appName, isPrivacyIdeaToken: isPrivacyIdeaToken, createdAt: createdAt)); -} diff --git a/lib/model/extensions/enum_extension.dart b/lib/model/extensions/enum_extension.dart index 3e4d1461d..01a0ff169 100644 --- a/lib/model/extensions/enum_extension.dart +++ b/lib/model/extensions/enum_extension.dart @@ -1,3 +1,4 @@ extension EnumExtension on Enum { bool isName(String enumName) => enumName == name; + bool isNameInsensitive(String enumName) => enumName.toLowerCase() == name.toLowerCase(); } diff --git a/lib/model/extensions/enums/introduction_extension.dart b/lib/model/extensions/enums/introduction_extension.dart new file mode 100644 index 000000000..7785570c2 --- /dev/null +++ b/lib/model/extensions/enums/introduction_extension.dart @@ -0,0 +1,46 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../l10n/app_localizations.dart'; +import '../../../utils/riverpod_providers.dart'; +import '../../enums/introduction.dart'; +import '../../states/introduction_state.dart'; + +extension IntroductionX on Introduction { + /// Checks if the condition for the given state is fulfilled. + /// Given ref might be watched to acces the state of different providers. + bool isConditionFulfilled(WidgetRef ref, IntroductionState state) => switch (this) { + Introduction.introductionScreen => state.isUncompleted(Introduction.introductionScreen), + Introduction.scanQrCode => state.isUncompleted(Introduction.scanQrCode), + Introduction.addManually => state.isCompleted(Introduction.scanQrCode) && state.isUncompleted(Introduction.addManually), + Introduction.tokenSwipe => + ref.watch(tokenProvider).tokens.isNotEmpty && state.isCompleted(Introduction.addManually) && state.isUncompleted(Introduction.tokenSwipe), + Introduction.editToken => state.isCompleted(Introduction.tokenSwipe) && state.isUncompleted(Introduction.editToken), + Introduction.lockToken => state.isCompleted(Introduction.editToken) && state.isUncompleted(Introduction.lockToken), + Introduction.dragToken => + ref.watch(tokenProvider).tokens.length >= 2 && state.isCompleted(Introduction.tokenSwipe) && state.isUncompleted(Introduction.dragToken), + Introduction.addFolder => ref.watch(tokenProvider).tokens.length >= 3 && + state.isCompleted(Introduction.dragToken) && + state.isUncompleted(Introduction.addFolder) && + Introduction.dragToken.isConditionFulfilled(ref, state) == false, + Introduction.pollForChallenges => ref.watch(tokenProvider).pushTokens.firstOrNull?.isRolledOut == true && + state.isCompleted(Introduction.tokenSwipe) && + state.isUncompleted(Introduction.pollForChallenges) && + Introduction.dragToken.isConditionFulfilled(ref, state) == false && + Introduction.addFolder.isConditionFulfilled(ref, state) == false, + Introduction.hidePushTokens => + ref.watch(settingsProvider).hidePushTokens && state.isCompleted(Introduction.pollForChallenges) && state.isUncompleted(Introduction.hidePushTokens), + }; + + String hintText(AppLocalizations localizations) => switch (this) { + Introduction.introductionScreen => '', + Introduction.scanQrCode => localizations.introScanQrCode, + Introduction.addManually => localizations.introAddTokenManually, + Introduction.tokenSwipe => localizations.introTokenSwipe, + Introduction.editToken => localizations.introEditToken, + Introduction.lockToken => localizations.introLockToken, + Introduction.dragToken => localizations.introDragToken, + Introduction.addFolder => localizations.introAddFolder, + Introduction.pollForChallenges => localizations.introPollForChallenges, + Introduction.hidePushTokens => localizations.introHidePushTokens, + }; +} diff --git a/lib/model/extensions/enums/patch_note_type_extension.dart b/lib/model/extensions/enums/patch_note_type_extension.dart new file mode 100644 index 000000000..adad05302 --- /dev/null +++ b/lib/model/extensions/enums/patch_note_type_extension.dart @@ -0,0 +1,10 @@ +import '../../../l10n/app_localizations.dart'; +import '../../enums/patch_note_type.dart'; + +extension PatchNoteTypeX on PatchNoteType { + String localizedName(AppLocalizations localizations) => switch (this) { + PatchNoteType.newFeature => localizations.patchNotesNewFeatures, + PatchNoteType.improvement => localizations.patchNotesImprovements, + PatchNoteType.bugFix => localizations.patchNotesBugFixes, + }; +} diff --git a/lib/model/extensions/enums/push_token_rollout_state_extension.dart b/lib/model/extensions/enums/push_token_rollout_state_extension.dart new file mode 100644 index 000000000..565615a0b --- /dev/null +++ b/lib/model/extensions/enums/push_token_rollout_state_extension.dart @@ -0,0 +1,25 @@ +import '../../../l10n/app_localizations.dart'; +import '../../enums/push_token_rollout_state.dart'; + +extension PushTokenRollOutStateX on PushTokenRollOutState { + bool get rollOutInProgress => switch (this) { + PushTokenRollOutState.rolloutNotStarted => false, + PushTokenRollOutState.generatingRSAKeyPair => true, + PushTokenRollOutState.generatingRSAKeyPairFailed => false, + PushTokenRollOutState.sendRSAPublicKey => true, + PushTokenRollOutState.sendRSAPublicKeyFailed => false, + PushTokenRollOutState.parsingResponse => true, + PushTokenRollOutState.parsingResponseFailed => false, + PushTokenRollOutState.rolloutComplete => false, + }; + String rolloutMsg(AppLocalizations localizations) => switch (this) { + PushTokenRollOutState.rolloutNotStarted => localizations.rollingOut, + PushTokenRollOutState.generatingRSAKeyPair => localizations.generatingRSAKeyPair, + PushTokenRollOutState.generatingRSAKeyPairFailed => localizations.generatingRSAKeyPairFailed, + PushTokenRollOutState.sendRSAPublicKey => localizations.sendingRSAPublicKey, + PushTokenRollOutState.sendRSAPublicKeyFailed => localizations.sendingRSAPublicKeyFailed, + PushTokenRollOutState.parsingResponse => localizations.parsingResponse, + PushTokenRollOutState.parsingResponseFailed => localizations.parsingResponseFailed, + PushTokenRollOutState.rolloutComplete => localizations.rolloutCompleted, + }; +} diff --git a/lib/model/extensions/enums/token_import_type_extension.dart b/lib/model/extensions/enums/token_import_type_extension.dart new file mode 100644 index 000000000..b1dd8ce19 --- /dev/null +++ b/lib/model/extensions/enums/token_import_type_extension.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +import '../../../l10n/app_localizations.dart'; +import '../../enums/token_import_type.dart'; + +extension TokenImportTypeExtension on TokenImportType { + IconData get icon => switch (this) { + const (TokenImportType.backupFile) => Icons.file_present, + const (TokenImportType.qrScan) => Icons.qr_code_scanner, + const (TokenImportType.qrFile) => Icons.qr_code_2, + const (TokenImportType.link) => Icons.link, + }; + + String buttonText(AppLocalizations localizations) => switch (this) { + const (TokenImportType.backupFile) => localizations.selectFile, + const (TokenImportType.qrScan) => localizations.scanQrCode, + const (TokenImportType.qrFile) => localizations.selectFile, + const (TokenImportType.link) => localizations.enterLink, + }; +} diff --git a/lib/model/extensions/enums/token_origin_source_type.dart b/lib/model/extensions/enums/token_origin_source_type.dart new file mode 100644 index 000000000..03220acfa --- /dev/null +++ b/lib/model/extensions/enums/token_origin_source_type.dart @@ -0,0 +1,17 @@ +import '../../../mains/main_netknights.dart'; +import '../../enums/token_origin_source_type.dart'; +import '../../token_import/token_origin_data.dart'; +import '../../tokens/token.dart'; + +extension TokenSourceTypeX on TokenOriginSourceType { + TokenOriginData toTokenOrigin({String data = '', String? appName, bool? isPrivacyIdeaToken, DateTime? createdAt}) => TokenOriginData( + source: this, + data: data, + appName: appName ?? PrivacyIDEAAuthenticator.currentCustomization?.appName, + isPrivacyIdeaToken: isPrivacyIdeaToken, + createdAt: createdAt ?? DateTime.now(), + ); + + T addOriginToToken({required T token, required String data, required bool? isPrivacyIdeaToken, String? appName, DateTime? createdAt}) => + token.copyWith(origin: toTokenOrigin(data: data, appName: appName, isPrivacyIdeaToken: isPrivacyIdeaToken, createdAt: createdAt)) as T; +} diff --git a/lib/model/states/introduction_state.dart b/lib/model/states/introduction_state.dart index 794295efb..3e75fc77d 100644 --- a/lib/model/states/introduction_state.dart +++ b/lib/model/states/introduction_state.dart @@ -1,5 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/introduction_extension.dart'; import '../enums/introduction.dart'; diff --git a/lib/model/tokens/token.dart b/lib/model/tokens/token.dart index 821719608..74f4e7e2d 100644 --- a/lib/model/tokens/token.dart +++ b/lib/model/tokens/token.dart @@ -33,22 +33,22 @@ abstract class Token with SortableMixin { factory Token.fromJson(Map json) { String type = json['type']; - if (TokenTypes.HOTP.isName(type)) return HOTPToken.fromJson(json); - if (TokenTypes.TOTP.isName(type)) return TOTPToken.fromJson(json); - if (TokenTypes.PIPUSH.isName(type)) return PushToken.fromJson(json); - if (TokenTypes.DAYPASSWORD.isName(type)) return DayPasswordToken.fromJson(json); - if (TokenTypes.STEAM.isName(type)) return SteamToken.fromJson(json); + if (TokenTypes.HOTP.isNameInsensitive(type)) return HOTPToken.fromJson(json); + if (TokenTypes.TOTP.isNameInsensitive(type)) return TOTPToken.fromJson(json); + if (TokenTypes.PIPUSH.isNameInsensitive(type)) return PushToken.fromJson(json); + if (TokenTypes.DAYPASSWORD.isNameInsensitive(type)) return DayPasswordToken.fromJson(json); + if (TokenTypes.STEAM.isNameInsensitive(type)) return SteamToken.fromJson(json); throw ArgumentError.value(json, 'Token#fromJson', 'Token type [$type] is not a supported'); } factory Token.fromUriMap( Map uriMap, ) { String type = uriMap[URI_TYPE]; - if (TokenTypes.HOTP.isName(type)) return HOTPToken.fromUriMap(uriMap); - if (TokenTypes.TOTP.isName(type)) return TOTPToken.fromUriMap(uriMap); - if (TokenTypes.PIPUSH.isName(type)) return PushToken.fromUriMap(uriMap); - if (TokenTypes.DAYPASSWORD.isName(type)) return DayPasswordToken.fromUriMap(uriMap); - if (TokenTypes.STEAM.isName(type)) return SteamToken.fromUriMap(uriMap); + if (TokenTypes.HOTP.isNameInsensitive(type)) return HOTPToken.fromUriMap(uriMap); + if (TokenTypes.TOTP.isNameInsensitive(type)) return TOTPToken.fromUriMap(uriMap); + if (TokenTypes.PIPUSH.isNameInsensitive(type)) return PushToken.fromUriMap(uriMap); + if (TokenTypes.DAYPASSWORD.isNameInsensitive(type)) return DayPasswordToken.fromUriMap(uriMap); + if (TokenTypes.STEAM.isNameInsensitive(type)) return SteamToken.fromUriMap(uriMap); throw ArgumentError.value(uriMap, 'Token#fromUriMap', 'Token type [$type] is not a supported'); } diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart index 414eb5bdc..f6fddb5e8 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart @@ -1,3 +1,5 @@ +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; + import '../../../model/enums/token_origin_source_type.dart'; import '../../../model/processor_result.dart'; import '../../../model/tokens/token.dart'; diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart index 49dc80eb9..2e55a0a06 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:base32/base32.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; import '../../../model/enums/token_origin_source_type.dart'; diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart index 531b6d4ad..be75a13b0 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart @@ -73,11 +73,11 @@ class OtpAuthProcessor extends TokenImportSchemeProcessor { /// to https://github.com/google/google-authenticator/wiki/Key-Uri-Format. Map _parseOtpToken(Uri uri) { final type = uri.host; - if (TokenTypes.PIPUSH.isName(type)) { + if (TokenTypes.PIPUSH.isNameInsensitive(type)) { // otpauth://pipush/LABEL?PARAMETERS return _parsePiPushToken(uri); } - if (TokenTypes.values.firstWhereOrNull((element) => element.isName(type)) != null) { + if (TokenTypes.values.firstWhereOrNull((element) => element.isNameInsensitive(type)) != null) { return _parseOtpAuth(uri); } throw ArgumentError.value( diff --git a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart index c377114fe..99bc66929 100644 --- a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart @@ -11,6 +11,7 @@ import 'package:pointycastle/export.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:privacyidea_authenticator/utils/identifiers.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; diff --git a/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart index 79d8ed805..be4c40ba6 100644 --- a/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart @@ -6,6 +6,7 @@ import 'package:cryptography/cryptography.dart'; import 'package:file_selector/file_selector.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/token_types.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart'; import 'package:privacyidea_authenticator/processors/token_import_file_processor/two_fas_import_file_processor.dart'; diff --git a/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart b/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart index ee27ec04f..f9f28d70a 100644 --- a/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart +++ b/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:file_selector/file_selector.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/model/processor_result.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:privacyidea_authenticator/utils/globals.dart'; diff --git a/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart b/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart index d89e934c6..bf8cd425a 100644 --- a/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:cryptography/cryptography.dart'; import 'package:file_selector/file_selector.dart'; import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/utils/token_import_origins.dart'; import '../../l10n/app_localizations.dart'; diff --git a/lib/state_notifiers/token_notifier.dart b/lib/state_notifiers/token_notifier.dart index 1db512e85..249a3cffb 100644 --- a/lib/state_notifiers/token_notifier.dart +++ b/lib/state_notifiers/token_notifier.dart @@ -11,6 +11,8 @@ import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; import 'package:pi_authenticator_legacy/pi_authenticator_legacy.dart'; import 'package:pointycastle/asymmetric/api.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/push_token_rollout_state_extension.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import '../interfaces/repo/token_repository.dart'; import '../l10n/app_localizations.dart'; @@ -649,6 +651,7 @@ class TokenNotifier extends StateNotifier { uri = Uri.parse(qrCode); } catch (_) { showMessage(message: 'The scanned QR code is not a valid URI.', duration: const Duration(seconds: 3)); + Logger.warning('Scanned Data: $qrCode', error: 'Scanned QR code is not a valid URI.', name: 'token_notifier.dart#handleQrCode'); return; } List tokens = await _tokensFromUri(uri); @@ -668,8 +671,9 @@ class TokenNotifier extends StateNotifier { try { final results = await TokenImportSchemeProcessor.processUriByAny(uri); return results?.whereType>().map((e) => e.resultData).toList() ?? []; - } catch (_) { + } catch (error, stackTrace) { showMessage(message: 'The scanned QR code is not a valid URI.', duration: const Duration(seconds: 3)); + Logger.warning('Scanned Data: $uri', error: error, name: 'token_notifier.dart#handleQrCode', stackTrace: stackTrace); return []; } } diff --git a/lib/views/add_token_manually_view/add_token_manually_view.dart b/lib/views/add_token_manually_view/add_token_manually_view.dart index 80c2367d9..538dce2af 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import '../../l10n/app_localizations.dart'; import '../../mains/main_netknights.dart'; diff --git a/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart b/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart index 61aadce78..ae626f1bb 100644 --- a/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart +++ b/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_import_type_extension.dart'; import '../../../l10n/app_localizations.dart'; import '../../../model/enums/token_import_type.dart'; diff --git a/lib/views/import_tokens_view/pages/import_start_page.dart b/lib/views/import_tokens_view/pages/import_start_page.dart index d6b0dec7d..c41c27ba3 100644 --- a/lib/views/import_tokens_view/pages/import_start_page.dart +++ b/lib/views/import_tokens_view/pages/import_start_page.dart @@ -7,6 +7,8 @@ import 'package:flutter/material.dart'; import 'package:image/image.dart' as img; import 'package:privacyidea_authenticator/model/enums/token_import_type.dart'; import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_import_type_extension.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/model/processor_result.dart'; import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface.dart'; import 'package:zxing2/qrcode.dart'; @@ -74,6 +76,7 @@ class _ImportStartPageState extends State { @override Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( title: Text(widget.appName), @@ -103,7 +106,7 @@ class _ImportStartPageState extends State { TextField( controller: _linkController, decoration: InputDecoration( - labelText: AppLocalizations.of(context)!.tokenLink, + labelText: localizations.tokenLink, ), ), ], @@ -113,7 +116,7 @@ class _ImportStartPageState extends State { width: double.infinity, child: ElevatedButton( child: Text( - widget.selectedSource.type.buttonText(context), + widget.selectedSource.type.buttonText(localizations), style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimary), overflow: TextOverflow.fade, softWrap: false, @@ -147,7 +150,8 @@ class _ImportStartPageState extends State { Future _pickBackupFile(TokenImportProcessor? processor) async { assert(processor is TokenImportFileProcessor); final fileProcessor = processor as TokenImportFileProcessor; - final XTypeGroup typeGroup = XTypeGroup(label: AppLocalizations.of(context)!.selectFile); + final localizations = AppLocalizations.of(context)!; + final XTypeGroup typeGroup = XTypeGroup(label: localizations.selectFile); final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]); if (file == null) { Logger.warning("No file selected", name: "_pickAFile#ImportSelectFilePage"); @@ -155,7 +159,7 @@ class _ImportStartPageState extends State { } if (await fileProcessor.fileIsValid(file: file) == false) { if (mounted == false) return; - setState(() => _errorText = AppLocalizations.of(context)!.invalidBackupFile(widget.appName)); + setState(() => _errorText = localizations.invalidBackupFile(widget.appName)); return; } setState(() => _errorText = null); @@ -166,7 +170,7 @@ class _ImportStartPageState extends State { var importResults = await fileProcessor.processFile(file: file); if (importResults.isEmpty) { if (mounted == false) return; - setState(() => _errorText = AppLocalizations.of(context)!.invalidBackupFile(widget.appName)); + setState(() => _errorText = localizations.invalidBackupFile(widget.appName)); return; } String fileString; @@ -194,6 +198,7 @@ class _ImportStartPageState extends State { Future _scanQrCode(TokenImportProcessor? processor) async { assert(processor is TokenImportSchemeProcessor); + final localizations = AppLocalizations.of(context)!; final schemeProcessor = processor as TokenImportSchemeProcessor; final result = await Navigator.of(context).pushNamed(QRScannerView.routeName); if (result is! String) return; @@ -202,7 +207,7 @@ class _ImportStartPageState extends State { uri = Uri.parse(result); } on FormatException catch (_) { if (mounted == false) return; - setState(() => _errorText = AppLocalizations.of(context)!.invalidQrScan(widget.appName)); + setState(() => _errorText = localizations.invalidQrScan(widget.appName)); return; } var results = await schemeProcessor.processUri(uri); @@ -224,28 +229,29 @@ class _ImportStartPageState extends State { Future _pickQrFile(TokenImportProcessor? processor) async { assert(processor is TokenImportSchemeProcessor); final schemeProcessor = processor as TokenImportSchemeProcessor; + final localizations = AppLocalizations.of(context)!; final XFile? file = await openFile(); if (file == null) return; Result qrResult; try { qrResult = await _startDecodeQrFile(file); } on FormatReaderException catch (_) { - setState(() => _errorText = AppLocalizations.of(context)!.qrFileDecodeError); + setState(() => _errorText = localizations.qrFileDecodeError); return; } catch (e) { - setState(() => _errorText = AppLocalizations.of(context)!.invalidQrFile(widget.appName)); + setState(() => _errorText = localizations.invalidQrFile(widget.appName)); return; } final Uri uri; try { uri = Uri.parse(qrResult.text); } on FormatException catch (_) { - setState(() => _errorText = AppLocalizations.of(context)!.invalidQrFile(widget.appName)); + setState(() => _errorText = localizations.invalidQrFile(widget.appName)); return; } var processorResults = await schemeProcessor.processUri(uri); if (processorResults.isEmpty) { - setState(() => _errorText = AppLocalizations.of(context)!.invalidQrFile(widget.appName)); + setState(() => _errorText = localizations.invalidQrFile(widget.appName)); return; } processorResults = processorResults.map>((t) { @@ -283,17 +289,18 @@ class _ImportStartPageState extends State { return; } assert(processor is TokenImportSchemeProcessor); + final localizations = AppLocalizations.of(context)!; final schemeProcessor = processor as TokenImportSchemeProcessor; final Uri uri; try { uri = Uri.parse(_linkController.text); } on FormatException catch (_) { - setState(() => _errorText = AppLocalizations.of(context)!.invalidLink(widget.appName)); + setState(() => _errorText = localizations.invalidLink(widget.appName)); return; } var results = await schemeProcessor.processUri(uri); if (results.isEmpty) { - setState(() => _errorText = AppLocalizations.of(context)!.invalidLink(widget.appName)); + setState(() => _errorText = localizations.invalidLink(widget.appName)); return; } results = results.map>((t) { diff --git a/lib/views/import_tokens_view/pages/select_import_type_page.dart b/lib/views/import_tokens_view/pages/select_import_type_page.dart index 9cdc43a3e..1dff759b0 100644 --- a/lib/views/import_tokens_view/pages/select_import_type_page.dart +++ b/lib/views/import_tokens_view/pages/select_import_type_page.dart @@ -2,6 +2,7 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_import_type_extension.dart'; import '../../../l10n/app_localizations.dart'; import '../../../model/enums/token_import_type.dart'; @@ -16,6 +17,7 @@ class SelectImportTypePage extends StatelessWidget { @override Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( title: Text(tokenImportOrigin.appName), @@ -37,7 +39,7 @@ class SelectImportTypePage extends StatelessWidget { ), const SizedBox(height: ImportTokensView.itemSpacingHorizontal), Text( - AppLocalizations.of(context)!.selectImportType, + localizations.selectImportType, textAlign: TextAlign.center, ), const SizedBox(height: ImportTokensView.itemSpacingHorizontal), @@ -51,10 +53,10 @@ class SelectImportTypePage extends StatelessWidget { flex: 8, child: Text( switch (importEntity.type) { - const (TokenImportType.backupFile) => AppLocalizations.of(context)!.selectFile, - const (TokenImportType.qrScan) => AppLocalizations.of(context)!.scanQrCode, - const (TokenImportType.qrFile) => AppLocalizations.of(context)!.selectFile, - const (TokenImportType.link) => AppLocalizations.of(context)!.enterLink, + const (TokenImportType.backupFile) => localizations.selectFile, + const (TokenImportType.qrScan) => localizations.scanQrCode, + const (TokenImportType.qrFile) => localizations.selectFile, + const (TokenImportType.link) => localizations.enterLink, }, style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimary), overflow: TextOverflow.fade, diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart index c9f8df4eb..852705fda 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/push_token_rollout_state_extension.dart'; import '../../../../../l10n/app_localizations.dart'; -import '../../../../../model/enums/push_token_rollout_state.dart'; import '../../../../../model/tokens/push_token.dart'; import '../../../../../utils/globals.dart'; import '../../../../../utils/riverpod_providers.dart'; @@ -16,6 +16,7 @@ class RolloutFailedWidget extends StatelessWidget { @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; + final localizations = AppLocalizations.of(context)!; return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -24,7 +25,7 @@ class RolloutFailedWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 6.0), child: FittedBox( child: Text( - token.rolloutState.rolloutMsg(context), + token.rolloutState.rolloutMsg(localizations), style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center, ), @@ -38,7 +39,7 @@ class RolloutFailedWidget extends StatelessWidget { child: PressButton( onPressed: () => globalRef?.read(tokenProvider.notifier).rolloutPushToken(token), child: Text( - AppLocalizations.of(context)!.retryRollout, + localizations.retryRollout, style: Theme.of(context).textTheme.bodyMedium, overflow: TextOverflow.fade, softWrap: false, @@ -52,7 +53,7 @@ class RolloutFailedWidget extends StatelessWidget { style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.errorContainer)), onPressed: () => _showDialog(), child: Text( - AppLocalizations.of(context)!.delete, + localizations.delete, style: Theme.of(context).textTheme.bodyMedium, overflow: TextOverflow.fade, softWrap: false, @@ -70,17 +71,18 @@ class RolloutFailedWidget extends StatelessWidget { useRootNavigator: false, context: globalNavigatorKey.currentContext!, builder: (BuildContext context) { + final localizations = AppLocalizations.of(context)!; return DefaultDialog( scrollable: true, title: Text( - AppLocalizations.of(context)!.confirmDeletion, + localizations.confirmDeletion, ), - content: Text(AppLocalizations.of(context)!.confirmDeletionOf(token.label)), + content: Text(localizations.confirmDeletionOf(token.label)), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text( - AppLocalizations.of(context)!.cancel, + localizations.cancel, overflow: TextOverflow.fade, softWrap: false, ), @@ -91,7 +93,7 @@ class RolloutFailedWidget extends StatelessWidget { Navigator.of(context).pop(); }, child: Text( - AppLocalizations.of(context)!.delete, + localizations.delete, overflow: TextOverflow.fade, softWrap: false, ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart index 77c364540..d9fd029e6 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/push_token_rollout_state_extension.dart'; -import '../../../../../model/enums/push_token_rollout_state.dart'; +import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/tokens/push_token.dart'; class RolloutWidget extends StatelessWidget { @@ -13,7 +14,7 @@ class RolloutWidget extends StatelessWidget { children: [ const CircularProgressIndicator(), Text( - token.rolloutState.rolloutMsg(context), + token.rolloutState.rolloutMsg(AppLocalizations.of(context)!), style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, overflow: TextOverflow.fade, diff --git a/lib/widgets/dialog_widgets/patch_notes_dialog.dart b/lib/widgets/dialog_widgets/patch_notes_dialog.dart index c472fd934..4aa216ff9 100644 --- a/lib/widgets/dialog_widgets/patch_notes_dialog.dart +++ b/lib/widgets/dialog_widgets/patch_notes_dialog.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/patch_note_type_extension.dart'; import '../../l10n/app_localizations.dart'; import '../../model/enums/patch_note_type.dart'; diff --git a/test/unit_test/state_notifiers/token_notifier_test.dart b/test/unit_test/state_notifiers/token_notifier_test.dart index c283c4112..b03d66901 100644 --- a/test/unit_test/state_notifiers/token_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_notifier_test.dart @@ -9,6 +9,7 @@ import 'package:privacyidea_authenticator/interfaces/repo/token_repository.dart' import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/push_token_rollout_state.dart'; import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/model/states/token_state.dart'; import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; From 673834ca53fde81982c7b43774bdca1adfb24628 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Mon, 8 Apr 2024 12:22:25 +0200 Subject: [PATCH 04/11] added more tests --- lib/l10n/app_localizations_cs.dart | 41 ++++--- lib/model/enums/algorithms.dart | 1 + lib/model/enums/app_feature.dart | 1 + .../enums/day_password_token_view_mode.dart | 2 +- lib/model/enums/token_origin_source_type.dart | 2 +- lib/model/enums/token_types.dart | 2 +- lib/model/extensions/color_extension.dart | 23 ++-- lib/model/extensions/enum_extension.dart | 3 +- .../enums/algorithms_extension.dart | 9 +- .../enums/introduction_extension.dart | 2 +- .../push_token_rollout_state_extension.dart | 12 ++ lib/model/extensions/int_extension.dart | 2 + .../extensions/theme_mode_extension.dart | 16 --- lib/model/tokens/token.dart | 20 ++-- .../otp_auth_processor.dart | 4 +- ...rivacyidea_authenticator_qr_processor.dart | 2 +- .../model/enums/algorithms_test.dart | 1 - .../model/enums/app_feature_test.dart | 17 --- .../day_passoword_token_view_mode_test.dart | 1 - .../unit_test/model/enums/encodings_test.dart | 1 - .../model/enums/introduction_test.dart | 1 - .../model/enums/patch_note_type_test.dart | 1 - .../enums/push_token_rollout_state_test.dart | 0 .../model/enums/token_import_type_test.dart | 0 .../enums/token_origin_source_type_test.dart | 1 - .../model/enums/token_types_test.dart | 1 - .../extensions/color_extension_test.dart | 1 - .../model/extensions/enum_extension_test.dart | 38 +++++++ .../enums/algorithms_extension_test.dart | 105 ++++++++++++++++++ .../enums/encodings_extension_test.dart | 81 ++++++++++++++ ...sh_token_rollout_state_extension_test.dart | 32 ++++++ ...ken_origin_source_type_extension_test.dart | 60 ++++++++++ .../model/extensions/int_extension_test.dart | 96 ++++++++++++++++ .../extensions/theme_mode_extension_test.dart | 1 - 34 files changed, 488 insertions(+), 92 deletions(-) delete mode 100644 lib/model/extensions/theme_mode_extension.dart delete mode 100644 test/unit_test/model/enums/algorithms_test.dart delete mode 100644 test/unit_test/model/enums/app_feature_test.dart delete mode 100644 test/unit_test/model/enums/day_passoword_token_view_mode_test.dart delete mode 100644 test/unit_test/model/enums/encodings_test.dart delete mode 100644 test/unit_test/model/enums/introduction_test.dart delete mode 100644 test/unit_test/model/enums/patch_note_type_test.dart delete mode 100644 test/unit_test/model/enums/push_token_rollout_state_test.dart delete mode 100644 test/unit_test/model/enums/token_import_type_test.dart delete mode 100644 test/unit_test/model/enums/token_origin_source_type_test.dart delete mode 100644 test/unit_test/model/enums/token_types_test.dart delete mode 100644 test/unit_test/model/extensions/color_extension_test.dart create mode 100644 test/unit_test/model/extensions/enums/algorithms_extension_test.dart create mode 100644 test/unit_test/model/extensions/enums/encodings_extension_test.dart create mode 100644 test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart create mode 100644 test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart delete mode 100644 test/unit_test/model/extensions/theme_mode_extension_test.dart diff --git a/lib/l10n/app_localizations_cs.dart b/lib/l10n/app_localizations_cs.dart index 98a0b2b47..0ff522876 100644 --- a/lib/l10n/app_localizations_cs.dart +++ b/lib/l10n/app_localizations_cs.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_super_parameters + import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; @@ -22,7 +24,8 @@ class AppLocalizationsCs extends AppLocalizations { String get patchNotesV4_3_1Improvement1 => 'Skener QR kódů byl vylepšen.'; @override - String get patchNotesV4_3_0NewFeatures1 => 'Přidána podpora pro import tokenů z Google, Aegis a 2FAS Authenticator. Další zdroje importu budou přidány v budoucnu.'; + String get patchNotesV4_3_0NewFeatures1 => + 'Přidána podpora pro import tokenů z Google, Aegis a 2FAS Authenticator. Další zdroje importu budou přidány v budoucnu.'; @override String get patchNotesV4_3_0NewFeatures2 => 'Do nastavení byla přidána možnost zpětné vazby.'; @@ -108,7 +111,8 @@ class AppLocalizationsCs extends AppLocalizations { } @override - String get confirmTokenDeletionHint => 'Pokud tento token odstraníte, nebude již možné se přihlásit.\nProsím, ujistěte se, že se můžete přihlásit k přidruženému účtu bez tohoto tokenu.'; + String get confirmTokenDeletionHint => + 'Pokud tento token odstraníte, nebude již možné se přihlásit.\nProsím, ujistěte se, že se můžete přihlásit k přidruženému účtu bez tohoto tokenu.'; @override String get confirmFolderDeletionHint => 'Odstranění složky nemá žádný vliv na tokeny v ní.\nTokeny jsou přesunuty do hlavního seznamu.'; @@ -329,7 +333,8 @@ class AppLocalizationsCs extends AppLocalizations { String get send => 'Odeslat'; @override - String get sendErrorLogDescription => 'Vytvoří se připravený e-mail.\nObsahuje informace o aplikaci, chybě a zařízení.\nPřed odesláním můžete e-mail upravit.\nZde se můžete podívat, jak informace používáme:'; + String get sendErrorLogDescription => + 'Vytvoří se připravený e-mail.\nObsahuje informace o aplikaci, chybě a zařízení.\nPřed odesláním můžete e-mail upravit.\nZde se můžete podívat, jak informace používáme:'; @override String get showPrivacyPolicy => 'Zobrazit zásady ochrany osobních údajů'; @@ -356,7 +361,8 @@ class AppLocalizationsCs extends AppLocalizations { String get open => 'Otevřít'; @override - String get sendErrorDialogBody => 'V aplikaci se vyskytla neznámá chyba. Informace uvedené níže mohou být odeslány vývojářům e-mailem pro vyřešení chyby v budoucnu.'; + String get sendErrorDialogBody => + 'V aplikaci se vyskytla neznámá chyba. Informace uvedené níže mohou být odeslány vývojářům e-mailem pro vyřešení chyby v budoucnu.'; @override String get noFbToken => 'Není k dispozici žádný token Firebase.'; @@ -489,7 +495,8 @@ class AppLocalizationsCs extends AppLocalizations { String get decryptErrorTitle => 'Chyba dešifrování'; @override - String get decryptErrorContent => 'Bohužel se aplikaci nepodařilo dešifrovat vaše tokeny. To znamená, že šifrovací klíč je poškozen. Můžete to zkusit znovu nebo odstranit data aplikace, čímž by došlo k odstranění tokenů v aplikaci.'; + String get decryptErrorContent => + 'Bohužel se aplikaci nepodařilo dešifrovat vaše tokeny. To znamená, že šifrovací klíč je poškozen. Můžete to zkusit znovu nebo odstranit data aplikace, čímž by došlo k odstranění tokenů v aplikaci.'; @override String get decryptErrorButtonDelete => 'Odstranit'; @@ -551,7 +558,8 @@ class AppLocalizationsCs extends AppLocalizations { } @override - String get legacySigningErrorMessage => 'Token byl vytvořen v zastaralé verzi aplikace, což může vést k problémům při jeho používání.\nPokud problém přetrvává, doporučujeme vytvořit nový push token!'; + String get legacySigningErrorMessage => + 'Token byl vytvořen v zastaralé verzi aplikace, což může vést k problémům při jeho používání.\nPokud problém přetrvává, doporučujeme vytvořit nový push token!'; @override String get selectImportSource => 'Vyberte zdroj importu'; @@ -661,7 +669,8 @@ class AppLocalizationsCs extends AppLocalizations { String get importHint2FAS => 'Vyberte zálohu 2FAS.\nPokud nemáte zálohu, vytvořte ji v aplikaci 2FAS. Doporučujeme použít heslo.'; @override - String get importHintAegisBackupFile => 'Vyberte svůj export Aegis (.JSON).\nPokud nemáte export, vytvořte si jej prostřednictvím nabídky nastavení v aplikaci Aegis. Doporučujeme použít heslo.'; + String get importHintAegisBackupFile => + 'Vyberte svůj export Aegis (.JSON).\nPokud nemáte export, vytvořte si jej prostřednictvím nabídky nastavení v aplikaci Aegis. Doporučujeme použít heslo.'; @override String get importHintAegisQrScan => 'Naskenujte QR kód, který obdržíte při přenosu záznamů z aplikace Aegis.'; @@ -673,16 +682,19 @@ class AppLocalizationsCs extends AppLocalizations { String get importHintGoogleQrScan => 'Naskenujte QR kód, který obdržíte při exportu účtů z Google Authenticator.'; @override - String get importHintGoogleQrFile => 'Vyberte obrazový soubor s QR kódem, který obdržíte při exportu účtů z Google Authenticator.\n!! Upozorňujeme, že není bezpečné ukládat QR kód do zařízení, protože tokeny nejsou šifrovány !!'; + String get importHintGoogleQrFile => + 'Vyberte obrazový soubor s QR kódem, který obdržíte při exportu účtů z Google Authenticator.\n!! Upozorňujeme, že není bezpečné ukládat QR kód do zařízení, protože tokeny nejsou šifrovány !!'; @override - String get importHintAuthenticatorProFile => 'Chcete-li vytvořit zálohu aplikace Authenticator Pro, přejděte do nastavení a klepněte na položku \"Automatické zálohování\". Vyberte umístění úložiště a nastavte heslo. Poté stiskněte \"Zálohovat nyní\" a exportujte tokeny.'; + String get importHintAuthenticatorProFile => + 'Chcete-li vytvořit zálohu aplikace Authenticator Pro, přejděte do nastavení a klepněte na položku \"Automatické zálohování\". Vyberte umístění úložiště a nastavte heslo. Poté stiskněte \"Zálohovat nyní\" a exportujte tokeny.'; @override String get importHintFreeOtpPlusQrScan => 'Naskenujte QR kód, který obdržíte po stisknutí tří teček na dlaždici tokenu, a vyberte možnost \"Sdílet QR kód\".'; @override - String get importHintFreeOtpPlusFile => 'Chcete-li vytvořit zálohu aplikace FreeOTP+, klepněte na tři tečky v pravém horním rohu a vyberte možnost \"Exportovat\". Můžete si vybrat mezi formátem JSON a URI. Zálohu doporučujeme po importu odstranit, protože není šifrovaná.'; + String get importHintFreeOtpPlusFile => + 'Chcete-li vytvořit zálohu aplikace FreeOTP+, klepněte na tři tečky v pravém horním rohu a vyberte možnost \"Exportovat\". Můžete si vybrat mezi formátem JSON a URI. Zálohu doporučujeme po importu odstranit, protože není šifrovaná.'; @override String get qrFileDecodeError => 'Z vybraného obrázku nebylo možné dekódovat QR kód, použijte prosím místo toho skener QR kódů.'; @@ -700,7 +712,8 @@ class AppLocalizationsCs extends AppLocalizations { String get feedbackDescription => 'Pokud máte nějaké dotazy, návrhy nebo problémy, dejte nám prosím vědět.'; @override - String get feedbackHint => 'Otevře se připravený e-mail, který nám můžete zaslat. V případě potřeby budou doplněny informace o vašem zařízení a verzi aplikace. Před odesláním můžete e-mail zkontrolovat a upravit.'; + String get feedbackHint => + 'Otevře se připravený e-mail, který nám můžete zaslat. V případě potřeby budou doplněny informace o vašem zařízení a verzi aplikace. Před odesláním můžete e-mail zkontrolovat a upravit.'; @override String get feedbackPrivacyPolicy1 => 'Odesláním zpětné vazby souhlasíte s našimi '; @@ -730,7 +743,8 @@ class AppLocalizationsCs extends AppLocalizations { String get noMailAppTitle => 'Není nainstalována žádná e-mailová aplikace'; @override - String get noMailAppDescription => 'There is no e-mail app installed or initialised on this device, please try again when you are able to send an email message.'; + String get noMailAppDescription => + 'There is no e-mail app installed or initialised on this device, please try again when you are able to send an email message.'; @override String get authenticationRequest => 'Žádost o ověření'; @@ -746,7 +760,8 @@ class AppLocalizationsCs extends AppLocalizations { } @override - String get pleaseSyncManuallyWhenNetworkIsAvailable => 'Synchronizujte prosím push tokeny ručně prostřednictvím nastavení, když je k dispozici síťové připojení.'; + String get pleaseSyncManuallyWhenNetworkIsAvailable => + 'Synchronizujte prosím push tokeny ručně prostřednictvím nastavení, když je k dispozici síťové připojení.'; @override String get pushTokens => 'Žetony Push'; diff --git a/lib/model/enums/algorithms.dart b/lib/model/enums/algorithms.dart index b0a468d12..fa09abb40 100644 --- a/lib/model/enums/algorithms.dart +++ b/lib/model/enums/algorithms.dart @@ -1,4 +1,5 @@ // ignore_for_file: constant_identifier_names +// Do not rename or remove values, they are used for serialization. Only add new values. enum Algorithms { SHA1, SHA256, diff --git a/lib/model/enums/app_feature.dart b/lib/model/enums/app_feature.dart index 45db738fa..11c4e6f17 100644 --- a/lib/model/enums/app_feature.dart +++ b/lib/model/enums/app_feature.dart @@ -1,3 +1,4 @@ +// Do not rename or remove values, they are used for serialization. Only add new values. enum AppFeature { patchNotes, } diff --git a/lib/model/enums/day_password_token_view_mode.dart b/lib/model/enums/day_password_token_view_mode.dart index 5196afd95..e06569c57 100644 --- a/lib/model/enums/day_password_token_view_mode.dart +++ b/lib/model/enums/day_password_token_view_mode.dart @@ -1,5 +1,5 @@ // ignore_for_file: constant_identifier_names - +// Do not rename or remove values, they are used for serialization. Only add new values. enum DayPasswordTokenViewMode { VALIDFOR, VALIDUNTIL, diff --git a/lib/model/enums/token_origin_source_type.dart b/lib/model/enums/token_origin_source_type.dart index 691aa1a2e..c24d1a34a 100644 --- a/lib/model/enums/token_origin_source_type.dart +++ b/lib/model/enums/token_origin_source_type.dart @@ -1,4 +1,4 @@ -// Do not rename any value, only add new values at the end of the list. The order of values must not change. +// Do not rename or remove values, they are used for serialization. Only add new values. enum TokenOriginSourceType { backupFile, qrScan, diff --git a/lib/model/enums/token_types.dart b/lib/model/enums/token_types.dart index 21f203b6e..a4756ce86 100644 --- a/lib/model/enums/token_types.dart +++ b/lib/model/enums/token_types.dart @@ -1,5 +1,5 @@ // ignore_for_file: constant_identifier_names - +// Do not rename or remove values, they are used for serialization. Only add new values. enum TokenTypes { HOTP, TOTP, diff --git a/lib/model/extensions/color_extension.dart b/lib/model/extensions/color_extension.dart index a8a207cb3..0c3d53011 100644 --- a/lib/model/extensions/color_extension.dart +++ b/lib/model/extensions/color_extension.dart @@ -1,21 +1,12 @@ import 'dart:ui'; extension ColorExtension on Color { - Color mixWith(Color other) { - return Color.fromARGB( - (alpha + other.alpha) ~/ 2.clamp(0, 255), - (red + other.red) ~/ 2.clamp(0, 255), - (green + other.green) ~/ 2.clamp(0, 255), - (blue + other.blue) ~/ 2.clamp(0, 255), - ); - } + Color mixWith(Color other) => Color.fromARGB( + (alpha + other.alpha) ~/ 2.clamp(0, 255), + (red + other.red) ~/ 2.clamp(0, 255), + (green + other.green) ~/ 2.clamp(0, 255), + (blue + other.blue) ~/ 2.clamp(0, 255), + ); - Color opposite() { - return Color.fromARGB( - alpha, - 255 - red, - 255 - green, - 255 - blue, - ); - } + Color inverted() => Color.fromARGB(alpha, 255 - red, 255 - green, 255 - blue); } diff --git a/lib/model/extensions/enum_extension.dart b/lib/model/extensions/enum_extension.dart index 01a0ff169..301f32513 100644 --- a/lib/model/extensions/enum_extension.dart +++ b/lib/model/extensions/enum_extension.dart @@ -1,4 +1,3 @@ extension EnumExtension on Enum { - bool isName(String enumName) => enumName == name; - bool isNameInsensitive(String enumName) => enumName.toLowerCase() == name.toLowerCase(); + bool isName(String enumName, {bool caseSensitive = true}) => caseSensitive ? name == enumName : name.toLowerCase() == enumName.toLowerCase(); } diff --git a/lib/model/extensions/enums/algorithms_extension.dart b/lib/model/extensions/enums/algorithms_extension.dart index 93910ce5e..5402adc3e 100644 --- a/lib/model/extensions/enums/algorithms_extension.dart +++ b/lib/model/extensions/enums/algorithms_extension.dart @@ -3,12 +3,15 @@ import 'package:otp/otp.dart'; import '../../enums/algorithms.dart'; extension AlgorithmsX on Algorithms { + /// Generates a Time-based one time password code and return as a 0 padded string. + /// DateTime should be the current time. + /// Ig isGoogle is true, the secret will be decoded as base32, otherwise it will be decoded as utf8. String generateTOTPCodeString({ required String secret, required DateTime time, required int length, required Duration interval, - required bool isGoogle, + bool isGoogle = true, }) => switch (this) { Algorithms.SHA1 => OTP.generateTOTPCodeString(secret, time.millisecondsSinceEpoch, @@ -19,11 +22,13 @@ extension AlgorithmsX on Algorithms { length: length, interval: interval.inSeconds, algorithm: Algorithm.SHA512, isGoogle: isGoogle), }; + /// Generates a Counter-based one time password code and return as a 0 padded string. + /// If isGoogle is true, the secret will be decoded as base32, otherwise it will be decoded as utf8. String generateHOTPCodeString({ required String secret, required int counter, required int length, - required bool isGoogle, + bool isGoogle = true, }) => switch (this) { Algorithms.SHA1 => OTP.generateHOTPCodeString(secret, counter, length: length, algorithm: Algorithm.SHA1, isGoogle: isGoogle), diff --git a/lib/model/extensions/enums/introduction_extension.dart b/lib/model/extensions/enums/introduction_extension.dart index 7785570c2..cc319b1fb 100644 --- a/lib/model/extensions/enums/introduction_extension.dart +++ b/lib/model/extensions/enums/introduction_extension.dart @@ -32,7 +32,7 @@ extension IntroductionX on Introduction { }; String hintText(AppLocalizations localizations) => switch (this) { - Introduction.introductionScreen => '', + Introduction.introductionScreen => 'Not implemented', Introduction.scanQrCode => localizations.introScanQrCode, Introduction.addManually => localizations.introAddTokenManually, Introduction.tokenSwipe => localizations.introTokenSwipe, diff --git a/lib/model/extensions/enums/push_token_rollout_state_extension.dart b/lib/model/extensions/enums/push_token_rollout_state_extension.dart index 565615a0b..925ecbd64 100644 --- a/lib/model/extensions/enums/push_token_rollout_state_extension.dart +++ b/lib/model/extensions/enums/push_token_rollout_state_extension.dart @@ -12,6 +12,18 @@ extension PushTokenRollOutStateX on PushTokenRollOutState { PushTokenRollOutState.parsingResponseFailed => false, PushTokenRollOutState.rolloutComplete => false, }; + + PushTokenRollOutState getFailed() => switch (this) { + PushTokenRollOutState.rolloutNotStarted => PushTokenRollOutState.rolloutNotStarted, + PushTokenRollOutState.generatingRSAKeyPair => PushTokenRollOutState.generatingRSAKeyPairFailed, + PushTokenRollOutState.generatingRSAKeyPairFailed => PushTokenRollOutState.generatingRSAKeyPairFailed, + PushTokenRollOutState.sendRSAPublicKey => PushTokenRollOutState.sendRSAPublicKeyFailed, + PushTokenRollOutState.sendRSAPublicKeyFailed => PushTokenRollOutState.sendRSAPublicKeyFailed, + PushTokenRollOutState.parsingResponse => PushTokenRollOutState.parsingResponseFailed, + PushTokenRollOutState.parsingResponseFailed => PushTokenRollOutState.parsingResponseFailed, + PushTokenRollOutState.rolloutComplete => PushTokenRollOutState.rolloutComplete, + }; + String rolloutMsg(AppLocalizations localizations) => switch (this) { PushTokenRollOutState.rolloutNotStarted => localizations.rollingOut, PushTokenRollOutState.generatingRSAKeyPair => localizations.generatingRSAKeyPair, diff --git a/lib/model/extensions/int_extension.dart b/lib/model/extensions/int_extension.dart index 334753970..2dcace0cd 100644 --- a/lib/model/extensions/int_extension.dart +++ b/lib/model/extensions/int_extension.dart @@ -2,6 +2,8 @@ import 'dart:math' as math; import 'dart:typed_data'; extension IntExtension on int { + static const int maxInteger = 0x7FFFFFFFFFFFFFFF; + static const int minInteger = -0x8000000000000000; Uint8List get bytes { int long = this; final byteArray = Uint8List(8); diff --git a/lib/model/extensions/theme_mode_extension.dart b/lib/model/extensions/theme_mode_extension.dart deleted file mode 100644 index 664912904..000000000 --- a/lib/model/extensions/theme_mode_extension.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; - -extension ThemeModeExtension on ThemeMode { - String get name { - switch (this) { - case ThemeMode.system: - return 'System'; - case ThemeMode.light: - return 'Light'; - case ThemeMode.dark: - return 'Dark'; - default: - return 'Unknown'; - } - } -} diff --git a/lib/model/tokens/token.dart b/lib/model/tokens/token.dart index 74f4e7e2d..deefd2e51 100644 --- a/lib/model/tokens/token.dart +++ b/lib/model/tokens/token.dart @@ -33,22 +33,22 @@ abstract class Token with SortableMixin { factory Token.fromJson(Map json) { String type = json['type']; - if (TokenTypes.HOTP.isNameInsensitive(type)) return HOTPToken.fromJson(json); - if (TokenTypes.TOTP.isNameInsensitive(type)) return TOTPToken.fromJson(json); - if (TokenTypes.PIPUSH.isNameInsensitive(type)) return PushToken.fromJson(json); - if (TokenTypes.DAYPASSWORD.isNameInsensitive(type)) return DayPasswordToken.fromJson(json); - if (TokenTypes.STEAM.isNameInsensitive(type)) return SteamToken.fromJson(json); + if (TokenTypes.HOTP.isName(type, caseSensitive: false)) return HOTPToken.fromJson(json); + if (TokenTypes.TOTP.isName(type, caseSensitive: false)) return TOTPToken.fromJson(json); + if (TokenTypes.PIPUSH.isName(type, caseSensitive: false)) return PushToken.fromJson(json); + if (TokenTypes.DAYPASSWORD.isName(type, caseSensitive: false)) return DayPasswordToken.fromJson(json); + if (TokenTypes.STEAM.isName(type, caseSensitive: false)) return SteamToken.fromJson(json); throw ArgumentError.value(json, 'Token#fromJson', 'Token type [$type] is not a supported'); } factory Token.fromUriMap( Map uriMap, ) { String type = uriMap[URI_TYPE]; - if (TokenTypes.HOTP.isNameInsensitive(type)) return HOTPToken.fromUriMap(uriMap); - if (TokenTypes.TOTP.isNameInsensitive(type)) return TOTPToken.fromUriMap(uriMap); - if (TokenTypes.PIPUSH.isNameInsensitive(type)) return PushToken.fromUriMap(uriMap); - if (TokenTypes.DAYPASSWORD.isNameInsensitive(type)) return DayPasswordToken.fromUriMap(uriMap); - if (TokenTypes.STEAM.isNameInsensitive(type)) return SteamToken.fromUriMap(uriMap); + if (TokenTypes.HOTP.isName(type, caseSensitive: false)) return HOTPToken.fromUriMap(uriMap); + if (TokenTypes.TOTP.isName(type, caseSensitive: false)) return TOTPToken.fromUriMap(uriMap); + if (TokenTypes.PIPUSH.isName(type, caseSensitive: false)) return PushToken.fromUriMap(uriMap); + if (TokenTypes.DAYPASSWORD.isName(type, caseSensitive: false)) return DayPasswordToken.fromUriMap(uriMap); + if (TokenTypes.STEAM.isName(type, caseSensitive: false)) return SteamToken.fromUriMap(uriMap); throw ArgumentError.value(uriMap, 'Token#fromUriMap', 'Token type [$type] is not a supported'); } diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart index be75a13b0..182e7742e 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart @@ -73,11 +73,11 @@ class OtpAuthProcessor extends TokenImportSchemeProcessor { /// to https://github.com/google/google-authenticator/wiki/Key-Uri-Format. Map _parseOtpToken(Uri uri) { final type = uri.host; - if (TokenTypes.PIPUSH.isNameInsensitive(type)) { + if (TokenTypes.PIPUSH.isName(type, caseSensitive: false)) { // otpauth://pipush/LABEL?PARAMETERS return _parsePiPushToken(uri); } - if (TokenTypes.values.firstWhereOrNull((element) => element.isNameInsensitive(type)) != null) { + if (TokenTypes.values.firstWhereOrNull((element) => element.isName(type, caseSensitive: false)) != null) { return _parseOtpAuth(uri); } throw ArgumentError.value( diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart index 17689df30..56e564f46 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart @@ -23,7 +23,7 @@ class PrivacyIDEAAuthenticatorQrProcessor extends TokenImportSchemeProcessor { } try { - final token = await TokenEncryption.fromQrCodeUri(uri); + final token = TokenEncryption.fromQrCodeUri(uri); return [ProcessorResult.success(token)]; } catch (e) { diff --git a/test/unit_test/model/enums/algorithms_test.dart b/test/unit_test/model/enums/algorithms_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/model/enums/algorithms_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/model/enums/app_feature_test.dart b/test/unit_test/model/enums/app_feature_test.dart deleted file mode 100644 index 0b380ae64..000000000 --- a/test/unit_test/model/enums/app_feature_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:privacyidea_authenticator/model/enums/app_feature.dart'; - -void main() { - _testAppFeatureX(); -} - -void _testAppFeatureX() { - group('App Feature Extension', () { - test('name', () { - expect((AppFeature.patchNotes.name), equals('patchNotes')); - }); - test('fromName', () { - expect(AppFeature.values.byName('patchNotes'), equals(AppFeature.patchNotes)); - }); - }); -} diff --git a/test/unit_test/model/enums/day_passoword_token_view_mode_test.dart b/test/unit_test/model/enums/day_passoword_token_view_mode_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/model/enums/day_passoword_token_view_mode_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/model/enums/encodings_test.dart b/test/unit_test/model/enums/encodings_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/model/enums/encodings_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/model/enums/introduction_test.dart b/test/unit_test/model/enums/introduction_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/model/enums/introduction_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/model/enums/patch_note_type_test.dart b/test/unit_test/model/enums/patch_note_type_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/model/enums/patch_note_type_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/model/enums/push_token_rollout_state_test.dart b/test/unit_test/model/enums/push_token_rollout_state_test.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/unit_test/model/enums/token_import_type_test.dart b/test/unit_test/model/enums/token_import_type_test.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/unit_test/model/enums/token_origin_source_type_test.dart b/test/unit_test/model/enums/token_origin_source_type_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/model/enums/token_origin_source_type_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/model/enums/token_types_test.dart b/test/unit_test/model/enums/token_types_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/model/enums/token_types_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/model/extensions/color_extension_test.dart b/test/unit_test/model/extensions/color_extension_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/model/extensions/color_extension_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/model/extensions/enum_extension_test.dart b/test/unit_test/model/extensions/enum_extension_test.dart index 8b1378917..322a496e9 100644 --- a/test/unit_test/model/extensions/enum_extension_test.dart +++ b/test/unit_test/model/extensions/enum_extension_test.dart @@ -1 +1,39 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/extensions/enum_extension.dart'; +void main() { + _testEnumExtension(); +} + +enum _TestEnum { + entryOne, + entryTwo, + entryThree, +} + +void _testEnumExtension() { + group('Enum Extension', () { + group('isName', () { + test('caseSensitive', () { + expect(_TestEnum.entryOne.isName('entryOne'), true); + expect(_TestEnum.entryOne.isName('entryone'), false); + expect(_TestEnum.entryOne.isName('entryTwo'), false); + expect(_TestEnum.entryTwo.isName('entryTwo'), true); + expect(_TestEnum.entryTwo.isName('entrytwo'), false); + expect(_TestEnum.entryTwo.isName('entryThree'), false); + expect(_TestEnum.entryThree.isName('entryThree'), true); + expect(_TestEnum.entryThree.isName('entrythree'), false); + }); + test('caseInsensitive', () { + expect(_TestEnum.entryOne.isName('entryone', caseSensitive: false), true); + expect(_TestEnum.entryOne.isName('ENTRYONE', caseSensitive: false), true); + expect(_TestEnum.entryOne.isName('entryTwo', caseSensitive: false), false); + expect(_TestEnum.entryTwo.isName('entrytwo', caseSensitive: false), true); + expect(_TestEnum.entryTwo.isName('ENTRYTWO', caseSensitive: false), true); + expect(_TestEnum.entryTwo.isName('entryThree', caseSensitive: false), false); + expect(_TestEnum.entryThree.isName('entrythree', caseSensitive: false), true); + expect(_TestEnum.entryThree.isName('ENTRYTHREE', caseSensitive: false), true); + }); + }); + }); +} diff --git a/test/unit_test/model/extensions/enums/algorithms_extension_test.dart b/test/unit_test/model/extensions/enums/algorithms_extension_test.dart new file mode 100644 index 000000000..187987702 --- /dev/null +++ b/test/unit_test/model/extensions/enums/algorithms_extension_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/algorithms_extension.dart'; + +void main() { + _testAlgorithmsExtension(); +} + +void _testAlgorithmsExtension() { + group('Algorithms Extension', () { + group('generateTOTPCodeString', () {}); + group('generateHOTPCodeString', () { + group('different couters 6 digits', () { + test('OTP for counter == 0', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + expect(otpValue, equals('328482')); + }); + + test('OTP for counter == 1', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 1, length: 6); + expect(otpValue, equals('812658')); + }); + + test('OTP for counter == 2', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 2, length: 6); + expect(otpValue, equals('073348')); + }); + + test('OTP for counter == 8', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 8, length: 6); + expect(otpValue, equals('985814')); + }); + }); + group('different couters 8 digits', () { + test('OTP for counter == 0', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + expect(otpValue, equals('35328482')); + }); + + test('OTP for counter == 1', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 1, length: 8); + expect(otpValue, equals('30812658')); + }); + + test('OTP for counter == 2', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 2, length: 8); + expect(otpValue, equals('41073348')); + }); + + test('OTP for counter == 8', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 8, length: 8); + expect(otpValue, equals('12985814')); + }); + }); + group('different algorithms 6 digits', () { + test('OTP for sha1', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + expect(otpValue, equals('328482')); + }); + + test('OTP for sha256', () { + final otpValue = Algorithms.SHA256.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + expect(otpValue, equals('356306')); + }); + + test('OTP for sha512', () { + final otpValue = Algorithms.SHA512.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + expect(otpValue, equals('674061')); + }); + }); + group('different algorithms 8 digits', () { + test('OTP for sha1', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + expect(otpValue, equals('35328482')); + }); + + test('OTP for sha256', () { + final otpValue = Algorithms.SHA256.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + expect(otpValue, equals('03356306')); + }); + + test('OTP for sha512', () { + final otpValue = Algorithms.SHA512.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + expect(otpValue, equals('66674061')); + }); + }); + group('is not google', () { + test('OTP for sha1', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6, isGoogle: false); + expect(otpValue, equals('814628')); + }); + + test('OTP for sha256', () { + final otpValue = Algorithms.SHA256.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6, isGoogle: false); + expect(otpValue, equals('059019')); + }); + + test('OTP for sha512', () { + final otpValue = Algorithms.SHA512.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6, isGoogle: false); + expect(otpValue, equals('377469')); + }); + }); + }); + }); +} diff --git a/test/unit_test/model/extensions/enums/encodings_extension_test.dart b/test/unit_test/model/extensions/enums/encodings_extension_test.dart new file mode 100644 index 000000000..c5c53aa43 --- /dev/null +++ b/test/unit_test/model/extensions/enums/encodings_extension_test.dart @@ -0,0 +1,81 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; + +void main() { + _testEncodingsExtension(); +} + +void _testEncodingsExtension() { + group('Encodings Extension', () { + group('encode', () { + group('valid', () { + test('base32', () => expect(Encodings.base32.encode(Uint8List.fromList([153, 37, 57])), equals('TESTS==='))); + test('hex', () => expect(Encodings.hex.encode(Uint8List.fromList([153, 37, 57])), equals('992539'))); + test('none', () => expect(Encodings.none.encode(Uint8List.fromList([116, 101, 115, 116, 115])), equals('tests'))); + }); + + group('invalid', () { + test('none', () => expect(() => Encodings.none.encode(Uint8List.fromList([153, 37, 57])), throwsException)); + }); + }); + group('encodeStringTo', () { + test('base32 to hex', () => expect(Encodings.base32.encodeStringTo(Encodings.hex, 'TESTS==='), equals('992539'))); + test('hex to base32', () => expect(Encodings.hex.encodeStringTo(Encodings.base32, '992539'), equals('TESTS==='))); + }); + + group('decode', () { + group('valid', () { + test('base32', () => expect(Encodings.base32.decode('TESTS==='), equals(Uint8List.fromList([153, 37, 57])))); + test('hex', () => expect(Encodings.hex.decode('992539'), equals(Uint8List.fromList([153, 37, 57])))); + test('none', () => expect(Encodings.none.decode('tests'), equals(Uint8List.fromList([116, 101, 115, 116, 115])))); + }); + + group('invalid', () { + test('base32', () => expect(() => Encodings.base32.decode('TESTS+++'), throwsException)); + test('hex', () => expect(() => Encodings.hex.decode('abcdefg'), throwsException)); + // Every utf8 string has a valid binary representation + }); + }); + + group('isValidEncoding', () { + test('base32', () => expect(Encodings.base32.isValidEncoding('TESTS==='), isTrue)); + test('hex', () => expect(Encodings.hex.isValidEncoding('992539'), isTrue)); + test('none', () => expect(Encodings.none.isValidEncoding('tests'), isTrue)); + }); + + group('isInvalidEncoding', () { + test('base32', () => expect(Encodings.base32.isInvalidEncoding('TESTS==='), isFalse)); + test('hex', () => expect(Encodings.hex.isInvalidEncoding('992539'), isFalse)); + // Every utf8 string has a valid binary representation + }); + + group('tryDecode', () { + group('valid', () { + test('base32', () => expect(Encodings.base32.tryDecode('TESTS==='), equals(Uint8List.fromList([153, 37, 57])))); + test('hex', () => expect(Encodings.hex.tryDecode('992539'), equals(Uint8List.fromList([153, 37, 57])))); + test('none', () => expect(Encodings.none.tryDecode('tests'), equals(Uint8List.fromList([116, 101, 115, 116, 115])))); + }); + + group('invalid', () { + test('base32', () => expect(Encodings.base32.tryDecode('TESTS+++'), isNull)); + test('hex', () => expect(Encodings.hex.tryDecode('abcdefg'), isNull)); + // Every utf8 string has a valid binary representation + }); + }); + + group('tryEncode', () { + group('valid', () { + test('base32', () => expect(Encodings.base32.tryEncode(Uint8List.fromList([153, 37, 57])), equals('TESTS==='))); + test('hex', () => expect(Encodings.hex.tryEncode(Uint8List.fromList([153, 37, 57])), equals('992539'))); + test('none', () => expect(Encodings.none.tryEncode(Uint8List.fromList([116, 101, 115, 116, 115])), equals('tests'))); + }); + group('invalid', () { + // Every binary data can be encoded to base32 and hex + test('none', () => expect(Encodings.none.tryEncode(Uint8List.fromList([153, 37, 57])), isNull)); + }); + }); + }); +} diff --git a/test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart b/test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart new file mode 100644 index 000000000..dd2fa493f --- /dev/null +++ b/test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/push_token_rollout_state.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/push_token_rollout_state_extension.dart'; + +void main() { + _testPushTokenRolloutstateExtension(); +} + +void _testPushTokenRolloutstateExtension() { + group('Push-Token Rolloutstate Extension', () { + test('rollOutInProgress', () { + expect(PushTokenRollOutState.rolloutNotStarted.rollOutInProgress, false); + expect(PushTokenRollOutState.generatingRSAKeyPair.rollOutInProgress, true); + expect(PushTokenRollOutState.generatingRSAKeyPairFailed.rollOutInProgress, false); + expect(PushTokenRollOutState.sendRSAPublicKey.rollOutInProgress, true); + expect(PushTokenRollOutState.sendRSAPublicKeyFailed.rollOutInProgress, false); + expect(PushTokenRollOutState.parsingResponse.rollOutInProgress, true); + expect(PushTokenRollOutState.parsingResponseFailed.rollOutInProgress, false); + expect(PushTokenRollOutState.rolloutComplete.rollOutInProgress, false); + }); + test('getFailed', () { + expect(PushTokenRollOutState.rolloutNotStarted.getFailed(), PushTokenRollOutState.rolloutNotStarted); + expect(PushTokenRollOutState.generatingRSAKeyPair.getFailed(), PushTokenRollOutState.generatingRSAKeyPairFailed); + expect(PushTokenRollOutState.generatingRSAKeyPairFailed.getFailed(), PushTokenRollOutState.generatingRSAKeyPairFailed); + expect(PushTokenRollOutState.sendRSAPublicKey.getFailed(), PushTokenRollOutState.sendRSAPublicKeyFailed); + expect(PushTokenRollOutState.sendRSAPublicKeyFailed.getFailed(), PushTokenRollOutState.sendRSAPublicKeyFailed); + expect(PushTokenRollOutState.parsingResponse.getFailed(), PushTokenRollOutState.parsingResponseFailed); + expect(PushTokenRollOutState.parsingResponseFailed.getFailed(), PushTokenRollOutState.parsingResponseFailed); + expect(PushTokenRollOutState.rolloutComplete.getFailed(), PushTokenRollOutState.rolloutComplete); + }); + }); +} diff --git a/test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart b/test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart new file mode 100644 index 000000000..567c40e1d --- /dev/null +++ b/test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/token_import/token_origin_data.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; + +void main() { + _testTokenOriginSourceTypeExtension(); +} + +void _testTokenOriginSourceTypeExtension() { + group('Token Origin Source Type Extension', () { + test('toTokenOrigin', () { + final TokenOriginData tokenOriginDataMatch = TokenOriginData( + source: TokenOriginSourceType.qrScan, + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), + ); + final TokenOriginData tokenOriginData = TokenOriginSourceType.qrScan.toTokenOrigin( + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), + ); + expect(tokenOriginData.source, tokenOriginDataMatch.source); + expect(tokenOriginData.data, tokenOriginDataMatch.data); + expect(tokenOriginData.appName, tokenOriginDataMatch.appName); + expect(tokenOriginData.isPrivacyIdeaToken, tokenOriginDataMatch.isPrivacyIdeaToken); + expect(tokenOriginData.createdAt, tokenOriginDataMatch.createdAt); + expect(tokenOriginData, tokenOriginDataMatch); + }); + test('addOriginToToken', () { + final token = HOTPToken(id: 'id', algorithm: Algorithms.SHA512, digits: 6, secret: 'secret'); + final TokenOriginData tokenOriginDataMatch = TokenOriginData( + source: TokenOriginSourceType.qrScan, + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), + ); + final tokenMatch = token.copyWith(origin: tokenOriginDataMatch); + final tokenWithOrigin = TokenOriginSourceType.qrScan.addOriginToToken( + token: token, + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), + ); + expect(tokenWithOrigin.origin!.source, tokenOriginDataMatch.source); + expect(tokenWithOrigin.origin!.data, tokenOriginDataMatch.data); + expect(tokenWithOrigin.origin!.appName, tokenOriginDataMatch.appName); + expect(tokenWithOrigin.origin!.isPrivacyIdeaToken, tokenOriginDataMatch.isPrivacyIdeaToken); + expect(tokenWithOrigin.origin!.createdAt, tokenOriginDataMatch.createdAt); + expect(tokenWithOrigin, tokenMatch); + }); + }); +} diff --git a/test/unit_test/model/extensions/int_extension_test.dart b/test/unit_test/model/extensions/int_extension_test.dart index 8b1378917..1cea777c8 100644 --- a/test/unit_test/model/extensions/int_extension_test.dart +++ b/test/unit_test/model/extensions/int_extension_test.dart @@ -1 +1,97 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/extensions/int_extension.dart'; +void main() { + _testIntExtension(); +} + +void _testIntExtension() { + group('int extension', () { + group('bytes', () { + test('min int value', () => expect((-0x8000000000000000).bytes, [128, 0, 0, 0, 0, 0, 0, 0])); + test('zero', () => expect(0.bytes, [0, 0, 0, 0, 0, 0, 0, 0])); + test('max int value', () => expect(0x7FFFFFFFFFFFFFFF.bytes, [127, 255, 255, 255, 255, 255, 255, 255])); + test('20 different int values', () { + expect(8254763140651989312.bytes, [114, 142, 207, 87, 63, 140, 185, 64]); + expect(6867929122700968103.bytes, [95, 79, 201, 38, 53, 38, 192, 167]); + expect(6658070822668124012.bytes, [92, 102, 56, 35, 34, 134, 191, 108]); + expect(6233195836444142436.bytes, [86, 128, 194, 150, 158, 178, 107, 100]); + expect(5665252064165200114.bytes, [78, 159, 4, 188, 143, 157, 128, 242]); + expect(3836812696023088046.bytes, [53, 63, 24, 209, 152, 42, 59, 174]); + expect(4217815205603023728.bytes, [58, 136, 176, 149, 34, 53, 155, 112]); + expect(1859730558376856620.bytes, [25, 207, 23, 22, 237, 253, 204, 44]); + expect((-1341891893474570224).bytes, [237, 96, 164, 110, 186, 120, 208, 16]); + expect((-1947790576164582988).bytes, [228, 248, 14, 202, 114, 219, 73, 180]); + expect((-5204853149876150168).bytes, [183, 196, 165, 162, 253, 143, 32, 104]); + expect((-5765512478999408848).bytes, [175, 252, 200, 242, 133, 40, 143, 48]); + expect((-6273311771369426144).bytes, [168, 240, 184, 46, 110, 63, 179, 32]); + expect((-6384342681465787276).bytes, [167, 102, 66, 40, 42, 221, 236, 116]); + expect((-6805707171905842232).bytes, [161, 141, 69, 98, 165, 57, 83, 200]); + expect((-7708924412950309696).bytes, [149, 4, 102, 23, 13, 194, 148, 192]); + expect((-7731444997339132318).bytes, [148, 180, 99, 188, 229, 33, 78, 98]); + expect((-7855692611255695686).bytes, [146, 250, 249, 48, 249, 113, 134, 186]); + expect((-8557951589827587072).bytes, [137, 60, 12, 94, 251, 86, 84, 0]); + expect((-9153687162235632943).bytes, [128, 247, 146, 6, 53, 228, 66, 209]); + }); + }); + test('digits', () { + expect(0.digits, [0]); + expect(1.digits, [1]); + expect(9.digits, [9]); + expect(10.digits, [0, 1]); + expect(11.digits, [1, 1]); + expect(99.digits, [9, 9]); + expect(100.digits, [0, 0, 1]); + expect(101.digits, [1, 0, 1]); + expect(999.digits, [9, 9, 9]); + expect(1000.digits, [0, 0, 0, 1]); + expect(1001.digits, [1, 0, 0, 1]); + expect(9999.digits, [9, 9, 9, 9]); + expect(10000.digits, [0, 0, 0, 0, 1]); + expect(10001.digits, [1, 0, 0, 0, 1]); + expect(99999.digits, [9, 9, 9, 9, 9]); + expect(100000.digits, [0, 0, 0, 0, 0, 1]); + expect(100001.digits, [1, 0, 0, 0, 0, 1]); + expect(999999.digits, [9, 9, 9, 9, 9, 9]); + expect(1000000.digits, [0, 0, 0, 0, 0, 0, 1]); + expect(1000001.digits, [1, 0, 0, 0, 0, 0, 1]); + expect(9999999.digits, [9, 9, 9, 9, 9, 9, 9]); + expect(10000000.digits, [0, 0, 0, 0, 0, 0, 0, 1]); + expect(10000001.digits, [1, 0, 0, 0, 0, 0, 0, 1]); + expect(99999999.digits, [9, 9, 9, 9, 9, 9, 9, 9]); + }); + test('pow', () { + expect(0.pow(0), 1); + expect(0.pow(1), 0); + expect(1.pow(0), 1); + expect(1.pow(1), 1); + expect(2.pow(0), 1); + expect(2.pow(1), 2); + expect(2.pow(2), 4); + expect(3.pow(1), 3); + expect(3.pow(2), 9); + expect(3.pow(3), 27); + expect(4.pow(2), 16); + expect(4.pow(3), 64); + expect(4.pow(4), 256); + expect(5.pow(3), 125); + expect(5.pow(4), 625); + expect(5.pow(5), 3125); + expect(6.pow(4), 1296); + expect(6.pow(5), 7776); + expect(6.pow(6), 46656); + expect(7.pow(5), 16807); + expect(7.pow(6), 117649); + expect(7.pow(7), 823543); + expect(8.pow(6), 262144); + expect(8.pow(7), 2097152); + expect(8.pow(8), 16777216); + expect(9.pow(7), 4782969); + expect(9.pow(8), 43046721); + expect(9.pow(9), 387420489); + expect(10.pow(8), 100000000); + expect(10.pow(9), 1000000000); + expect(10.pow(10), 10000000000); + }); + }); +} diff --git a/test/unit_test/model/extensions/theme_mode_extension_test.dart b/test/unit_test/model/extensions/theme_mode_extension_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/model/extensions/theme_mode_extension_test.dart +++ /dev/null @@ -1 +0,0 @@ - From 4445c7da132ef5e6932f63204b0f029ca35202f8 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Tue, 9 Apr 2024 12:07:10 +0200 Subject: [PATCH 05/11] fixed token sort --- lib/l10n/app_localizations_cs.dart | 41 ++----- lib/model/mixins/sortable_mixin.dart | 70 ++++++++++- lib/model/states/token_state.dart | 49 +++----- lib/model/token_folder.dart | 5 + lib/model/tokens/day_password_token.dart | 7 +- lib/model/tokens/hotp_token.dart | 9 +- lib/model/tokens/otp_token.dart | 4 +- lib/model/tokens/push_token.dart | 9 +- lib/model/tokens/steam_token.dart | 7 +- lib/model/tokens/token.dart | 4 + lib/model/tokens/totp_token.dart | 9 +- lib/utils/riverpod_state_listener.dart | 9 +- .../drag_target_divider.dart | 116 +++++++----------- .../token_folder_expandable.dart | 30 +++-- .../main_view_tokens_list.dart | 68 +++++----- .../widgets/push_tokens_view_list.dart | 33 +---- lib/widgets/app_wrapper.dart | 1 + 17 files changed, 248 insertions(+), 223 deletions(-) diff --git a/lib/l10n/app_localizations_cs.dart b/lib/l10n/app_localizations_cs.dart index 0ff522876..98a0b2b47 100644 --- a/lib/l10n/app_localizations_cs.dart +++ b/lib/l10n/app_localizations_cs.dart @@ -1,5 +1,3 @@ -// ignore_for_file: use_super_parameters - import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; @@ -24,8 +22,7 @@ class AppLocalizationsCs extends AppLocalizations { String get patchNotesV4_3_1Improvement1 => 'Skener QR kódů byl vylepšen.'; @override - String get patchNotesV4_3_0NewFeatures1 => - 'Přidána podpora pro import tokenů z Google, Aegis a 2FAS Authenticator. Další zdroje importu budou přidány v budoucnu.'; + String get patchNotesV4_3_0NewFeatures1 => 'Přidána podpora pro import tokenů z Google, Aegis a 2FAS Authenticator. Další zdroje importu budou přidány v budoucnu.'; @override String get patchNotesV4_3_0NewFeatures2 => 'Do nastavení byla přidána možnost zpětné vazby.'; @@ -111,8 +108,7 @@ class AppLocalizationsCs extends AppLocalizations { } @override - String get confirmTokenDeletionHint => - 'Pokud tento token odstraníte, nebude již možné se přihlásit.\nProsím, ujistěte se, že se můžete přihlásit k přidruženému účtu bez tohoto tokenu.'; + String get confirmTokenDeletionHint => 'Pokud tento token odstraníte, nebude již možné se přihlásit.\nProsím, ujistěte se, že se můžete přihlásit k přidruženému účtu bez tohoto tokenu.'; @override String get confirmFolderDeletionHint => 'Odstranění složky nemá žádný vliv na tokeny v ní.\nTokeny jsou přesunuty do hlavního seznamu.'; @@ -333,8 +329,7 @@ class AppLocalizationsCs extends AppLocalizations { String get send => 'Odeslat'; @override - String get sendErrorLogDescription => - 'Vytvoří se připravený e-mail.\nObsahuje informace o aplikaci, chybě a zařízení.\nPřed odesláním můžete e-mail upravit.\nZde se můžete podívat, jak informace používáme:'; + String get sendErrorLogDescription => 'Vytvoří se připravený e-mail.\nObsahuje informace o aplikaci, chybě a zařízení.\nPřed odesláním můžete e-mail upravit.\nZde se můžete podívat, jak informace používáme:'; @override String get showPrivacyPolicy => 'Zobrazit zásady ochrany osobních údajů'; @@ -361,8 +356,7 @@ class AppLocalizationsCs extends AppLocalizations { String get open => 'Otevřít'; @override - String get sendErrorDialogBody => - 'V aplikaci se vyskytla neznámá chyba. Informace uvedené níže mohou být odeslány vývojářům e-mailem pro vyřešení chyby v budoucnu.'; + String get sendErrorDialogBody => 'V aplikaci se vyskytla neznámá chyba. Informace uvedené níže mohou být odeslány vývojářům e-mailem pro vyřešení chyby v budoucnu.'; @override String get noFbToken => 'Není k dispozici žádný token Firebase.'; @@ -495,8 +489,7 @@ class AppLocalizationsCs extends AppLocalizations { String get decryptErrorTitle => 'Chyba dešifrování'; @override - String get decryptErrorContent => - 'Bohužel se aplikaci nepodařilo dešifrovat vaše tokeny. To znamená, že šifrovací klíč je poškozen. Můžete to zkusit znovu nebo odstranit data aplikace, čímž by došlo k odstranění tokenů v aplikaci.'; + String get decryptErrorContent => 'Bohužel se aplikaci nepodařilo dešifrovat vaše tokeny. To znamená, že šifrovací klíč je poškozen. Můžete to zkusit znovu nebo odstranit data aplikace, čímž by došlo k odstranění tokenů v aplikaci.'; @override String get decryptErrorButtonDelete => 'Odstranit'; @@ -558,8 +551,7 @@ class AppLocalizationsCs extends AppLocalizations { } @override - String get legacySigningErrorMessage => - 'Token byl vytvořen v zastaralé verzi aplikace, což může vést k problémům při jeho používání.\nPokud problém přetrvává, doporučujeme vytvořit nový push token!'; + String get legacySigningErrorMessage => 'Token byl vytvořen v zastaralé verzi aplikace, což může vést k problémům při jeho používání.\nPokud problém přetrvává, doporučujeme vytvořit nový push token!'; @override String get selectImportSource => 'Vyberte zdroj importu'; @@ -669,8 +661,7 @@ class AppLocalizationsCs extends AppLocalizations { String get importHint2FAS => 'Vyberte zálohu 2FAS.\nPokud nemáte zálohu, vytvořte ji v aplikaci 2FAS. Doporučujeme použít heslo.'; @override - String get importHintAegisBackupFile => - 'Vyberte svůj export Aegis (.JSON).\nPokud nemáte export, vytvořte si jej prostřednictvím nabídky nastavení v aplikaci Aegis. Doporučujeme použít heslo.'; + String get importHintAegisBackupFile => 'Vyberte svůj export Aegis (.JSON).\nPokud nemáte export, vytvořte si jej prostřednictvím nabídky nastavení v aplikaci Aegis. Doporučujeme použít heslo.'; @override String get importHintAegisQrScan => 'Naskenujte QR kód, který obdržíte při přenosu záznamů z aplikace Aegis.'; @@ -682,19 +673,16 @@ class AppLocalizationsCs extends AppLocalizations { String get importHintGoogleQrScan => 'Naskenujte QR kód, který obdržíte při exportu účtů z Google Authenticator.'; @override - String get importHintGoogleQrFile => - 'Vyberte obrazový soubor s QR kódem, který obdržíte při exportu účtů z Google Authenticator.\n!! Upozorňujeme, že není bezpečné ukládat QR kód do zařízení, protože tokeny nejsou šifrovány !!'; + String get importHintGoogleQrFile => 'Vyberte obrazový soubor s QR kódem, který obdržíte při exportu účtů z Google Authenticator.\n!! Upozorňujeme, že není bezpečné ukládat QR kód do zařízení, protože tokeny nejsou šifrovány !!'; @override - String get importHintAuthenticatorProFile => - 'Chcete-li vytvořit zálohu aplikace Authenticator Pro, přejděte do nastavení a klepněte na položku \"Automatické zálohování\". Vyberte umístění úložiště a nastavte heslo. Poté stiskněte \"Zálohovat nyní\" a exportujte tokeny.'; + String get importHintAuthenticatorProFile => 'Chcete-li vytvořit zálohu aplikace Authenticator Pro, přejděte do nastavení a klepněte na položku \"Automatické zálohování\". Vyberte umístění úložiště a nastavte heslo. Poté stiskněte \"Zálohovat nyní\" a exportujte tokeny.'; @override String get importHintFreeOtpPlusQrScan => 'Naskenujte QR kód, který obdržíte po stisknutí tří teček na dlaždici tokenu, a vyberte možnost \"Sdílet QR kód\".'; @override - String get importHintFreeOtpPlusFile => - 'Chcete-li vytvořit zálohu aplikace FreeOTP+, klepněte na tři tečky v pravém horním rohu a vyberte možnost \"Exportovat\". Můžete si vybrat mezi formátem JSON a URI. Zálohu doporučujeme po importu odstranit, protože není šifrovaná.'; + String get importHintFreeOtpPlusFile => 'Chcete-li vytvořit zálohu aplikace FreeOTP+, klepněte na tři tečky v pravém horním rohu a vyberte možnost \"Exportovat\". Můžete si vybrat mezi formátem JSON a URI. Zálohu doporučujeme po importu odstranit, protože není šifrovaná.'; @override String get qrFileDecodeError => 'Z vybraného obrázku nebylo možné dekódovat QR kód, použijte prosím místo toho skener QR kódů.'; @@ -712,8 +700,7 @@ class AppLocalizationsCs extends AppLocalizations { String get feedbackDescription => 'Pokud máte nějaké dotazy, návrhy nebo problémy, dejte nám prosím vědět.'; @override - String get feedbackHint => - 'Otevře se připravený e-mail, který nám můžete zaslat. V případě potřeby budou doplněny informace o vašem zařízení a verzi aplikace. Před odesláním můžete e-mail zkontrolovat a upravit.'; + String get feedbackHint => 'Otevře se připravený e-mail, který nám můžete zaslat. V případě potřeby budou doplněny informace o vašem zařízení a verzi aplikace. Před odesláním můžete e-mail zkontrolovat a upravit.'; @override String get feedbackPrivacyPolicy1 => 'Odesláním zpětné vazby souhlasíte s našimi '; @@ -743,8 +730,7 @@ class AppLocalizationsCs extends AppLocalizations { String get noMailAppTitle => 'Není nainstalována žádná e-mailová aplikace'; @override - String get noMailAppDescription => - 'There is no e-mail app installed or initialised on this device, please try again when you are able to send an email message.'; + String get noMailAppDescription => 'There is no e-mail app installed or initialised on this device, please try again when you are able to send an email message.'; @override String get authenticationRequest => 'Žádost o ověření'; @@ -760,8 +746,7 @@ class AppLocalizationsCs extends AppLocalizations { } @override - String get pleaseSyncManuallyWhenNetworkIsAvailable => - 'Synchronizujte prosím push tokeny ručně prostřednictvím nastavení, když je k dispozici síťové připojení.'; + String get pleaseSyncManuallyWhenNetworkIsAvailable => 'Synchronizujte prosím push tokeny ručně prostřednictvím nastavení, když je k dispozici síťové připojení.'; @override String get pushTokens => 'Žetony Push'; diff --git a/lib/model/mixins/sortable_mixin.dart b/lib/model/mixins/sortable_mixin.dart index 9081babf4..458fd492a 100644 --- a/lib/model/mixins/sortable_mixin.dart +++ b/lib/model/mixins/sortable_mixin.dart @@ -1,8 +1,11 @@ mixin SortableMixin { int? get sortIndex; + int? get dependsOnSortIndex; - SortableMixin copyWith({int? sortIndex}); + SortableMixin copyWith({int? sortIndex, int? Function() dependsOnSortIndex}); + /// Compares the sortIndex of two SortableMixin objects. + /// Null values are considered to be the highest index. int compareTo(SortableMixin other) { if (sortIndex == null) { if (other.sortIndex == null) return 0; @@ -13,3 +16,68 @@ mixin SortableMixin { return sortIndex!.compareTo(other.sortIndex!); } } + +extension SortableList on List { + List get sorted { + var list = List.from(this); + var highestIndex = 0; + for (var item in list) { + if (item.sortIndex != null && item.sortIndex! > highestIndex) { + highestIndex = item.sortIndex!; + } + } + + list.sort((a, b) => a.compareTo(b)); + for (var i = 0; i < list.length; i++) { + if (list[i].sortIndex == null) { + highestIndex++; + list[i] = list[i].copyWith(sortIndex: highestIndex) as T; + } else { + highestIndex = list[i].sortIndex!; + } + } + return list; + } + + List moveBetween({T? moveAfter, required T movedItem, T? moveBefore}) { + var list = List.from(this).sorted.withCurrentSortIndexSet(); + final success = list.remove(movedItem); + if (!success) return list; + final newIndex = moveBefore != null + ? list.indexOf(moveBefore) + : moveAfter != null && list.contains(moveAfter) + ? list.indexOf(moveAfter) + 1 + : list.length; + list.insert(newIndex, movedItem); + list = list.withCurrentSortIndexSet(); + return list; + } + + List moveAllBetween({T? moveAfter, required List movedItems, T? moveBefore}) { + var list = List.from(this).sorted.withCurrentSortIndexSet(); + List removedItems = []; + for (final movedItem in movedItems) { + final success = list.remove(movedItem); + if (success) removedItems.add(movedItem); + } + if (removedItems.isEmpty) return list; + final newIndex = moveBefore != null + ? list.indexOf(moveBefore) + : moveAfter != null && list.contains(moveAfter) + ? list.indexOf(moveAfter) + 1 + : list.length; + list.insertAll(newIndex, removedItems); + list = list.withCurrentSortIndexSet(); + return list; + } + + List withCurrentSortIndexSet() { + final list = List.from(this); + for (var i = 0; i < list.length; i++) { + if (list[i].sortIndex != i) { + list[i] = list[i].copyWith(sortIndex: i) as T; + } + } + return list; + } +} diff --git a/lib/model/states/token_state.dart b/lib/model/states/token_state.dart index 29fcb752e..9f3d2199f 100644 --- a/lib/model/states/token_state.dart +++ b/lib/model/states/token_state.dart @@ -25,9 +25,7 @@ class TokenState { TokenState({required List tokens, List? lastlyUpdatedTokens}) : tokens = List.from(tokens), - lastlyUpdatedTokens = List.from(lastlyUpdatedTokens ?? tokens) { - _sort(this.tokens); - } + lastlyUpdatedTokens = List.from(lastlyUpdatedTokens ?? tokens); List get nonPiTokens => tokens.where((token) => token.isPrivacyIdeaToken == false).toList(); @@ -44,27 +42,9 @@ class TokenState { for (var token in tokens) { sameTokensMap[token] = stateTokens.firstWhereOrNull((element) => element.isSameTokenAs(token)); } - // List otpTokens = tokens.whereType().toList(); - // Map stateOtpTokens = {for (var e in stateTokens.whereType()) (e).secret: e}; - // List pushTokens = tokens.whereType().toList(); - // Map<(String?, String?, String?), PushToken> statePushTokens = { - // for (var e in stateTokens.whereType()) (e.publicServerKey, e.privateTokenKey, e.publicTokenKey): e - // }; - - // for (var pushToken in pushTokens) { - // tokensWithSameSectet[pushToken] = statePushTokens[(pushToken.publicServerKey, pushToken.privateTokenKey, pushToken.publicTokenKey)]; - // } - // for (var otpToken in otpTokens) { - // tokensWithSameSectet[otpToken] = stateOtpTokens[otpToken.secret]; - // } - return sameTokensMap; } - static void _sort(List tokens) { - tokens.sort((a, b) => (a.sortIndex ?? double.infinity).compareTo(b.sortIndex ?? double.infinity)); - } - T? currentOf(T token) => tokens.firstWhereOrNull((element) => element.id == token.id) as T?; T? currentOfId(String id) => tokens.firstWhereOrNull((element) => element.id == id) as T?; @@ -156,21 +136,24 @@ class TokenState { return (TokenState(tokens: newTokens, lastlyUpdatedTokens: updatedTokens), failedToReplace); } - List tokensInFolder(TokenFolder folder, {List? only, List? exclude}) => tokens.where((token) { - if (token.folderId != folder.folderId) { - return false; - } - if (exclude != null && exclude.contains(token.runtimeType)) return false; - if (only != null && !only.contains(token.runtimeType)) return false; + List tokensInFolder(TokenFolder folder, {List only = const [], List exclude = const []}) => + tokens.inFolder(folder, only: only, exclude: exclude); + + List tokensWithoutFolder({List only = const [], List exclude = const []}) => tokens.withoutFolder(only: only, exclude: exclude); +} + +extension TokenListExtension on List { + List inFolder(TokenFolder folder, {List only = const [], List exclude = const []}) => where((token) { + if (token.folderId != folder.folderId) return false; + if (exclude.contains(token.runtimeType)) return false; + if (only.isNotEmpty && !only.contains(token.runtimeType)) return false; return true; }).toList(); - List tokensWithoutFolder({List? only, List? exclude}) => tokens.where((token) { - if (token.folderId != null) { - return false; - } - if (exclude != null && exclude.contains(token.runtimeType)) return false; - if (only != null && !only.contains(token.runtimeType)) return false; + List withoutFolder({List only = const [], List exclude = const []}) => where((token) { + if (token.folderId != null) return false; + if (exclude.contains(token.runtimeType)) return false; + if (only.isNotEmpty && !only.contains(token.runtimeType)) return false; return true; }).toList(); } diff --git a/lib/model/token_folder.dart b/lib/model/token_folder.dart index 745240c1a..a6f9ad7de 100644 --- a/lib/model/token_folder.dart +++ b/lib/model/token_folder.dart @@ -14,6 +14,8 @@ class TokenFolder with SortableMixin { final bool isLocked; @override final int? sortIndex; + @override + final int? dependsOnSortIndex; const TokenFolder({ required this.label, @@ -21,6 +23,7 @@ class TokenFolder with SortableMixin { this.isExpanded = true, this.isLocked = false, this.sortIndex, + this.dependsOnSortIndex, }); @override @@ -30,6 +33,7 @@ class TokenFolder with SortableMixin { bool? isExpanded, bool? isLocked, int? sortIndex, + int? Function()? dependsOnSortIndex, }) { return TokenFolder( label: label ?? this.label, @@ -37,6 +41,7 @@ class TokenFolder with SortableMixin { sortIndex: sortIndex ?? this.sortIndex, isLocked: isLocked ?? this.isLocked, isExpanded: isExpanded ?? this.isExpanded, + dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, ); } diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart index 55386b9dc..45958266b 100644 --- a/lib/model/tokens/day_password_token.dart +++ b/lib/model/tokens/day_password_token.dart @@ -32,6 +32,7 @@ class DayPasswordToken extends OTPToken { String? type, // just for @JsonSerializable(): type of DayPasswordToken is always TokenTypes.DAYPASSWORD super.tokenImage, super.sortIndex, + super.dependsOnSortIndex, super.folderId, super.pin, super.isLocked, @@ -73,10 +74,11 @@ class DayPasswordToken extends OTPToken { int? digits, String? secret, String? tokenImage, - int? sortIndex, bool? pin, bool? isLocked, bool? isHidden, + int? sortIndex, + int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, }) => @@ -95,7 +97,8 @@ class DayPasswordToken extends OTPToken { pin: pin ?? this.pin, isLocked: isLocked ?? this.isLocked, isHidden: isHidden ?? this.isHidden, - folderId: folderId != null ? folderId.call() : this.folderId, + dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, + folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); diff --git a/lib/model/tokens/hotp_token.dart b/lib/model/tokens/hotp_token.dart index 81e2b2a43..c8637bf36 100644 --- a/lib/model/tokens/hotp_token.dart +++ b/lib/model/tokens/hotp_token.dart @@ -29,10 +29,11 @@ class HOTPToken extends OTPToken { required super.secret, String? type, // just for @JsonSerializable(): type of HOTPToken is always TokenTypes.HOTP super.tokenImage, - super.sortIndex, super.pin, super.isLocked, super.isHidden, + super.sortIndex, + super.dependsOnSortIndex, super.folderId, super.origin, super.label = '', @@ -66,10 +67,11 @@ class HOTPToken extends OTPToken { int? digits, String? secret, String? tokenImage, - int? sortIndex, bool? pin, bool? isLocked, bool? isHidden, + int? sortIndex, + int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, }) => @@ -82,10 +84,11 @@ class HOTPToken extends OTPToken { digits: digits ?? this.digits, secret: secret ?? this.secret, tokenImage: tokenImage ?? this.tokenImage, - sortIndex: sortIndex ?? this.sortIndex, pin: pin ?? this.pin, isLocked: isLocked ?? this.isLocked, isHidden: isHidden ?? this.isHidden, + sortIndex: sortIndex ?? this.sortIndex, + dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); diff --git a/lib/model/tokens/otp_token.dart b/lib/model/tokens/otp_token.dart index 9187d0ff3..bbc5eee8a 100644 --- a/lib/model/tokens/otp_token.dart +++ b/lib/model/tokens/otp_token.dart @@ -22,9 +22,10 @@ abstract class OTPToken extends Token { required super.type, super.pin, super.tokenImage, - super.sortIndex, super.isLocked, super.isHidden, + super.sortIndex, + super.dependsOnSortIndex, super.folderId, super.origin, super.label = '', @@ -53,6 +54,7 @@ abstract class OTPToken extends Token { bool? isHidden, String? tokenImage, int? sortIndex, + int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, }); diff --git a/lib/model/tokens/push_token.dart b/lib/model/tokens/push_token.dart index 93d2a2f25..65ca8f585 100644 --- a/lib/model/tokens/push_token.dart +++ b/lib/model/tokens/push_token.dart @@ -58,8 +58,9 @@ class PushToken extends Token { bool? sslVerify, PushTokenRollOutState? rolloutState, String? type, // just for @JsonSerializable(): type of PushToken is always TokenTypes.PIPUSH - super.sortIndex, super.tokenImage, + super.sortIndex, + super.dependsOnSortIndex, super.folderId, super.pin, super.isLocked, @@ -106,7 +107,6 @@ class PushToken extends Token { bool? sslVerify, String? enrollmentCredentials, Uri? url, - int? sortIndex, String? publicServerKey, String? publicTokenKey, String? privateTokenKey, @@ -114,6 +114,8 @@ class PushToken extends Token { bool? isRolledOut, PushTokenRollOutState? rolloutState, CustomIntBuffer? knownPushRequests, + int? sortIndex, + int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, }) { @@ -130,13 +132,14 @@ class PushToken extends Token { sslVerify: sslVerify ?? this.sslVerify, enrollmentCredentials: enrollmentCredentials ?? this.enrollmentCredentials, url: url ?? this.url, - sortIndex: sortIndex ?? this.sortIndex, publicServerKey: publicServerKey ?? this.publicServerKey, publicTokenKey: publicTokenKey ?? this.publicTokenKey, privateTokenKey: privateTokenKey ?? this.privateTokenKey, expirationDate: expirationDate ?? this.expirationDate, isRolledOut: isRolledOut ?? this.isRolledOut, rolloutState: rolloutState ?? this.rolloutState, + sortIndex: sortIndex ?? this.sortIndex, + dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); diff --git a/lib/model/tokens/steam_token.dart b/lib/model/tokens/steam_token.dart index 6c88f93da..f73bdb497 100644 --- a/lib/model/tokens/steam_token.dart +++ b/lib/model/tokens/steam_token.dart @@ -29,10 +29,11 @@ class SteamToken extends TOTPToken { required super.secret, String? type, super.tokenImage, - super.sortIndex, super.pin, super.isLocked, super.isHidden, + super.sortIndex, + super.dependsOnSortIndex, super.folderId, super.origin, super.label = '', @@ -52,6 +53,7 @@ class SteamToken extends TOTPToken { bool? pin, String? tokenImage, int? sortIndex, + int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, int? period, @@ -67,10 +69,11 @@ class SteamToken extends TOTPToken { algorithm: algorithm ?? this.algorithm, secret: secret ?? this.secret, tokenImage: tokenImage ?? this.tokenImage, - sortIndex: sortIndex ?? this.sortIndex, pin: pin ?? this.pin, isLocked: isLocked ?? this.isLocked, isHidden: isHidden ?? this.isHidden, + sortIndex: sortIndex ?? this.sortIndex, + dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); diff --git a/lib/model/tokens/token.dart b/lib/model/tokens/token.dart index deefd2e51..9299b832b 100644 --- a/lib/model/tokens/token.dart +++ b/lib/model/tokens/token.dart @@ -25,6 +25,8 @@ abstract class Token with SortableMixin { final int? folderId; @override final int? sortIndex; + @override + final int? dependsOnSortIndex; final TokenOriginData? origin; @@ -59,6 +61,7 @@ abstract class Token with SortableMixin { required this.type, this.tokenImage, this.sortIndex, + this.dependsOnSortIndex, this.folderId, this.origin, bool? pin, @@ -89,6 +92,7 @@ abstract class Token with SortableMixin { String? tokenImage, int? sortIndex, int? Function()? folderId, + int? Function()? dependsOnSortIndex, TokenOriginData? origin, }); diff --git a/lib/model/tokens/totp_token.dart b/lib/model/tokens/totp_token.dart index dc8bbfb77..a1f524733 100644 --- a/lib/model/tokens/totp_token.dart +++ b/lib/model/tokens/totp_token.dart @@ -47,10 +47,11 @@ class TOTPToken extends OTPToken { required super.secret, String? type, super.tokenImage, - super.sortIndex, super.pin, super.isLocked, super.isHidden, + super.sortIndex, + super.dependsOnSortIndex, super.folderId, super.origin, super.label = '', @@ -75,10 +76,11 @@ class TOTPToken extends OTPToken { String? secret, int? period, String? tokenImage, - int? sortIndex, bool? pin, bool? isLocked, bool? isHidden, + int? sortIndex, + int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, }) { @@ -91,10 +93,11 @@ class TOTPToken extends OTPToken { secret: secret ?? this.secret, period: period ?? this.period, tokenImage: tokenImage ?? this.tokenImage, - sortIndex: sortIndex ?? this.sortIndex, pin: pin ?? this.pin, isLocked: isLocked ?? this.isLocked, isHidden: isHidden ?? this.isHidden, + sortIndex: sortIndex ?? this.sortIndex, + dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); diff --git a/lib/utils/riverpod_state_listener.dart b/lib/utils/riverpod_state_listener.dart index e403caa5b..bdd90301d 100644 --- a/lib/utils/riverpod_state_listener.dart +++ b/lib/utils/riverpod_state_listener.dart @@ -9,11 +9,12 @@ import '../state_notifiers/token_notifier.dart'; import 'home_widget_utils.dart'; abstract class StateNotifierProviderListener, S> { - final StateNotifierProvider provider; - final void Function(S? previous, S next) onNewState; - const StateNotifierProviderListener({required this.provider, required this.onNewState}); + final StateNotifierProvider? provider; + final void Function(S? previous, S next)? onNewState; + const StateNotifierProviderListener({this.provider, this.onNewState}); void buildListen(WidgetRef ref) { - ref.listen(provider, onNewState); + if (provider == null || onNewState == null) return; + ref.listen(provider!, onNewState!); } } diff --git a/lib/views/main_view/main_view_widgets/drag_target_divider.dart b/lib/views/main_view/main_view_widgets/drag_target_divider.dart index 64180d088..0388eec72 100644 --- a/lib/views/main_view/main_view_widgets/drag_target_divider.dart +++ b/lib/views/main_view/main_view_widgets/drag_target_divider.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -12,16 +11,24 @@ import '../../../widgets/drag_item_scroller.dart'; /// It will accept a Sortable from the type T class DragTargetDivider extends ConsumerStatefulWidget { final TokenFolder? dependingFolder; + final SortableMixin? previousSortable; final SortableMixin? nextSortable; - final double? bottomPaddingIfLast; + final double dividerBaseHeight; + final double dividerExpandedHeight; + final double bottomPaddingIfLast; + final bool isExpandalbe; final bool ignoreFolderId; final bool isLastDivider; const DragTargetDivider({ super.key, required this.dependingFolder, + required this.previousSortable, required this.nextSortable, - this.bottomPaddingIfLast, + this.bottomPaddingIfLast = 0, + this.dividerBaseHeight = 1.5, + this.dividerExpandedHeight = 40, + this.isExpandalbe = true, this.ignoreFolderId = false, this.isLastDivider = false, }); @@ -52,10 +59,10 @@ class _DragTargetDividerState extends ConsumerState DragTarget( - onWillAccept: (data) { - final willAccept = _onWillAccept(data, ref); - if (willAccept) { + Widget build(BuildContext context) => DragTarget( + onWillAcceptWithDetails: (details) { + final willAccept = _onWillAccept(details.data, ref); + if (willAccept && widget.isExpandalbe) { expansionController.forward(); } return willAccept; @@ -63,10 +70,11 @@ class _DragTargetDividerState extends ConsumerState extends ConsumerState(Object? data, WidgetRef ref) { - if (data is! T) return false; +bool _onWillAccept(SortableMixin? data, WidgetRef ref) { if (ref.read(dragItemScrollerStateProvider)) return false; - return true; } void _onAccept({ - required Object? dragedSortable, - SortableMixin? nextSortable, + required SortableMixin? previousSortable, + required SortableMixin dragedSortable, + required SortableMixin? nextSortable, required bool ignoreFolderId, TokenFolder? dependingFolder, required WidgetRef ref, }) { - if (dragedSortable is! SortableMixin) return; - // Higher index = lower in the list final allTokens = ref.read(tokenProvider).tokens; final allFolders = ref.read(tokenFolderProvider).folders; - final allSortables = [...allTokens, ...allFolders]; - allSortables.sort((a, b) => a.compareTo(b)); - final oldIndex = allSortables.indexOf(dragedSortable); - if (oldIndex == -1) return; // If the draged item is not in the list we dont need to do anything - int newIndex; - if (nextSortable == null) { - // If the draged item is moved to the end of the list the nextSortable is null. The newIndex will be set to the last index - newIndex = allSortables.length - 1; - } else { - if (oldIndex < allSortables.indexOf(nextSortable)) { - // If the draged item is moved down it dont pass the nextSortable so the newIndex is before the nextSortable - newIndex = allSortables.indexOf(nextSortable) - 1; - } else { - // If the draged item is moved up it pass the nextSortable so the newIndex will be the place of the nextSortable - newIndex = allSortables.indexOf(nextSortable); - } - } - final dragedItemMovedUp = newIndex < oldIndex; + var allSortables = [...allTokens, ...allFolders]; - final modifiedSortables = []; - for (var i = 0; i < allSortables.length; i++) { - if (i < oldIndex && i < newIndex) { - // This is before dragedSortable and newIndex so no changes needed - continue; - } - if (i > oldIndex && i > newIndex) { - // This is after dragedSortable and newIndex so no changes needed - continue; - } - if (i == oldIndex) { - // This is dragedSortable so it needs to be moved to newIndex - SortableMixin currentSortable = allSortables[i]; - if (currentSortable is Token && !ignoreFolderId) { - // When the draged Sortable is a Token we need to update the folderId so it is in the correct folder - final previousFolderId = dependingFolder?.folderId; - currentSortable = currentSortable.copyWith(folderId: () => previousFolderId); - } - modifiedSortables.add(currentSortable.copyWith(sortIndex: newIndex)); - continue; - } - modifiedSortables.add(allSortables[i] - .copyWith(sortIndex: i + (dragedItemMovedUp ? 1 : -1))); // This is between dragedSortable and newIndex so it needs to be moved up (-1) or down (+1) - continue; + if (dragedSortable is TokenFolder) { + final tokensInFolder = ref.read(tokenProvider).tokens.where((element) => element.folderId == dragedSortable.folderId).toList(); + final allMovingItems = [dragedSortable, ...tokensInFolder]; + allSortables = allSortables.moveAllBetween(moveAfter: previousSortable, movedItems: allMovingItems, moveBefore: nextSortable); + } else { + allSortables = allSortables.moveBetween(moveAfter: previousSortable, movedItem: dragedSortable as Token, moveBefore: nextSortable); + allSortables = allSortables.map((e) { + return e is Token && e.id == dragedSortable.id ? e.copyWith(folderId: () => dependingFolder?.folderId) : e; + }).toList(); } - - globalRef?.read(tokenProvider.notifier).updateTokens(allTokens, (p0) { - final modifiedToken = modifiedSortables.whereType().firstWhereOrNull((updated) => updated.id == p0.id); - return p0.copyWith(sortIndex: modifiedToken?.sortIndex, folderId: modifiedToken != null ? () => modifiedToken.folderId : null); - }); - globalRef?.read(tokenFolderProvider.notifier).updateFolders(modifiedSortables.whereType().toList()); + final modifiedTokens = allSortables.whereType().toList(); + final modifiedFolders = allSortables.whereType().toList(); + ref.read(tokenProvider.notifier).addOrReplaceTokens(modifiedTokens); + ref.read(tokenFolderProvider.notifier).updateFolders(modifiedFolders); } diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart index 01fc9adc3..2d310ee07 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart @@ -71,10 +71,9 @@ class _TokenFolderExpandableState extends ConsumerState w @override ExpandablePanel build(BuildContext context) { - List tokens = ref.watch(tokenProvider).tokensInFolder(widget.folder, exclude: ref.watch(settingsProvider).hidePushTokens ? [PushToken] : []); - tokens = widget.filter?.filterTokens(tokens) ?? tokens; + final tokens = ref.watch(tokenProvider).tokensInFolder(widget.folder, exclude: ref.watch(settingsProvider).hidePushTokens ? [PushToken] : []); + tokens.sort((a, b) => a.compareTo(b)); final draggingSortable = ref.watch(draggingSortableProvider); - if (widget.expandOverride == null) { if (tokens.isEmpty && expandableController.expanded) { expandableController.value = false; @@ -115,9 +114,9 @@ class _TokenFolderExpandableState extends ConsumerState w ), child: Padding( padding: const EdgeInsets.only(left: 15, right: 0), - child: DragTarget( - onWillAccept: (data) { - if (data is Token && data.folderId != widget.folder.folderId) { + child: DragTarget( + onWillAcceptWithDetails: (details) { + if (details.data.folderId != widget.folder.folderId) { if (widget.folder.isLocked) return true; _expandTimer?.cancel(); _expandTimer = Timer(const Duration(milliseconds: 500), () { @@ -129,11 +128,10 @@ class _TokenFolderExpandableState extends ConsumerState w return false; }, onLeave: (data) => _expandTimer?.cancel(), - onAccept: (data) { - if (data is! Token) return; + onAcceptWithDetails: (details) { ref.read(tokenProvider.notifier).updateToken( - data, - (p0) => p0.copyWith(folderId: () => widget.folder.folderId), + details.data, + (p0) => p0.copyWith(folderId: () => widget.folder.folderId, sortIndex: (widget.folder.sortIndex!) + 1), ); }, builder: (context, willAccept, willReject) => Center( @@ -247,11 +245,19 @@ class _TokenFolderExpandableState extends ConsumerState w children: [ for (var i = 0; i < tokens.length; i++) ...[ if (draggingSortable != tokens[i] && (i != 0 || draggingSortable is Token)) - widget.filter == null ? DragTargetDivider(dependingFolder: widget.folder, nextSortable: tokens[i]) : const Divider(), + widget.filter == null + ? DragTargetDivider( + dependingFolder: widget.folder, + previousSortable: (i - 1) < 0 ? null : tokens[i - 1], + nextSortable: tokens[i], + ) + : const Divider(), TokenWidgetBuilder.fromToken(tokens[i]), ], if (tokens.isNotEmpty && draggingSortable is Token) - widget.filter == null ? DragTargetDivider(dependingFolder: widget.folder, nextSortable: null) : const Divider(), + widget.filter == null + ? DragTargetDivider(dependingFolder: widget.folder, previousSortable: tokens.last, nextSortable: null) + : const Divider(), if (tokens.isNotEmpty && draggingSortable is! Token) const SizedBox(height: 8), ], ), diff --git a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart index ba9ad0e7d..933c04345 100644 --- a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart +++ b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:privacyidea_authenticator/model/states/token_state.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; import '../../../model/mixins/sortable_mixin.dart'; import '../../../model/token_folder.dart'; @@ -22,6 +24,28 @@ class MainViewTokensList extends ConsumerStatefulWidget { @override ConsumerState createState() => _MainViewTokensListState(); + + static List buildSortableWidgets(List sortables, SortableMixin? draggingSortable) { + List widgets = []; + if (sortables.isEmpty) return []; + sortables.sort((a, b) => a.compareTo(b)); + for (var i = 0; i < sortables.length; i++) { + final isFirst = i == 0; + final isDraggingTheCurrent = draggingSortable == sortables[i]; + final previousWasExpandedFolder = i > 0 && sortables[i - 1] is TokenFolder && (sortables[i - 1] as TokenFolder).isExpanded; + // 1. Add a divider if the current sortable is not the one which is dragged + // 2. Dont add a divider if the current sortable is the first + // 3. Dont add a divider if the previous sortable was an expanded folder + // 4. Ignore 2. and 3. if there is a sortable that is dragged + // 1 2 3 4 + if (!isDraggingTheCurrent && ((!isFirst && !previousWasExpandedFolder) || draggingSortable != null)) { + widgets.add(DragTargetDivider(dependingFolder: null, previousSortable: sortables.last, nextSortable: sortables[i])); + } + widgets.add(SortableWidgetBuilder.fromSortable(sortables[i])); + } + + return widgets; + } } class _MainViewTokensListState extends ConsumerState { @@ -37,10 +61,11 @@ class _MainViewTokensListState extends ConsumerState { final allowToRefresh = tokenState.hasPushTokens; final draggingSortable = ref.watch(draggingSortableProvider); bool filterPushTokens = ref.watch(settingsProvider).hidePushTokens && tokenState.hasOTPTokens; + var allSortables = [...tokenFolders, ...tokenState.tokens]; + final List tokens = allSortables.whereType().toList(); + final tokensWithNoFolder = tokens.withoutFolder(exclude: filterPushTokens ? [PushToken] : []); - final tokenStateWithNoFolder = tokenState.tokensWithoutFolder(exclude: filterPushTokens ? [PushToken] : []); - - List sortables = [...tokenFolders, ...tokenStateWithNoFolder]; + List sortables = [...tokenFolders, ...tokensWithNoFolder]; return Stack( children: [ if (sortables.isEmpty) const NoTokenScreen(), @@ -64,17 +89,24 @@ class _MainViewTokensListState extends ConsumerState { TokenIntroduction( child: Column( children: [ - ..._buildSortableWidgets(sortables, draggingSortable), + ...MainViewTokensList.buildSortableWidgets(sortables, draggingSortable), ], ), ), ...(draggingSortable != null) ? [ - const DragTargetDivider(dependingFolder: null, nextSortable: null, isLastDivider: true, bottomPaddingIfLast: 80), - const Expanded( + DragTargetDivider( + dependingFolder: null, previousSortable: sortables.last, nextSortable: null, isLastDivider: true, bottomPaddingIfLast: 80), + Expanded( child: Opacity( opacity: 0, - child: DragTargetDivider(dependingFolder: null, nextSortable: null, isLastDivider: true, bottomPaddingIfLast: 80)), + child: DragTargetDivider( + dependingFolder: null, + previousSortable: sortables.last, + nextSortable: null, + isLastDivider: true, + bottomPaddingIfLast: 0, + )), ) ] : [const SizedBox(height: 80)] @@ -89,26 +121,4 @@ class _MainViewTokensListState extends ConsumerState { ], ); } - - List _buildSortableWidgets(List sortables, SortableMixin? draggingSortable) { - List widgets = []; - if (sortables.isEmpty) return []; - sortables.sort((a, b) => a.compareTo(b)); - for (var i = 0; i < sortables.length; i++) { - final isFirst = i == 0; - final isDraggingTheCurrent = draggingSortable == sortables[i]; - final previousWasExpandedFolder = i > 0 && sortables[i - 1] is TokenFolder && (sortables[i - 1] as TokenFolder).isExpanded; - // 1. Add a divider if the current sortable is not the one which is dragged - // 2. Dont add a divider if the current sortable is the first - // 3. Dont add a divider if the previous sortable was an expanded folder - // 4. Ignore 2. and 3. if there is a sortable that is dragged - // 1 2 3 4 - if (!isDraggingTheCurrent && ((!isFirst && !previousWasExpandedFolder) || draggingSortable != null)) { - widgets.add(DragTargetDivider(dependingFolder: null, nextSortable: sortables[i])); - } - widgets.add(SortableWidgetBuilder.fromSortable(sortables[i])); - } - - return widgets; - } } diff --git a/lib/views/push_token_view/widgets/push_tokens_view_list.dart b/lib/views/push_token_view/widgets/push_tokens_view_list.dart index d06594dc6..508192dd7 100644 --- a/lib/views/push_token_view/widgets/push_tokens_view_list.dart +++ b/lib/views/push_token_view/widgets/push_tokens_view_list.dart @@ -3,14 +3,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../model/mixins/sortable_mixin.dart'; -import '../../../model/token_folder.dart'; import '../../../utils/push_provider.dart'; import '../../../utils/riverpod_providers.dart'; import '../../../widgets/deactivateable_refresh_indicator.dart'; import '../../../widgets/drag_item_scroller.dart'; -import '../../main_view/main_view_widgets/drag_target_divider.dart'; import '../../main_view/main_view_widgets/loading_indicator.dart'; -import '../../main_view/main_view_widgets/sortable_widget_builder.dart'; +import '../../main_view/main_view_widgets/main_view_tokens_list.dart'; class PushTokensViwList extends ConsumerStatefulWidget { const PushTokensViwList({super.key}); @@ -50,7 +48,7 @@ class _PushTokensViwListState extends ConsumerState { slivers: [ SliverList( delegate: SliverChildListDelegate( - [..._buildSortableWidgets(sortables, draggingSortable)], + [...MainViewTokensList.buildSortableWidgets(sortables, draggingSortable)], ), ), ], @@ -62,30 +60,3 @@ class _PushTokensViwListState extends ConsumerState { ); } } - -List _buildSortableWidgets(List sortables, SortableMixin? draggingSortable) { - List widgets = []; - if (sortables.isEmpty) return widgets; - sortables.sort((a, b) => a.compareTo(b)); - for (var i = 0; i < sortables.length; i++) { - final isFirst = i == 0; - final isDraggingTheCurrent = draggingSortable == sortables[i]; - final previousWasExpandedFolder = i > 0 && sortables[i - 1] is TokenFolder && (sortables[i - 1] as TokenFolder).isExpanded; - // 1. Add a divider if the current sortable is not the one which is dragged - // 2. Dont add a divider if the current sortable is the first - // 3. Dont add a divider if the previous sortable was an expanded folder - // 4. Ignore 2. and 3. if there is a sortable that is dragged - // 1 2 3 4 - if (!isDraggingTheCurrent && ((!isFirst && !previousWasExpandedFolder) || draggingSortable != null)) { - widgets.add( - DragTargetDivider(dependingFolder: null, nextSortable: sortables[i], ignoreFolderId: true), - ); - } - widgets.add(SortableWidgetBuilder.fromSortable(sortables[i])); - } - if (draggingSortable != null) { - widgets.add(const DragTargetDivider(dependingFolder: null, nextSortable: null, isLastDivider: true, ignoreFolderId: true)); - } - widgets.add(const SizedBox(height: 80)); - return widgets; -} diff --git a/lib/widgets/app_wrapper.dart b/lib/widgets/app_wrapper.dart index c18dbde58..c27016be8 100644 --- a/lib/widgets/app_wrapper.dart +++ b/lib/widgets/app_wrapper.dart @@ -90,6 +90,7 @@ class _AppWrapperState extends ConsumerState<_AppWrapper> { listeners: [ NavigationDeepLinkListener(deeplinkProvider: deeplinkProvider), HomeWidgetTokenStateListener(tokenProvider: tokenProvider), + // SortableListener(tokenProvider, tokenFolderProvider), ], child: EasyDynamicThemeWidget( child: widget.child, From bd66f32820ff6c12fe18e0390b9d3fe8bca0874b Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:04:41 +0200 Subject: [PATCH 06/11] fixed some tests --- lib/l10n/app_en.arb | 3 +- lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_cs.dart | 3 + lib/l10n/app_localizations_de.dart | 3 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_es.dart | 3 + lib/l10n/app_localizations_fr.dart | 3 + lib/l10n/app_localizations_nl.dart | 3 + lib/l10n/app_localizations_pl.dart | 3 + lib/model/mixins/sortable_mixin.dart | 4 +- lib/model/token_folder.dart | 5 - lib/model/tokens/day_password_token.dart | 3 - lib/model/tokens/hotp_token.dart | 3 - lib/model/tokens/hotp_token.g.dart | 2 +- lib/model/tokens/otp_token.dart | 2 - lib/model/tokens/push_token.dart | 3 - lib/model/tokens/push_token.g.dart | 2 +- lib/model/tokens/steam_token.dart | 56 +++--- lib/model/tokens/steam_token.g.dart | 12 +- lib/model/tokens/token.dart | 5 - lib/model/tokens/totp_token.dart | 3 - lib/model/tokens/totp_token.g.dart | 2 +- .../model/encryption/aes_encrypted_test.dart | 2 +- .../encryption/token_encryption_test.dart | 103 +---------- .../model/token/push_token_test.dart | 3 +- .../model/token/steam_token_test.dart | 164 ++++++++++++++++++ .../model/token/totp_token_test.dart | 2 +- .../state_notifiers/token_notifier_test.dart | 61 ++++--- test/unit_test/utils/crypto_utils_test.dart | 8 +- 29 files changed, 281 insertions(+), 194 deletions(-) create mode 100644 test/unit_test/model/token/steam_token_test.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b159cbea2..cf6794884 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -636,5 +636,6 @@ "scanThisQrWithNewDevice": "Scan this QR code with your new device to import the token.", "oneMore": "One more", "done": "Done", - "confirmPassword": "Confirm password" + "confirmPassword": "Confirm password", + "secretIsRequired": "Secret is required" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index add4d4838..6d4f1755f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1644,6 +1644,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Confirm password'** String get confirmPassword; + + /// No description provided for @secretIsRequired. + /// + /// In en, this message translates to: + /// **'Secret is required'** + String get secretIsRequired; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_cs.dart b/lib/l10n/app_localizations_cs.dart index 98a0b2b47..0b43f02fa 100644 --- a/lib/l10n/app_localizations_cs.dart +++ b/lib/l10n/app_localizations_cs.dart @@ -859,4 +859,7 @@ class AppLocalizationsCs extends AppLocalizations { @override String get confirmPassword => 'Potvrďte heslo'; + + @override + String get secretIsRequired => 'Secret is required'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 107a728e1..448c475fa 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -859,4 +859,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get confirmPassword => 'Passwort bestätigen'; + + @override + String get secretIsRequired => 'Secret is required'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index c821e1946..e0318362a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -859,4 +859,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get confirmPassword => 'Confirm password'; + + @override + String get secretIsRequired => 'Secret is required'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index fccb3e29d..479edee8b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -859,4 +859,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get confirmPassword => 'Confirmar contraseña'; + + @override + String get secretIsRequired => 'Secret is required'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index a84c16359..7935996b1 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -859,4 +859,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get confirmPassword => 'Confirmer le mot de passe'; + + @override + String get secretIsRequired => 'Secret is required'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 03d93b77c..f104e8ae2 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -859,4 +859,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get confirmPassword => 'Wachtwoord bevestigen'; + + @override + String get secretIsRequired => 'Secret is required'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 4ebe1bfea..4d0db1071 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -859,4 +859,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String get confirmPassword => 'Potwierdź hasło'; + + @override + String get secretIsRequired => 'Secret is required'; } diff --git a/lib/model/mixins/sortable_mixin.dart b/lib/model/mixins/sortable_mixin.dart index 458fd492a..ac1882eca 100644 --- a/lib/model/mixins/sortable_mixin.dart +++ b/lib/model/mixins/sortable_mixin.dart @@ -1,8 +1,6 @@ mixin SortableMixin { int? get sortIndex; - int? get dependsOnSortIndex; - - SortableMixin copyWith({int? sortIndex, int? Function() dependsOnSortIndex}); + SortableMixin copyWith({int? sortIndex}); /// Compares the sortIndex of two SortableMixin objects. /// Null values are considered to be the highest index. diff --git a/lib/model/token_folder.dart b/lib/model/token_folder.dart index a6f9ad7de..12c48674f 100644 --- a/lib/model/token_folder.dart +++ b/lib/model/token_folder.dart @@ -15,15 +15,12 @@ class TokenFolder with SortableMixin { @override final int? sortIndex; @override - final int? dependsOnSortIndex; - const TokenFolder({ required this.label, required this.folderId, this.isExpanded = true, this.isLocked = false, this.sortIndex, - this.dependsOnSortIndex, }); @override @@ -33,7 +30,6 @@ class TokenFolder with SortableMixin { bool? isExpanded, bool? isLocked, int? sortIndex, - int? Function()? dependsOnSortIndex, }) { return TokenFolder( label: label ?? this.label, @@ -41,7 +37,6 @@ class TokenFolder with SortableMixin { sortIndex: sortIndex ?? this.sortIndex, isLocked: isLocked ?? this.isLocked, isExpanded: isExpanded ?? this.isExpanded, - dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, ); } diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart index 45958266b..d39a573a8 100644 --- a/lib/model/tokens/day_password_token.dart +++ b/lib/model/tokens/day_password_token.dart @@ -32,7 +32,6 @@ class DayPasswordToken extends OTPToken { String? type, // just for @JsonSerializable(): type of DayPasswordToken is always TokenTypes.DAYPASSWORD super.tokenImage, super.sortIndex, - super.dependsOnSortIndex, super.folderId, super.pin, super.isLocked, @@ -78,7 +77,6 @@ class DayPasswordToken extends OTPToken { bool? isLocked, bool? isHidden, int? sortIndex, - int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, }) => @@ -97,7 +95,6 @@ class DayPasswordToken extends OTPToken { pin: pin ?? this.pin, isLocked: isLocked ?? this.isLocked, isHidden: isHidden ?? this.isHidden, - dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); diff --git a/lib/model/tokens/hotp_token.dart b/lib/model/tokens/hotp_token.dart index c8637bf36..7cb6faf57 100644 --- a/lib/model/tokens/hotp_token.dart +++ b/lib/model/tokens/hotp_token.dart @@ -33,7 +33,6 @@ class HOTPToken extends OTPToken { super.isLocked, super.isHidden, super.sortIndex, - super.dependsOnSortIndex, super.folderId, super.origin, super.label = '', @@ -71,7 +70,6 @@ class HOTPToken extends OTPToken { bool? isLocked, bool? isHidden, int? sortIndex, - int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, }) => @@ -88,7 +86,6 @@ class HOTPToken extends OTPToken { isLocked: isLocked ?? this.isLocked, isHidden: isHidden ?? this.isHidden, sortIndex: sortIndex ?? this.sortIndex, - dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); diff --git a/lib/model/tokens/hotp_token.g.dart b/lib/model/tokens/hotp_token.g.dart index 66024c94f..b8ef1d22d 100644 --- a/lib/model/tokens/hotp_token.g.dart +++ b/lib/model/tokens/hotp_token.g.dart @@ -14,10 +14,10 @@ HOTPToken _$HOTPTokenFromJson(Map json) => HOTPToken( secret: json['secret'] as String, type: json['type'] as String?, tokenImage: json['tokenImage'] as String?, - sortIndex: json['sortIndex'] as int?, pin: json['pin'] as bool?, isLocked: json['isLocked'] as bool?, isHidden: json['isHidden'] as bool?, + sortIndex: json['sortIndex'] as int?, folderId: json['folderId'] as int?, origin: json['origin'] == null ? null diff --git a/lib/model/tokens/otp_token.dart b/lib/model/tokens/otp_token.dart index bbc5eee8a..36711210e 100644 --- a/lib/model/tokens/otp_token.dart +++ b/lib/model/tokens/otp_token.dart @@ -25,7 +25,6 @@ abstract class OTPToken extends Token { super.isLocked, super.isHidden, super.sortIndex, - super.dependsOnSortIndex, super.folderId, super.origin, super.label = '', @@ -54,7 +53,6 @@ abstract class OTPToken extends Token { bool? isHidden, String? tokenImage, int? sortIndex, - int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, }); diff --git a/lib/model/tokens/push_token.dart b/lib/model/tokens/push_token.dart index 65ca8f585..a84b94d4f 100644 --- a/lib/model/tokens/push_token.dart +++ b/lib/model/tokens/push_token.dart @@ -60,7 +60,6 @@ class PushToken extends Token { String? type, // just for @JsonSerializable(): type of PushToken is always TokenTypes.PIPUSH super.tokenImage, super.sortIndex, - super.dependsOnSortIndex, super.folderId, super.pin, super.isLocked, @@ -115,7 +114,6 @@ class PushToken extends Token { PushTokenRollOutState? rolloutState, CustomIntBuffer? knownPushRequests, int? sortIndex, - int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, }) { @@ -139,7 +137,6 @@ class PushToken extends Token { isRolledOut: isRolledOut ?? this.isRolledOut, rolloutState: rolloutState ?? this.rolloutState, sortIndex: sortIndex ?? this.sortIndex, - dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); diff --git a/lib/model/tokens/push_token.g.dart b/lib/model/tokens/push_token.g.dart index 48126dafb..2f3acccfa 100644 --- a/lib/model/tokens/push_token.g.dart +++ b/lib/model/tokens/push_token.g.dart @@ -25,8 +25,8 @@ PushToken _$PushTokenFromJson(Map json) => PushToken( rolloutState: $enumDecodeNullable( _$PushTokenRollOutStateEnumMap, json['rolloutState']), type: json['type'] as String?, - sortIndex: json['sortIndex'] as int?, tokenImage: json['tokenImage'] as String?, + sortIndex: json['sortIndex'] as int?, folderId: json['folderId'] as int?, pin: json['pin'] as bool?, isLocked: json['isLocked'] as bool?, diff --git a/lib/model/tokens/steam_token.dart b/lib/model/tokens/steam_token.dart index f73bdb497..6e1d2eb24 100644 --- a/lib/model/tokens/steam_token.dart +++ b/lib/model/tokens/steam_token.dart @@ -1,7 +1,8 @@ import 'package:base32/base32.dart'; -import 'package:crypto/crypto.dart' show Hmac, sha1; +import 'package:crypto/crypto.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; +import 'package:privacyidea_authenticator/utils/errors.dart'; import 'package:uuid/uuid.dart'; import '../../utils/identifiers.dart'; @@ -11,7 +12,7 @@ import '../enums/token_types.dart'; import '../extensions/int_extension.dart'; import '../token_import/token_origin_data.dart'; import 'token.dart'; -import 'totp_token.dart' show TOTPToken; +import 'totp_token.dart'; part 'steam_token.g.dart'; @@ -23,9 +24,7 @@ class SteamToken extends TOTPToken { static const String steamAlphabet = "23456789BCDFGHJKMNPQRTVWXY"; SteamToken({ - required super.period, required super.id, - required super.algorithm, required super.secret, String? type, super.tokenImage, @@ -33,14 +32,15 @@ class SteamToken extends TOTPToken { super.isLocked, super.isHidden, super.sortIndex, - super.dependsOnSortIndex, super.folderId, super.origin, super.label = '', super.issuer = '', }) : super( type: type ?? tokenType, + period: 30, digits: 5, + algorithm: Algorithms.SHA1, ); @override @@ -53,27 +53,23 @@ class SteamToken extends TOTPToken { bool? pin, String? tokenImage, int? sortIndex, - int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, - int? period, + int? period, // unused steam tokens always have 30 seconds period int? digits, // unused steam tokens always have 5 digits - Algorithms? algorithm, + Algorithms? algorithm, // unused steam tokens always have SHA1 algorithm String? secret, }) { return SteamToken( - period: period ?? this.period, label: label ?? this.label, issuer: issuer ?? this.issuer, id: id ?? this.id, - algorithm: algorithm ?? this.algorithm, secret: secret ?? this.secret, tokenImage: tokenImage ?? this.tokenImage, pin: pin ?? this.pin, isLocked: isLocked ?? this.isLocked, isHidden: isHidden ?? this.isHidden, sortIndex: sortIndex ?? this.sortIndex, - dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); @@ -86,9 +82,9 @@ class SteamToken extends TOTPToken { @override bool isSameTokenAs(Token other) => super.isSameTokenAs(other) && other is SteamToken; - @override - String get otpValue { - final counterBytes = (DateTime.now().millisecondsSinceEpoch ~/ 1000 ~/ period).bytes; + String otpOfTime(DateTime time) { + // Flooring time/counter is TOTP default, but yes, steam uses the rounded time/counter. + final counterBytes = (time.millisecondsSinceEpoch / 1000 / period).round().bytes; final secretList = base32.decode(secret.toUpperCase()); final hmac = Hmac(sha1, secretList); final digest = hmac.convert(counterBytes).bytes; @@ -104,17 +100,29 @@ class SteamToken extends TOTPToken { return stringBuffer.toString(); } - static SteamToken fromUriMap(Map uriMap) => SteamToken( - period: uriMap[URI_PERIOD] as int? ?? 30, - label: uriMap[URI_LABEL] as String, - issuer: uriMap[URI_ISSUER] as String, - id: const Uuid().v4(), - algorithm: Algorithms.values.byName(uriMap[URI_ALGORITHM] ?? 'SHA1'), - secret: Encodings.base32.encode(uriMap[URI_SECRET]), - tokenImage: uriMap[URI_IMAGE] as String?, - pin: uriMap[URI_PIN] as bool?, - origin: uriMap[URI_ORIGIN], + @override + String get otpValue => otpOfTime(DateTime.now()); + + static SteamToken fromUriMap(Map uriMap) { + if (uriMap[URI_SECRET] == null) { + throw LocalizedArgumentError( + localizedMessage: (localizations, value, name) => localizations.secretIsRequired, + unlocalizedMessage: 'Secret is required', + invalidValue: uriMap[URI_SECRET], + name: 'SteamToken#fromUriMap', ); + } + return SteamToken( + label: (uriMap[URI_LABEL] as String?) ?? '', + issuer: (uriMap[URI_ISSUER] as String?) ?? '', + id: const Uuid().v4(), + secret: Encodings.base32.encode(uriMap[URI_SECRET]), + tokenImage: uriMap[URI_IMAGE] as String?, + pin: uriMap[URI_PIN] as bool?, + origin: uriMap[URI_ORIGIN] as TokenOriginData?, + ); + } + static SteamToken fromJson(Map json) => _$SteamTokenFromJson(json); @override Map toJson() => _$SteamTokenToJson(this); diff --git a/lib/model/tokens/steam_token.g.dart b/lib/model/tokens/steam_token.g.dart index f8a72b8ff..68a5d14ab 100644 --- a/lib/model/tokens/steam_token.g.dart +++ b/lib/model/tokens/steam_token.g.dart @@ -7,16 +7,14 @@ part of 'steam_token.dart'; // ************************************************************************** SteamToken _$SteamTokenFromJson(Map json) => SteamToken( - period: json['period'] as int, id: json['id'] as String, - algorithm: $enumDecode(_$AlgorithmsEnumMap, json['algorithm']), secret: json['secret'] as String, type: json['type'] as String?, tokenImage: json['tokenImage'] as String?, - sortIndex: json['sortIndex'] as int?, pin: json['pin'] as bool?, isLocked: json['isLocked'] as bool?, isHidden: json['isHidden'] as bool?, + sortIndex: json['sortIndex'] as int?, folderId: json['folderId'] as int?, origin: json['origin'] == null ? null @@ -38,13 +36,5 @@ Map _$SteamTokenToJson(SteamToken instance) => 'sortIndex': instance.sortIndex, 'origin': instance.origin, 'type': instance.type, - 'algorithm': _$AlgorithmsEnumMap[instance.algorithm]!, 'secret': instance.secret, - 'period': instance.period, }; - -const _$AlgorithmsEnumMap = { - Algorithms.SHA1: 'SHA1', - Algorithms.SHA256: 'SHA256', - Algorithms.SHA512: 'SHA512', -}; diff --git a/lib/model/tokens/token.dart b/lib/model/tokens/token.dart index 9299b832b..6d55e8cea 100644 --- a/lib/model/tokens/token.dart +++ b/lib/model/tokens/token.dart @@ -25,9 +25,6 @@ abstract class Token with SortableMixin { final int? folderId; @override final int? sortIndex; - @override - final int? dependsOnSortIndex; - final TokenOriginData? origin; // Must be string representation of TokenType enum. @@ -61,7 +58,6 @@ abstract class Token with SortableMixin { required this.type, this.tokenImage, this.sortIndex, - this.dependsOnSortIndex, this.folderId, this.origin, bool? pin, @@ -92,7 +88,6 @@ abstract class Token with SortableMixin { String? tokenImage, int? sortIndex, int? Function()? folderId, - int? Function()? dependsOnSortIndex, TokenOriginData? origin, }); diff --git a/lib/model/tokens/totp_token.dart b/lib/model/tokens/totp_token.dart index a1f524733..c09665871 100644 --- a/lib/model/tokens/totp_token.dart +++ b/lib/model/tokens/totp_token.dart @@ -51,7 +51,6 @@ class TOTPToken extends OTPToken { super.isLocked, super.isHidden, super.sortIndex, - super.dependsOnSortIndex, super.folderId, super.origin, super.label = '', @@ -80,7 +79,6 @@ class TOTPToken extends OTPToken { bool? isLocked, bool? isHidden, int? sortIndex, - int? Function()? dependsOnSortIndex, int? Function()? folderId, TokenOriginData? origin, }) { @@ -97,7 +95,6 @@ class TOTPToken extends OTPToken { isLocked: isLocked ?? this.isLocked, isHidden: isHidden ?? this.isHidden, sortIndex: sortIndex ?? this.sortIndex, - dependsOnSortIndex: dependsOnSortIndex != null ? dependsOnSortIndex() : this.dependsOnSortIndex, folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); diff --git a/lib/model/tokens/totp_token.g.dart b/lib/model/tokens/totp_token.g.dart index 0f9151c1c..70e4b229b 100644 --- a/lib/model/tokens/totp_token.g.dart +++ b/lib/model/tokens/totp_token.g.dart @@ -14,10 +14,10 @@ TOTPToken _$TOTPTokenFromJson(Map json) => TOTPToken( secret: json['secret'] as String, type: json['type'] as String?, tokenImage: json['tokenImage'] as String?, - sortIndex: json['sortIndex'] as int?, pin: json['pin'] as bool?, isLocked: json['isLocked'] as bool?, isHidden: json['isHidden'] as bool?, + sortIndex: json['sortIndex'] as int?, folderId: json['folderId'] as int?, origin: json['origin'] == null ? null diff --git a/test/unit_test/model/encryption/aes_encrypted_test.dart b/test/unit_test/model/encryption/aes_encrypted_test.dart index 542668294..981e2daec 100644 --- a/test/unit_test/model/encryption/aes_encrypted_test.dart +++ b/test/unit_test/model/encryption/aes_encrypted_test.dart @@ -37,7 +37,7 @@ void _testAesEncrypted() { ), ); expect(aesEncrypted.cypher, AesGcm.with256bits()); - expect(aesEncrypted.mac, Mac.empty); + expect(aesEncrypted.mac, const Mac([103, 169, 139, 92, 212, 40, 200, 3, 208, 110, 165, 128, 152, 185, 48, 3])); }); test('encrypt', () async { final AesEncrypted aesEncrypted = await AesEncrypted.encrypt( diff --git a/test/unit_test/model/encryption/token_encryption_test.dart b/test/unit_test/model/encryption/token_encryption_test.dart index 6a2fff294..4e7c5d4ae 100644 --- a/test/unit_test/model/encryption/token_encryption_test.dart +++ b/test/unit_test/model/encryption/token_encryption_test.dart @@ -18,7 +18,7 @@ void _testTokenEncryption() { final tokensList = [ HOTPToken(id: 'id1', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret1'), TOTPToken(period: 30, id: 'id2', algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2'), - SteamToken(period: 30, id: 'id3', algorithm: Algorithms.SHA512, secret: 'secret3'), + SteamToken(id: 'id3', secret: 'secret3'), DayPasswordToken(period: const Duration(hours: 24), id: 'id4', algorithm: Algorithms.SHA512, digits: 10, secret: 'secret4'), PushToken(serial: 'serial', id: 'id5'), ]; @@ -43,14 +43,14 @@ void _testTokenEncryption() { final tokensList = [ HOTPToken(id: 'id1', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret1'), TOTPToken(period: 30, id: 'id2', algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2'), - SteamToken(period: 30, id: 'id3', algorithm: Algorithms.SHA512, secret: 'secret3'), + SteamToken(id: 'id3', secret: 'secret3'), DayPasswordToken(period: const Duration(hours: 24), id: 'id4', algorithm: Algorithms.SHA512, digits: 10, secret: 'secret4'), PushToken(serial: 'serial', id: 'id5'), ]; const compressedTokensBase64 = [ 'H4sIAAAAAAAACk1PMQ7CMAz8i-cOsDBkY2slJJDgA2nshqhuUiWuBEL8HUelgsnnu7PPfgHbnhgMQAOhlIXyF6PWgHuFc4hgBsuFquWU3Ej4R7QBkX4OSSPFbrKewMSFuYEhMVLucOtLytJFpMdGpBx8zVg7ec46Cu35dtFwy15luU9KXdtjvQfVLQXMQVeRyyQqraCqLi1R6he79wc_WDvI3QAAAA==', 'H4sIAAAAAAAACk1OzQrCMAx-l5x3kIkivXlbQVDYXqAuWS3L2tF2oIjvbuYcesr3l3x5ApsrMSiAAlxKE8UvRpkOS4Gj86A6w4nmyCm0PeGfUDlE-iVy6MnrwVgC5SfmArrASFHjylOIWXuk-yqE6OzcsbD8GGUVmnNzkXLDVux8G0Sqq2O524uIks8J1EGOURspi7mAz78UXZC27eb1Bgi4xPffAAAA', - 'H4sIAAAAAAAACk2OsQ7CMAxE_8VzB6BiydYBqZFggh8ItRuiukmVpBII8e84lAom3z2fdX4CmysxKIAKXEozxa9GmQ5rkZPzoHrDiUrkGLqB8A-0DpF-iRwG8no0lkD5mbmCPjBS1Lj6FGLWHum-ghCdLR2Ly49JTuF8OTQnaTdsZZ9vY2Fts9_uBCbqImUhi_h8SdEF6ag3rzcEg5Vm1QAAAA==', + 'H4sIAAAAAAAACk2NwQrCMBBE_2XPvfWWmwfBgJ70B2J3WkLXbElSsIj_7pZa9LRvHrPMiyTcIeSIGoqlzMhfZruRW8MpJnJ9kIK1ctZuBP-JU2TGr1F1RPKPMIBcmkUa6lUY2fOei-bqE-O5C81xWDe2VJfJXul6Ox4utl7QZVQTG7T0_gDM0h9FtAAAAA==', 'H4sIAAAAAAAACk1PTQvCMAz9Lznv4GSK7DYY4mAy2UDxOE2cZV072s4PxP9uyhyaS15eXpKXF8j6RBJigACEtQOZL0bOAiOGvVAQX2ppyUtyfW4J_4iNQKSfwumWVNbVDUGsBikDuGiJZDKcaquNyxTSYyK0EY2_MVbu2fMopMlxl1TVoShT9lDLhlXu2nGn2iSLcM4k8pizEIczXkpnQ467I_C-b4LuW41-2T7Js3RdlP4bMkKzl9Uymn3j_QHdQYgrBgEAAA==', 'H4sIAAAAAAAAClVQwW7CMAz9F5-57tIrO1ANMbSy3VPiIguTVI6DqKb9-5xCOnbK88t7fk_-BnY9MjQAK6CUMsoDe3vJvxgcKUAzOE5YJNt4PKN_IjbkPf4pNJ4xtBd3QmhCZl7BENmjtL7OKYq2weOtElHoVDLuk06jWWHf7j-7jcXjbSRxSjG8Ol2WJhRypfcDWEx_KNGLIPGXfQ3T0gyDROYLBl0LWmU1X6ryLFwhpQ_ToX_PuniLM2btdK5Qx10sjKjdw86Ue6Zjh3JFecOpbhuFrmaauz3Ts_o_-_ML-WVvco4BAAA=', ]; @@ -67,7 +67,7 @@ void _testTokenEncryption() { final tokensList = [ HOTPToken(id: 'id1', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret1'), TOTPToken(period: 30, id: 'id2', algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2'), - SteamToken(period: 30, id: 'id3', algorithm: Algorithms.SHA512, secret: 'secret3'), + SteamToken(id: 'id3', secret: 'secret3'), DayPasswordToken(period: const Duration(hours: 24), id: 'id4', algorithm: Algorithms.SHA512, digits: 10, secret: 'secret4'), PushToken(serial: 'serial', id: 'id5'), ]; @@ -87,98 +87,3 @@ void _testTokenEncryption() { }); }); } - -// const asd = [ -// { -// "label": "", -// "issuer": "", -// "id": "id1", -// "pin": false, -// "isLocked": false, -// "isHidden": false, -// "tokenImage": null, -// "folderId": null, -// "sortIndex": null, -// "origin": null, -// "type": "HOTP", -// "algorithm": "SHA1", -// "digits": 6, -// "secret": "secret1", -// "counter": 0 -// }, -// { -// "label": "", -// "issuer": "", -// "id": "id2", -// "pin": false, -// "isLocked": false, -// "isHidden": false, -// "tokenImage": null, -// "folderId": null, -// "sortIndex": null, -// "origin": null, -// "type": "TOTP", -// "algorithm": "SHA256", -// "digits": 8, -// "secret": "secret2", -// "period": 30 -// }, -// { -// "label": "", -// "issuer": "", -// "id": "id3", -// "pin": false, -// "isLocked": false, -// "isHidden": false, -// "tokenImage": null, -// "folderId": null, -// "sortIndex": null, -// "origin": null, -// "type": "STEAM", -// "algorithm": "SHA512", -// "secret": "secret3", -// "period": 30 -// }, -// { -// "label": "", -// "issuer": "", -// "id": "id4", -// "pin": false, -// "isLocked": false, -// "isHidden": false, -// "tokenImage": null, -// "folderId": null, -// "sortIndex": null, -// "origin": null, -// "type": "DAYPASSWORD", -// "algorithm": "SHA512", -// "digits": 10, -// "secret": "secret4", -// "viewMode": "VALIDFOR", -// "period": 86400000000 -// }, -// { -// "label": "", -// "issuer": "", -// "id": "id5", -// "pin": false, -// "isLocked": false, -// "isHidden": false, -// "tokenImage": null, -// "folderId": null, -// "sortIndex": null, -// "origin": null, -// "type": "PIPUSH", -// "expirationDate": null, -// "serial": "serial", -// "fbToken": null, -// "sslVerify": false, -// "enrollmentCredentials": null, -// "url": null, -// "isRolledOut": false, -// "rolloutState": "rolloutNotStarted", -// "publicServerKey": null, -// "privateTokenKey": null, -// "publicTokenKey": null -// } -// ]; diff --git a/test/unit_test/model/token/push_token_test.dart b/test/unit_test/model/token/push_token_test.dart index 536b60b1a..31a935157 100644 --- a/test/unit_test/model/token/push_token_test.dart +++ b/test/unit_test/model/token/push_token_test.dart @@ -152,6 +152,7 @@ void _testPushToken() { "type": "PIPUSH", "expirationDate": "2017-09-07T17:30:00.000", "serial": "serial", + "fbToken": null, "sslVerify": true, "enrollmentCredentials": "enrollmentCredentials", "url": "http://www.example.com", @@ -159,7 +160,7 @@ void _testPushToken() { "rolloutState": "rolloutNotStarted", "publicServerKey": "publicServerKey", "privateTokenKey": "privateTokenKey", - "publicTokenKey": "publicTokenKey", + "publicTokenKey": "publicTokenKey" }; expect(jsonEncode(tokenJson), jsonEncode(json)); }); diff --git a/test/unit_test/model/token/steam_token_test.dart b/test/unit_test/model/token/steam_token_test.dart new file mode 100644 index 000000000..6f96dc2fe --- /dev/null +++ b/test/unit_test/model/token/steam_token_test.dart @@ -0,0 +1,164 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/tokens/steam_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; + +void main() { + _testSteamToken(); +} + +void _testSteamToken() { + group('Steam Token', () { + group('TOTP Token creation/method', () { + final steamToken = SteamToken( + label: 'label', + issuer: 'issuer', + id: 'id', + secret: 'secret', + pin: false, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: false, + folderId: 0, + ); + test('constructor', () { + expect(steamToken.period, 30); // default period + expect(steamToken.label, 'label'); + expect(steamToken.issuer, 'issuer'); + expect(steamToken.id, 'id'); + expect(steamToken.algorithm, Algorithms.SHA1); // default algorithm + expect(steamToken.digits, 5); // default digits + expect(steamToken.secret, 'secret'); + expect(steamToken.type, 'STEAM'); + expect(steamToken.pin, false); + expect(steamToken.tokenImage, 'example.png'); + expect(steamToken.sortIndex, 0); + expect(steamToken.isLocked, false); + expect(steamToken.folderId, 0); + }); + test('copyWith', () { + final totpCopy = steamToken.copyWith( + period: 60, // Should not affect the period because steam tokens always have 30 seconds period + label: 'labelCopy', + issuer: 'issuerCopy', + id: 'idCopy', + algorithm: Algorithms.SHA256, // Should not affect the algorithm because steam tokens always have SHA1 algorithm + digits: 8, // Should not affect the digits because steam tokens always have 5 digits + secret: 'secretCopy', + pin: true, + tokenImage: 'exampleCopy.png', + sortIndex: 1, + isLocked: true, + folderId: () => 1, + ); + expect(totpCopy.period, 30); + expect(totpCopy.label, 'labelCopy'); + expect(totpCopy.issuer, 'issuerCopy'); + expect(totpCopy.id, 'idCopy'); + expect(totpCopy.algorithm, Algorithms.SHA1); + expect(totpCopy.digits, 5); + expect(totpCopy.secret, 'secretCopy'); + expect(totpCopy.type, 'STEAM'); + expect(totpCopy.pin, true); + expect(totpCopy.tokenImage, 'exampleCopy.png'); + expect(totpCopy.sortIndex, 1); + expect(totpCopy.isLocked, true); + expect(totpCopy.folderId, 1); + }); + group('fromUriMap', () { + test('with full map', () { + final uriMap = { + 'URI_LABEL': 'label', + 'URI_ISSUER': 'issuer', + 'URI_SECRET': Uint8List.fromList(utf8.encode('secret')), + 'URI_TYPE': 'totp', + 'URI_PIN': false, + 'URI_IMAGE': 'example.png', + }; + final totpFromUriMap = SteamToken.fromUriMap(uriMap); + expect(totpFromUriMap.period, 30); + expect(totpFromUriMap.label, 'label'); + expect(totpFromUriMap.issuer, 'issuer'); + expect(totpFromUriMap.algorithm, Algorithms.SHA1); + expect(totpFromUriMap.digits, 5); + expect(totpFromUriMap.secret, 'ONSWG4TFOQ======'); + expect(totpFromUriMap.type, 'STEAM'); + expect(totpFromUriMap.pin, false); + expect(totpFromUriMap.tokenImage, 'example.png'); + }); + test('with missing secret', () { + final uriMap = { + 'URI_LABEL': 'label', + 'URI_ISSUER': 'issuer', + 'URI_TYPE': 'totp', + 'URI_PIN': false, + 'URI_IMAGE': 'example.png', + }; + expect(() => SteamToken.fromUriMap(uriMap), throwsA(isA())); + }); + test('with empty map', () { + final uriMap = {}; + expect(() => TOTPToken.fromUriMap(uriMap), throwsA(isA())); + }); + }); + test('fromJson', () { + final steamJson = { + 'label': 'label', + 'issuer': 'issuer', + 'id': 'id', + 'secret': 'secret', + 'type': 'STEAM', + 'pin': true, + 'tokenImage': 'example.png', + 'sortIndex': 33, + 'isLocked': true, + 'folderId': 44, + }; + final steamFromJson = SteamToken.fromJson(steamJson); + expect(steamFromJson.period, 30); + expect(steamFromJson.label, 'label'); + expect(steamFromJson.issuer, 'issuer'); + expect(steamFromJson.id, 'id'); + expect(steamFromJson.algorithm, Algorithms.SHA1); + expect(steamFromJson.digits, 5); + expect(steamFromJson.secret, 'secret'); + expect(steamFromJson.type, 'STEAM'); + expect(steamFromJson.pin, true); + expect(steamFromJson.tokenImage, 'example.png'); + expect(steamFromJson.sortIndex, 33); + expect(steamFromJson.isLocked, true); + expect(steamFromJson.folderId, 44); + }); + test('toJson', () { + final totpJson = steamToken.toJson(); + expect(totpJson['label'], 'label'); + expect(totpJson['issuer'], 'issuer'); + expect(totpJson['id'], 'id'); + expect(totpJson['secret'], 'secret'); + expect(totpJson['type'], 'STEAM'); + expect(totpJson['pin'], false); + expect(totpJson['tokenImage'], 'example.png'); + expect(totpJson['sortIndex'], 0); + expect(totpJson['isLocked'], false); + expect(totpJson['folderId'], 0); + }); + }); + test('otpValue', () { + final time = DateTime.fromMillisecondsSinceEpoch(1712666212056); + + final steamToken = SteamToken( + label: '', + issuer: '', + id: '', + secret: 'SECRETA=', + ); + final otp = steamToken.otpOfTime(time); + final otpNow = steamToken.otpOfTime(DateTime.now()); + expect(otp, equals('JGPCJ')); // Checks if the otpOfTime works correctly + expect(steamToken.otpValue, equals(otpNow)); // Checks if the otpValue delivers the same value as the otpOfTime method + }); + }); +} diff --git a/test/unit_test/model/token/totp_token_test.dart b/test/unit_test/model/token/totp_token_test.dart index 91cf42b15..2a0b01932 100644 --- a/test/unit_test/model/token/totp_token_test.dart +++ b/test/unit_test/model/token/totp_token_test.dart @@ -170,7 +170,7 @@ void _testTotpToken() { expect(totpFromJson.algorithm, Algorithms.SHA1); expect(totpFromJson.digits, 22); expect(totpFromJson.secret, 'secret'); - expect(totpFromJson.type, 'TOTP'); + expect(totpFromJson.type, 'totp'); expect(totpFromJson.pin, true); expect(totpFromJson.tokenImage, 'example.png'); expect(totpFromJson.sortIndex, 33); diff --git a/test/unit_test/state_notifiers/token_notifier_test.dart b/test/unit_test/state_notifiers/token_notifier_test.dart index b03d66901..2aa8cbef9 100644 --- a/test/unit_test/state_notifiers/token_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_notifier_test.dart @@ -40,6 +40,7 @@ void _testTokenNotifier() { test('loadStateFromRepo', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true)]; final after = [ PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true), @@ -47,8 +48,10 @@ void _testTokenNotifier() { ]; final responses = [before, after]; when(mockRepo.loadTokens()).thenAnswer((_) async => responses.removeAt(0)); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); expect((await notifier.loadStateFromRepo())?.tokens, after); @@ -60,11 +63,14 @@ void _testTokenNotifier() { test('getTokenFromId', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret')]; final after = before; when(mockRepo.loadTokens()).thenAnswer((_) async => before); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); await notifier.initState; @@ -76,6 +82,7 @@ void _testTokenNotifier() { test('incrementCounter', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', counter: 522), ]; @@ -84,10 +91,10 @@ void _testTokenNotifier() { ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.saveOrReplaceToken(after.first)).thenAnswer((_) async => true); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier( - repository: mockRepo, - ), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); final initState = await notifier.initState; @@ -101,6 +108,7 @@ void _testTokenNotifier() { test('removeToken', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), HOTPToken(label: 'label2', issuer: 'issuer2', id: 'id2', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2'), @@ -110,7 +118,11 @@ void _testTokenNotifier() { ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.deleteToken(before.last)).thenAnswer((_) async => true); - final testProvider = StateNotifierProvider((ref) => TokenNotifier(repository: mockRepo)); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); + final testProvider = StateNotifierProvider( + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), + ); final notifier = container.read(testProvider.notifier); final initState = await notifier.initState; expect(initState.tokens, before); @@ -124,6 +136,7 @@ void _testTokenNotifier() { test('add Token', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; @@ -133,10 +146,10 @@ void _testTokenNotifier() { ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.saveOrReplaceToken(after.last)).thenAnswer((_) async => true); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier( - repository: mockRepo, - ), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); final initState = await notifier.initState; @@ -150,6 +163,7 @@ void _testTokenNotifier() { test('replace Token', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), HOTPToken(label: 'label2', issuer: 'issuer2', id: 'id2', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2'), @@ -160,10 +174,10 @@ void _testTokenNotifier() { ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.saveOrReplaceToken(after.last)).thenAnswer((_) async => true); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier( - repository: mockRepo, - ), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); final initState = await notifier.initState; @@ -178,6 +192,7 @@ void _testTokenNotifier() { test('addOrReplaceTokens', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; @@ -187,11 +202,11 @@ void _testTokenNotifier() { HOTPToken(label: 'label3', issuer: 'issuer3', id: 'id3', algorithm: Algorithms.SHA512, digits: 8, secret: 'secret3'), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); when(mockRepo.saveOrReplaceTokens([...after])).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier( - repository: mockRepo, - ), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); await notifier.addOrReplaceTokens([...after]); @@ -202,6 +217,7 @@ void _testTokenNotifier() { test('addTokenFromOtpAuth', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; @@ -211,9 +227,9 @@ void _testTokenNotifier() { ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); - + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); await notifier.handleQrCode('otpauth://totp/issuer2:label2?secret=secret2&issuer=issuer2&algorithm=SHA256&digits=6&period=30'); @@ -221,7 +237,7 @@ void _testTokenNotifier() { expect(state, isNotNull); after.last = after.last.copyWith(id: state.tokens.last.id); expect(state.tokens, after); - verify(mockRepo.saveOrReplaceTokens(any)).called(1); + verify(mockRepo.saveOrReplaceTokens(any)).called(greaterThan(0)); }); test('addTokenFromOtpAuth: rolloutPushToken', () async { final container = ProviderContainer(); @@ -242,7 +258,6 @@ void _testTokenNotifier() { final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; - final pushTokenShouldBe = PushToken( label: 'PIPU0006BF18', issuer: 'privacyIDEA', @@ -269,7 +284,6 @@ void _testTokenNotifier() { ]; const otpAuth = 'otpauth://pipush/PIPU0006BF18?url=https%3A//192.168.178.30/ttype/push&ttl=10&issuer=privacyIDEA&enrollment_credential=ae60d4744ac5384515574b85f538c6a4e0c7bc82&v=1&serial=PIPU0006BF18&sslverify=0'; - when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'fbToken'); when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRsaUtils.generateRSAKeyPair()).thenAnswer((realInvocation) async => AsymmetricKeyPair(publicTokenKey, privateTokenKey)); @@ -280,6 +294,7 @@ void _testTokenNotifier() { when(mockRsaUtils.deserializeRSAPrivateKeyPKCS1(privateTokenKeyString)).thenReturn(privateTokenKey); when(mockRepo.saveOrReplaceTokens([after.last])).thenAnswer((_) async => []); // QrCode can contain multiple tokens when(mockRepo.saveOrReplaceToken(after.last)).thenAnswer((_) async => true); // Rollout one by one + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); when(mockIOClient.doPost( url: anyNamed('url'), body: anyNamed('body'), @@ -292,7 +307,6 @@ void _testTokenNotifier() { ), ), ); - final testProvider = StateNotifierProvider((ref) => TokenNotifier( repository: mockRepo, rsaUtils: mockRsaUtils, @@ -345,6 +359,7 @@ void _testTokenNotifier() { ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.saveOrReplaceToken(after.first)).thenAnswer((_) async => true); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); when(mockRsaUtils.serializeRSAPublicKeyPKCS8(any)).thenAnswer((_) => 'publicKey'); when(mockRsaUtils.generateRSAKeyPair()).thenAnswer((_) => const RsaUtils() .generateRSAKeyPair()); // We get here a random result anyway and is it more likely to make errors by mocking it than by using the real method @@ -363,7 +378,6 @@ void _testTokenNotifier() { firebaseUtils: mockFirebaseUtils, ), ); - final notifier = container.read(testProvider.notifier); final initState = await notifier.initState; expect(initState.tokens, before); @@ -385,10 +399,13 @@ void _testTokenNotifier() { }); test('loadFromRepo', () async { final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; when(mockRepo.loadTokens()).thenAnswer((_) => Future.value(before)); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final notifier = TokenNotifier(repository: mockRepo); Logger.info('before loadFromRepo'); final newState = await notifier.loadStateFromRepo(); diff --git a/test/unit_test/utils/crypto_utils_test.dart b/test/unit_test/utils/crypto_utils_test.dart index bbf4070ad..925f8722c 100644 --- a/test/unit_test/utils/crypto_utils_test.dart +++ b/test/unit_test/utils/crypto_utils_test.dart @@ -399,8 +399,8 @@ void _testPbkdf2() { void _testDecodeSecretToUint8() { group('decodeSecretToUint8', () { test('Test non hex secret', () { - expect(Encodings.hex.decode('oo'), throwsFormatException); - expect(Encodings.hex.decode('1Aö'), throwsFormatException); + expect(() => Encodings.hex.decode('oo'), throwsFormatException); + expect(() => Encodings.hex.decode('1Aö'), throwsFormatException); }); test('Test hex secret', () { @@ -409,8 +409,8 @@ void _testDecodeSecretToUint8() { }); test('Test non base32 secret', () { - expect(Encodings.base32.decode('p'), throwsFormatException); - expect(Encodings.base32.decode('AAAAAAöA'), throwsFormatException); + expect(() => Encodings.base32.decode('p'), throwsFormatException); + expect(() => Encodings.base32.decode('AAAAAAöA'), throwsFormatException); }); test('Test base32 secret', () { From fb91981502ef604e8fc16ccba9adb7082134f7d2 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:39:13 +0200 Subject: [PATCH 07/11] refactoring --- integration_test/add_tokens_test.dart | 2 +- integration_test/copy_to_clipboard_test.dart | 2 +- integration_test/rename_and_delete_test.dart | 2 +- integration_test/two_step_rollout_test.dart | 22 +--- integration_test/views_test.dart | 2 +- .../repo/token_folder_repository.dart | 4 +- lib/model/states/token_folder_state.dart | 37 +++++- .../token_import/token_import_origin.dart | 13 +- .../token_import/token_import_source.dart | 12 ++ .../preference_token_folder_repository.dart | 6 +- .../token_folder_notifier.dart | 122 +++++++++++++----- lib/state_notifiers/token_notifier.dart | 60 ++++----- lib/utils/riverpod_providers.dart | 33 +++++ lib/utils/token_import_origins.dart | 24 ++-- .../pages/import_start_page.dart | 4 +- .../pages/select_import_type_page.dart | 5 +- .../link_home_widget_view.dart | 2 +- .../drag_target_divider.dart | 8 +- .../add_token_folder_dialog.dart | 2 +- .../lock_token_folder_action.dart | 2 +- .../rename_token_folder_action.dart | 26 ++-- .../token_folder_expandable.dart | 4 +- .../main_view_tokens_list.dart | 32 +++-- test/tests_app_wrapper.mocks.dart | 13 +- .../model/processor_result_test.dart | 12 ++ .../model/states/token_folder_state_test.dart | 6 +- .../token_import_origin_test.dart | 1 - .../token_import/token_origin_data_test.dart | 55 ++++++++ .../token_folder_notifier_test.dart | 36 +++--- .../token_folder_notifier_test.mocks.dart | 10 +- 30 files changed, 368 insertions(+), 191 deletions(-) create mode 100644 lib/model/token_import/token_import_source.dart delete mode 100644 test/unit_test/model/token_import/token_import_origin_test.dart diff --git a/integration_test/add_tokens_test.dart b/integration_test/add_tokens_test.dart index 746a58085..d0b5476df 100644 --- a/integration_test/add_tokens_test.dart +++ b/integration_test/add_tokens_test.dart @@ -45,7 +45,7 @@ void main() { when(mockTokenRepository.deleteTokens(any)).thenAnswer((_) async => []); mockTokenFolderRepository = MockTokenFolderRepository(); when(mockTokenFolderRepository.loadFolders()).thenAnswer((_) async => []); - when(mockTokenFolderRepository.saveOrReplaceFolders(any)).thenAnswer((_) async => []); + when(mockTokenFolderRepository.saveReplaceList(any)).thenAnswer((_) async => true); mockIntroductionRepository = MockIntroductionRepository(); final introductions = {...Introduction.values}..remove(Introduction.introductionScreen); when(mockIntroductionRepository.loadCompletedIntroductions()).thenAnswer((_) async => IntroductionState(completedIntroductions: introductions)); diff --git a/integration_test/copy_to_clipboard_test.dart b/integration_test/copy_to_clipboard_test.dart index 5b12a5eeb..88eb2bd81 100644 --- a/integration_test/copy_to_clipboard_test.dart +++ b/integration_test/copy_to_clipboard_test.dart @@ -38,7 +38,7 @@ void main() { when(mockTokenRepository.deleteTokens(any)).thenAnswer((_) async => []); mockTokenFolderRepository = MockTokenFolderRepository(); when(mockTokenFolderRepository.loadFolders()).thenAnswer((_) async => []); - when(mockTokenFolderRepository.saveOrReplaceFolders(any)).thenAnswer((_) async => []); + when(mockTokenFolderRepository.saveReplaceList(any)).thenAnswer((_) async => true); mockIntroductionRepository = MockIntroductionRepository(); final introductions = {...Introduction.values}..remove(Introduction.introductionScreen); when(mockIntroductionRepository.loadCompletedIntroductions()).thenAnswer((_) async => IntroductionState(completedIntroductions: introductions)); diff --git a/integration_test/rename_and_delete_test.dart b/integration_test/rename_and_delete_test.dart index a34145709..d07924633 100644 --- a/integration_test/rename_and_delete_test.dart +++ b/integration_test/rename_and_delete_test.dart @@ -41,7 +41,7 @@ void main() { when(mockTokenRepository.deleteTokens(any)).thenAnswer((_) async => []); mockTokenFolderRepository = MockTokenFolderRepository(); when(mockTokenFolderRepository.loadFolders()).thenAnswer((_) async => []); - when(mockTokenFolderRepository.saveOrReplaceFolders(any)).thenAnswer((_) async => []); + when(mockTokenFolderRepository.saveReplaceList(any)).thenAnswer((_) async => true); mockIntroductionRepository = MockIntroductionRepository(); final introductions = {...Introduction.values}..remove(Introduction.introductionScreen); when(mockIntroductionRepository.loadCompletedIntroductions()).thenAnswer((_) async => IntroductionState(completedIntroductions: introductions)); diff --git a/integration_test/two_step_rollout_test.dart b/integration_test/two_step_rollout_test.dart index bdf209970..2b84ba31a 100644 --- a/integration_test/two_step_rollout_test.dart +++ b/integration_test/two_step_rollout_test.dart @@ -22,26 +22,6 @@ import 'package:privacyidea_authenticator/widgets/widget_keys.dart'; import '../test/tests_app_wrapper.dart'; import '../test/tests_app_wrapper.mocks.dart'; -/* - -// qr codes: -const String URI_TYPE = 'URI_TYPE'; -const String URI_LABEL = 'URI_LABEL'; -const String URI_ALGORITHM = 'URI_ALGORITHM'; -const String URI_DIGITS = 'URI_DIGITS'; -const String URI_SECRET = 'URI_SECRET'; -const String URI_COUNTER = 'URI_COUNTER'; -const String URI_PERIOD = 'URI_PERIOD'; -const String URI_ISSUER = 'URI_ISSUER'; -const String URI_PIN = 'URI_PIN'; -const String URI_IMAGE = 'URI_IMAGE'; - -// 2 step: -const String URI_SALT_LENGTH = 'URI_SALT_LENGTH'; -const String URI_OUTPUT_LENGTH_IN_BYTES = 'URI_OUTPUT_LENGTH_IN_BYTES'; -const String URI_ITERATIONS = 'URI_ITERATIONS'; - - */ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); late final MockSettingsRepository mockSettingsRepository; @@ -59,7 +39,7 @@ void main() { when(mockTokenRepository.deleteTokens(any)).thenAnswer((_) async => []); mockTokenFolderRepository = MockTokenFolderRepository(); when(mockTokenFolderRepository.loadFolders()).thenAnswer((_) async => []); - when(mockTokenFolderRepository.saveOrReplaceFolders(any)).thenAnswer((_) async => []); + when(mockTokenFolderRepository.saveReplaceList(any)).thenAnswer((_) async => true); mockIntroductionRepository = MockIntroductionRepository(); final introductions = {...Introduction.values}..remove(Introduction.introductionScreen); when(mockIntroductionRepository.loadCompletedIntroductions()).thenAnswer((_) async => IntroductionState(completedIntroductions: introductions)); diff --git a/integration_test/views_test.dart b/integration_test/views_test.dart index c3c5aa1b9..b3752ff03 100644 --- a/integration_test/views_test.dart +++ b/integration_test/views_test.dart @@ -41,7 +41,7 @@ void main() { when(mockTokenRepository.deleteTokens(any)).thenAnswer((_) async => []); mockTokenFolderRepository = MockTokenFolderRepository(); when(mockTokenFolderRepository.loadFolders()).thenAnswer((_) async => []); - when(mockTokenFolderRepository.saveOrReplaceFolders(any)).thenAnswer((_) async => []); + when(mockTokenFolderRepository.saveReplaceList(any)).thenAnswer((_) async => true); mockRsaUtils = MockRsaUtils(); when(mockRsaUtils.serializeRSAPublicKeyPKCS8(any)).thenAnswer((_) => 'publicKey'); when(mockRsaUtils.generateRSAKeyPair()).thenAnswer((_) => const RsaUtils() diff --git a/lib/interfaces/repo/token_folder_repository.dart b/lib/interfaces/repo/token_folder_repository.dart index 9a6f4055c..053acc9f6 100644 --- a/lib/interfaces/repo/token_folder_repository.dart +++ b/lib/interfaces/repo/token_folder_repository.dart @@ -1,6 +1,8 @@ import '../../model/token_folder.dart'; abstract class TokenFolderRepository { - Future> saveOrReplaceFolders(List folders); + /// Overwrite the current state with the new folders + /// Returns true if the operation is successful, false otherwise + Future saveReplaceList(List folders); Future> loadFolders(); } diff --git a/lib/model/states/token_folder_state.dart b/lib/model/states/token_folder_state.dart index c5010e532..f87626322 100644 --- a/lib/model/states/token_folder_state.dart +++ b/lib/model/states/token_folder_state.dart @@ -11,15 +11,19 @@ class TokenFolderState { const TokenFolderState({required this.folders}); - TokenFolderState withFolder(String name) { + /// Add a new folder with the given name + /// Returns a new TokenFolderState with the new folder + /// The original List is not modified + TokenFolderState addNewFolder(String name) { final newFolders = List.from(folders); newFolders.add(TokenFolder(label: name, folderId: newFolderId)); return TokenFolderState(folders: newFolders); } - // replace all folders where the folderid is the same - // if the folderid is none, add it to the list - TokenFolderState withUpdated(List folders) { + /// Add or replace the folders with the same folderId + /// Returns a new TokenFolderState with the new folders + /// The original List is not modified + TokenFolderState addOrReplaceFolders(List folders) { final newFolders = List.from(this.folders); for (var newFolder in folders) { final index = newFolders.indexWhere((oldFolder) => oldFolder.folderId == newFolder.folderId); @@ -30,13 +34,28 @@ class TokenFolderState { return TokenFolderState(folders: newFolders); } - TokenFolderState withoutFolder(TokenFolder folder) { + TokenFolderState addOrReplaceFolder(TokenFolder newFolder) { + final newFolders = List.from(folders); + final index = newFolders.indexWhere((element) => element.folderId == newFolder.folderId); + if (index != -1) { + newFolders[index] = newFolder; + } + return TokenFolderState(folders: newFolders); + } + + /// Remove the folder with the same folderId + /// Returns a new TokenFolderState without the folder + /// The original List is not modified + TokenFolderState removeFolder(TokenFolder folder) { final newFolders = List.from(folders); newFolders.removeWhere((element) => element.folderId == folder.folderId); return TokenFolderState(folders: newFolders); } - TokenFolderState withoutFolders(List folders) { + /// Remove the folders with the same folderId + /// Returns a new TokenFolderState without the folders + /// The original List is not modified + TokenFolderState removeFolders(List folders) { final newFolders = List.from(this.folders); newFolders.removeWhere((element) => folders.any((folder) => folder.folderId == element.folderId)); return TokenFolderState(folders: newFolders); @@ -54,5 +73,9 @@ class TokenFolderState { get newFolderId => folders.fold(0, (previousValue, element) => max(previousValue, element.folderId)) + 1; - TokenFolder? getFolderById(int? id) => id == null ? null : folders.firstWhereOrNull((element) => element.folderId == id); + /// Get the folder by the given id, or null if the folder does not exist + TokenFolder? currentById(int? id) => id == null ? null : folders.firstWhereOrNull((element) => element.folderId == id); + + /// Returns the current folder of the given folder, or null if the folder does not exist + TokenFolder? currentOf(TokenFolder folder) => folders.firstWhereOrNull((element) => element.folderId == folder.folderId); } diff --git a/lib/model/token_import/token_import_origin.dart b/lib/model/token_import/token_import_origin.dart index 21c3330f8..78f931819 100644 --- a/lib/model/token_import/token_import_origin.dart +++ b/lib/model/token_import/token_import_origin.dart @@ -1,7 +1,4 @@ -import 'package:flutter/material.dart'; - -import '../../processors/mixins/token_import_processor.dart'; -import '../enums/token_import_type.dart'; +import 'token_import_source.dart'; class TokenImportOrigin { final String appName; @@ -14,11 +11,3 @@ class TokenImportOrigin { this.iconPath, }); } - -class TokenImportSource { - final TokenImportType type; - final TokenImportProcessor processor; - final String Function(BuildContext context) importHint; - - const TokenImportSource({required this.processor, required this.type, required this.importHint}); -} diff --git a/lib/model/token_import/token_import_source.dart b/lib/model/token_import/token_import_source.dart new file mode 100644 index 000000000..10c720d6d --- /dev/null +++ b/lib/model/token_import/token_import_source.dart @@ -0,0 +1,12 @@ +import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; + +import '../../processors/mixins/token_import_processor.dart'; +import '../enums/token_import_type.dart'; + +class TokenImportSource { + final TokenImportType type; + final TokenImportProcessor processor; + final String Function(AppLocalizations localizations) importHint; + + const TokenImportSource({required this.processor, required this.type, required this.importHint}); +} diff --git a/lib/repo/preference_token_folder_repository.dart b/lib/repo/preference_token_folder_repository.dart index 880993af4..bba126a05 100644 --- a/lib/repo/preference_token_folder_repository.dart +++ b/lib/repo/preference_token_folder_repository.dart @@ -26,15 +26,15 @@ class PreferenceTokenFolderRepository extends TokenFolderRepository { } @override - Future> saveOrReplaceFolders(List folders) async { + Future saveReplaceList(List folders) async { try { final jsons = folders.map((e) => e.toJson()).toList(); final json = jsonEncode(jsons); await _prefs.then((prefs) => prefs.setString(_tokenFoldersKey, json)); - return []; + return true; } catch (e, s) { Logger.error('Failed to save folders', name: 'PreferenceTokenFolderRepository#saveFolders', error: e, stackTrace: s); - return folders; + return false; } } } diff --git a/lib/state_notifiers/token_folder_notifier.dart b/lib/state_notifiers/token_folder_notifier.dart index d814a3310..03be7a086 100644 --- a/lib/state_notifiers/token_folder_notifier.dart +++ b/lib/state_notifiers/token_folder_notifier.dart @@ -1,4 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mutex/mutex.dart'; import '../interfaces/repo/token_folder_repository.dart'; import '../model/states/token_folder_state.dart'; @@ -6,63 +7,120 @@ import '../model/token_folder.dart'; import '../utils/logger.dart'; class TokenFolderNotifier extends StateNotifier { - Future? isLoading; + late final Future initState; + final Mutex _loadingRepoMutex = Mutex(); + final Mutex _updateFolderMutex = Mutex(); final TokenFolderRepository _repo; TokenFolderNotifier({required TokenFolderRepository repository, TokenFolderState? initialState}) : _repo = repository, super(initialState ?? const TokenFolderState(folders: [])) { - _loadFromRepo(); + _init(); } - void _loadFromRepo() => isLoading = Future(() async => state = TokenFolderState(folders: await _repo.loadFolders())); - - void _saveOrReplaceFolders(List folders) { - isLoading = Future(() async { - final failedFolders = await _repo.saveOrReplaceFolders(folders); - if (failedFolders.isNotEmpty) { - Logger.error('Failed to save or replace folders: $failedFolders', name: 'TokenFolderNotifier#_saveOrReplaceFolders'); - state = state.withoutFolders(failedFolders); - } - }); + void _init() { + initState = Future(() async => state = TokenFolderState(folders: await _repo.loadFolders())); } - void addFolder(String name) { - final newState = state.withFolder(name); - state = newState; - _saveOrReplaceFolders(newState.folders); + Future _addOrReplaceFolders(List folders) async { + await _loadingRepoMutex.acquire(); + final success = await _repo.saveReplaceList(folders); + if (!success) { + Logger.warning( + 'Failed to save folders', + name: 'TokenFolderNotifier#_addOrReplaceFolders', + ); + return false; + } + state = state.addOrReplaceFolders(folders); + _loadingRepoMutex.release(); + return true; } - void removeFolder(TokenFolder folder) { - final newState = state.withoutFolder(folder); + Future _addOrReplaceFolder(TokenFolder folder) async { + await _loadingRepoMutex.acquire(); + final newState = state.addOrReplaceFolder(folder); + final success = await _repo.saveReplaceList(newState.folders); + if (!success) { + Logger.warning( + 'Failed to add or replace folder', + name: 'TokenFolderNotifier#_addOrReplaceFolder', + ); + return false; + } state = newState; - _saveOrReplaceFolders(newState.folders); + _loadingRepoMutex.release(); + return true; } - void updateFolder(TokenFolder folder) { - final newState = state.withUpdated([folder]); + Future _addNewFolder(String name) async { + await _loadingRepoMutex.acquire(); + final newState = state.addNewFolder(name); + final success = await _repo.saveReplaceList(newState.folders); + if (!success) { + Logger.warning( + 'Failed to add new folder', + name: 'TokenFolderNotifier#_addNewFolder', + ); + return false; + } state = newState; - _saveOrReplaceFolders(newState.folders); + _loadingRepoMutex.release(); + return true; } - void updateFolders(List folders) { - final newState = state.withUpdated(folders); + Future _removeFolder(TokenFolder folder) async { + await _loadingRepoMutex.acquire(); + final newState = state.removeFolder(folder); + final success = await _repo.saveReplaceList(newState.folders); + if (!success) { + Logger.warning( + 'Failed to remove folder', + name: 'TokenFolderNotifier#_removeFolder', + ); + return false; + } state = newState; - _saveOrReplaceFolders(newState.folders); + _loadingRepoMutex.release(); + return true; } - void expandFolderById(int id) { - final folder = state.folders.firstWhere((element) => element.folderId == id).copyWith(isExpanded: true); - updateFolder(folder); + Future addNewFolder(String name) => _addNewFolder(name); + + Future removeFolder(TokenFolder folder) => _removeFolder(folder); + + /// Search for the current version of the given folder and update it with the updater function. + /// If the folder is not found, nothing will happen. + /// Returns true if the operation is successful, false otherwise. + Future updateFolder(TokenFolder folder, Function(TokenFolder) updater) async { + await _updateFolderMutex.acquire(); + final curent = state.currentOf(folder); + if (curent == null) return false; + final newFolder = updater(curent); + final success = await _addOrReplaceFolder(newFolder); + _updateFolderMutex.release(); + return success; } - void collapseLockedFolders() { + Future addOrReplaceFolders(List folders) => _addOrReplaceFolders(folders); + + Future expandFolder(TokenFolder folder) => updateFolder(folder, (p0) => p0.copyWith(isExpanded: true)); + Future expandFolderById(int folderId) => updateFolder(state.currentById(folderId)!, (p0) => p0.copyWith(isExpanded: true)); + Future collapseFolder(TokenFolder folder) => updateFolder(folder, (p0) => p0.copyWith(isExpanded: false)); + Future lockFolder(TokenFolder folder) => updateFolder(folder, (p0) => p0.copyWith(isLocked: true)); + Future unlockFolder(TokenFolder folder) => updateFolder(folder, (p0) => p0.copyWith(isLocked: false)); + Future toggleFolderLock(TokenFolder folder) => updateFolder(folder, (p0) => p0.copyWith(isLocked: !folder.isLocked)); + Future updateLabel(TokenFolder folder, String label) => updateFolder(folder, (p0) => p0.copyWith(label: label)); + + Future collapseLockedFolders() async { + await _updateFolderMutex.acquire(); final lockedFolders = state.folders.where((element) => element.isLocked).toList(); for (var i = 0; i < lockedFolders.length; i++) { lockedFolders[i] = lockedFolders[i].copyWith(isExpanded: false); } - final newState = state.withUpdated(lockedFolders); - state = newState; - _saveOrReplaceFolders(newState.folders); + final newState = state.addOrReplaceFolders(lockedFolders); + final success = _addOrReplaceFolders(newState.folders); + _updateFolderMutex.release(); + return success; } } diff --git a/lib/state_notifiers/token_notifier.dart b/lib/state_notifiers/token_notifier.dart index 249a3cffb..8ec4c0690 100644 --- a/lib/state_notifiers/token_notifier.dart +++ b/lib/state_notifiers/token_notifier.dart @@ -41,8 +41,8 @@ import '../utils/view_utils.dart'; class TokenNotifier extends StateNotifier { static final Map _hidingTimers = {}; late final Future initState; - final loadingRepoMutex = Mutex(); - final updatingTokensMutex = Mutex(); + final _loadingRepoMutex = Mutex(); + final _updatingTokensMutex = Mutex(); final TokenRepository _repo; final RsaUtils _rsaUtils; final PrivacyIdeaIOClient _ioClient; @@ -82,24 +82,24 @@ class TokenNotifier extends StateNotifier { /// Adds a token and returns true if successful, false if not. Future _addOrReplaceToken(Token token) async { - await loadingRepoMutex.acquire(); + await _loadingRepoMutex.acquire(); final success = await _repo.saveOrReplaceToken(token); if (!success) { Logger.warning( 'Saving token failed. Token: ${token.id}', name: 'token_notifier.dart#_addOrReplaceToken', ); - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return false; } state = state.addOrReplaceToken(token); - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return true; } /// Adds a list of tokens and returns the tokens that could not be added or replaced. Future> _addOrReplaceTokens(List tokens) async { - await loadingRepoMutex.acquire(); + await _loadingRepoMutex.acquire(); final failedTokens = await _repo.saveOrReplaceTokens(tokens); if (failedTokens.isNotEmpty) { Logger.warning( @@ -113,17 +113,17 @@ class TokenNotifier extends StateNotifier { } // [failedTokens] is empty, so every token was saved successfully and we dont need to filter the tokens state = state.addOrReplaceTokens(tokens); - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return []; } /// Replaces a token if it exists and returns true if successful, false if not. Future _replaceToken(Token token) async { - await loadingRepoMutex.acquire(); + await _loadingRepoMutex.acquire(); final (newState, replaced) = state.replaceToken(token); if (!replaced) { Logger.warning('Tried to replace a token that does not exist.', name: 'token_notifier.dart#_replaceToken'); - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return false; } final saved = await _repo.saveOrReplaceToken(token); @@ -132,17 +132,17 @@ class TokenNotifier extends StateNotifier { 'Saving token failed. Token: ${token.id}', name: 'token_notifier.dart#_replaceToken', ); - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return false; } state = newState; - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return true; } /// Returns a list of tokens that could not be replaced Future> _replaceTokens(List tokens) async { - await loadingRepoMutex.acquire(); + await _loadingRepoMutex.acquire(); final oldState = state; final (newState, failedToReplace) = state.replaceTokens(tokens); state = newState; @@ -157,16 +157,16 @@ class TokenNotifier extends StateNotifier { ); final recovered = oldState.tokens.whereType().where((oldToken) => failedToSave.contains(oldToken)).toList(); state = state.addOrReplaceTokens(recovered); - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return failedToSave; } - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return []; } /// Removes a token and returns true if successful, false if not. Future _removeToken(Token token) async { - await loadingRepoMutex.acquire(); + await _loadingRepoMutex.acquire(); state = state.withoutToken(token); final success = await _repo.deleteToken(token); @@ -176,17 +176,17 @@ class TokenNotifier extends StateNotifier { name: 'token_notifier.dart#_deleteTokensRepo', ); state = state.addOrReplaceToken(token); - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return false; } - loadingRepoMutex.release(); + _loadingRepoMutex.release(); _handlePushTokensIfExist(); return true; } /// Loads the tokens from the repository sets it as the new state and returns the new state. Future _loadFromRepo() async { - await loadingRepoMutex.acquire(); + await _loadingRepoMutex.acquire(); TokenState newState; try { List tokens; @@ -199,16 +199,16 @@ class TokenNotifier extends StateNotifier { name: 'token_notifier.dart#_loadFromRepo', error: e, ); - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return state; } - loadingRepoMutex.release(); + _loadingRepoMutex.release(); _handlePushTokensIfExist(); return newState; } Future _saveStateToRepo(TokenState state) async { - await loadingRepoMutex.acquire(); + await _loadingRepoMutex.acquire(); try { await _repo.saveOrReplaceTokens(state.tokens); } catch (e) { @@ -217,10 +217,10 @@ class TokenNotifier extends StateNotifier { name: 'token_notifier.dart#_saveStateToRepo', error: e, ); - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return false; } - loadingRepoMutex.release(); + _loadingRepoMutex.release(); return true; } @@ -233,24 +233,24 @@ class TokenNotifier extends StateNotifier { /// Updates a token and returns the updated token if successful, the old token if not and null if the token does not exist. Future _updateToken(T token, T Function(T) updater) async { - await updatingTokensMutex.acquire(); - await loadingRepoMutex.acquire(); - loadingRepoMutex.release(); + await _updatingTokensMutex.acquire(); + await _loadingRepoMutex.acquire(); + _loadingRepoMutex.release(); final current = state.currentOf(token); if (current == null) { Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#updateToken'); - updatingTokensMutex.release(); + _updatingTokensMutex.release(); return null; } final updated = updater(current); final replaced = await _replaceToken(updated); - updatingTokensMutex.release(); + _updatingTokensMutex.release(); return replaced ? updated : current; } /// Updates a list of tokens and returns the updated tokens if successful, the old tokens if not and an empty list if the tokens does not exist. Future> _updateTokens(List tokens, T Function(T) updater) async { - await updatingTokensMutex.acquire(); + await _updatingTokensMutex.acquire(); final oldState = state; List updatedTokens = []; @@ -268,7 +268,7 @@ class TokenNotifier extends StateNotifier { orElse: () => updated, )) .toList(); - updatingTokensMutex.release(); + _updatingTokensMutex.release(); return mergedTokens; } diff --git a/lib/utils/riverpod_providers.dart b/lib/utils/riverpod_providers.dart index 04da1c2e2..eac4e554c 100644 --- a/lib/utils/riverpod_providers.dart +++ b/lib/utils/riverpod_providers.dart @@ -1,6 +1,9 @@ +import 'dart:developer'; + import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:uni_links/uni_links.dart'; import '../l10n/app_localizations.dart'; @@ -11,6 +14,7 @@ import '../model/states/settings_state.dart'; import '../model/states/token_filter.dart'; import '../model/states/token_folder_state.dart'; import '../model/states/token_state.dart'; +import '../model/token_folder.dart'; import '../model/tokens/otp_token.dart'; import '../repo/preference_introduction_repository.dart'; import '../repo/preference_settings_repository.dart'; @@ -185,5 +189,34 @@ final homeWidgetProvider = StateProvider>( }, ); +final sortableProvider = StateNotifierProvider>( + (ref) { + final SortableNotifier notifier = SortableNotifier(); + Logger.info("New sortableProvider created", name: 'sortableProvider'); + ref.listen(tokenProvider, (previous, next) => notifier.handleNewList(next.tokens)); + ref.listen(tokenFolderProvider, (previous, next) => notifier.handleNewList(next.folders)); + ref.read(tokenProvider.notifier).initState.then((newState) => notifier.handleNewList(newState.tokens)); + ref.read(tokenFolderProvider.notifier).initState.then((newState) => notifier.handleNewList(newState.folders)); + return notifier; + }, +); + +class SortableNotifier extends StateNotifier> { + SortableNotifier({List initState = const []}) : super(initState); + + void handleNewList(List newList) { + log('T type: ${newList.runtimeType}', name: 'SortableNotifier#handleNewList'); + var newState = List.from(state); + newState.removeWhere((element) => element is T); + newState.addAll(newList); + newState = newState.sorted.withCurrentSortIndexSet(); + state = newState; + if (newList.any((element) => element.sortIndex == null)) { + globalRef?.read(tokenProvider.notifier).addOrReplaceTokens(newState.whereType().toList()); + globalRef?.read(tokenFolderProvider.notifier).addOrReplaceFolders(newState.whereType().toList()); + } + } +} + /// Only used for the app customizer final applicationCustomizerProvider = StateProvider((ref) => ApplicationCustomization.defaultCustomization); diff --git a/lib/utils/token_import_origins.dart b/lib/utils/token_import_origins.dart index 602906c86..0454f3d76 100644 --- a/lib/utils/token_import_origins.dart +++ b/lib/utils/token_import_origins.dart @@ -1,7 +1,7 @@ -import '../l10n/app_localizations.dart'; import '../mains/main_netknights.dart'; import '../model/enums/token_import_type.dart'; import '../model/token_import/token_import_origin.dart'; +import '../model/token_import/token_import_source.dart'; import '../processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart'; import '../processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart'; import '../processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart'; @@ -31,12 +31,12 @@ class TokenImportOrigins { TokenImportSource( processor: const PrivacyIDEAAuthenticatorQrProcessor(), type: TokenImportType.qrScan, - importHint: (context) => 'AppLocalizations.of(context)!.importHintPrivacyIDEAQrScan', + importHint: (localizations) => 'localizations.importHintPrivacyIDEAQrScan', ), TokenImportSource( processor: const PrivacyIDEAAuthenticatorImportFileProcessor(), type: TokenImportType.backupFile, - importHint: (context) => 'AppLocalizations.of(context)!.importHintPrivacyIDEAFile'), + importHint: (localizations) => 'localizations.importHintPrivacyIDEAFile'), ], ); @@ -47,12 +47,12 @@ class TokenImportOrigins { TokenImportSource( processor: const GoogleAuthenticatorQrProcessor(), type: TokenImportType.qrScan, - importHint: (context) => AppLocalizations.of(context)!.importHintGoogleQrScan, + importHint: (localizations) => localizations.importHintGoogleQrScan, ), TokenImportSource( processor: const GoogleAuthenticatorQrProcessor(), type: TokenImportType.qrFile, - importHint: (context) => AppLocalizations.of(context)!.importHintGoogleQrFile, + importHint: (localizations) => localizations.importHintGoogleQrFile, ), ], ); @@ -63,17 +63,17 @@ class TokenImportOrigins { TokenImportSource( processor: const AegisImportFileProcessor(), type: TokenImportType.backupFile, - importHint: (context) => AppLocalizations.of(context)!.importHintAegisBackupFile, + importHint: (localizations) => localizations.importHintAegisBackupFile, ), TokenImportSource( processor: const OtpAuthProcessor(), type: TokenImportType.qrScan, - importHint: (context) => AppLocalizations.of(context)!.importHintAegisQrScan, + importHint: (localizations) => localizations.importHintAegisQrScan, ), TokenImportSource( processor: const OtpAuthProcessor(), type: TokenImportType.link, - importHint: (context) => AppLocalizations.of(context)!.importHintAegisLink, + importHint: (localizations) => localizations.importHintAegisLink, ), ], ); @@ -84,7 +84,7 @@ class TokenImportOrigins { TokenImportSource( processor: const TwoFasFileImportProcessor(), type: TokenImportType.backupFile, - importHint: (context) => AppLocalizations.of(context)!.importHint2FAS, + importHint: (localizations) => localizations.importHint2FAS, ), ], ); @@ -95,7 +95,7 @@ class TokenImportOrigins { TokenImportSource( processor: const AuthenticatorProImportFileProcessor(), type: TokenImportType.backupFile, - importHint: (context) => AppLocalizations.of(context)!.importHintAuthenticatorProFile, + importHint: (localizations) => localizations.importHintAuthenticatorProFile, ), ], ); @@ -106,12 +106,12 @@ class TokenImportOrigins { TokenImportSource( processor: const FreeOtpPlusQrProcessor(), type: TokenImportType.qrScan, - importHint: (context) => AppLocalizations.of(context)!.importHintFreeOtpPlusQrScan, + importHint: (localizations) => localizations.importHintFreeOtpPlusQrScan, ), TokenImportSource( processor: const FreeOtpPlusFileProcessor(), type: TokenImportType.backupFile, - importHint: (context) => AppLocalizations.of(context)!.importHintFreeOtpPlusFile, + importHint: (localizations) => localizations.importHintFreeOtpPlusFile, ), ], ); diff --git a/lib/views/import_tokens_view/pages/import_start_page.dart b/lib/views/import_tokens_view/pages/import_start_page.dart index c41c27ba3..f5b5e6022 100644 --- a/lib/views/import_tokens_view/pages/import_start_page.dart +++ b/lib/views/import_tokens_view/pages/import_start_page.dart @@ -14,7 +14,7 @@ import 'package:privacyidea_authenticator/processors/scheme_processors/token_imp import 'package:zxing2/qrcode.dart'; import '../../../l10n/app_localizations.dart'; -import '../../../model/token_import/token_import_origin.dart'; +import '../../../model/token_import/token_import_source.dart'; import '../../../model/tokens/token.dart'; import '../../../processors/mixins/token_import_processor.dart'; import '../../../processors/token_import_file_processor/token_import_file_processor_interface.dart'; @@ -100,7 +100,7 @@ class _ImportStartPageState extends State { _errorText!, textAlign: TextAlign.center, ) - : Text(widget.selectedSource.importHint(context), textAlign: TextAlign.center), + : Text(widget.selectedSource.importHint(localizations), textAlign: TextAlign.center), if (widget.selectedSource.type == TokenImportType.link) ...[ const SizedBox(height: ImportTokensView.itemSpacingHorizontal), TextField( diff --git a/lib/views/import_tokens_view/pages/select_import_type_page.dart b/lib/views/import_tokens_view/pages/select_import_type_page.dart index 1dff759b0..0a2729385 100644 --- a/lib/views/import_tokens_view/pages/select_import_type_page.dart +++ b/lib/views/import_tokens_view/pages/select_import_type_page.dart @@ -1,5 +1,3 @@ -// ignore_for_file: prefer_const_constructors - import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:privacyidea_authenticator/model/extensions/enums/token_import_type_extension.dart'; @@ -7,6 +5,7 @@ import 'package:privacyidea_authenticator/model/extensions/enums/token_import_ty import '../../../l10n/app_localizations.dart'; import '../../../model/enums/token_import_type.dart'; import '../../../model/token_import/token_import_origin.dart'; +import '../../../model/token_import/token_import_source.dart'; import '../import_tokens_view.dart'; import 'import_start_page.dart'; @@ -67,7 +66,7 @@ class SelectImportTypePage extends StatelessWidget { Expanded( child: Icon(importEntity.type.icon), ), - Expanded(child: SizedBox()), + const Expanded(child: SizedBox()), ], ), onPressed: () => _routeStartPage(context: context, importSource: importEntity), diff --git a/lib/views/link_home_widget_view/link_home_widget_view.dart b/lib/views/link_home_widget_view/link_home_widget_view.dart index 19edfc08d..67c689577 100644 --- a/lib/views/link_home_widget_view/link_home_widget_view.dart +++ b/lib/views/link_home_widget_view/link_home_widget_view.dart @@ -35,7 +35,7 @@ class _LinkHomeWidgetViewState extends ConsumerState { body: ListView.builder( itemBuilder: (context, index) { final otpToken = otpTokens[index]; - final folderIsLocked = ref.watch(tokenFolderProvider).getFolderById(otpToken.folderId)?.isLocked ?? false; + final folderIsLocked = ref.watch(tokenFolderProvider).currentById(otpToken.folderId)?.isLocked ?? false; final otpString = otpToken.isLocked || folderIsLocked ? veilingCharacter * otpToken.otpValue.length : otpToken.otpValue; return ListTile( title: Text(otpToken.label), diff --git a/lib/views/main_view/main_view_widgets/drag_target_divider.dart b/lib/views/main_view/main_view_widgets/drag_target_divider.dart index 0388eec72..e2573ae67 100644 --- a/lib/views/main_view/main_view_widgets/drag_target_divider.dart +++ b/lib/views/main_view/main_view_widgets/drag_target_divider.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -82,9 +84,9 @@ class _DragTargetDividerState extends ConsumerState().toList(); final modifiedFolders = allSortables.whereType().toList(); ref.read(tokenProvider.notifier).addOrReplaceTokens(modifiedTokens); - ref.read(tokenFolderProvider.notifier).updateFolders(modifiedFolders); + ref.read(tokenFolderProvider.notifier).addOrReplaceFolders(modifiedFolders); } diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart b/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart index cb44c4390..48389c67f 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart @@ -47,7 +47,7 @@ class AddTokenFolderDialog extends ConsumerWidget { if (ref.read(introductionProvider).isCompleted(Introduction.addFolder) == false) { ref.read(introductionProvider.notifier).complete(Introduction.addFolder); } - ref.read(tokenFolderProvider.notifier).addFolder(textController.text); + ref.read(tokenFolderProvider.notifier).addNewFolder(textController.text); Navigator.pop(context); }), ], diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart index f2047f77c..835e34024 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart @@ -18,7 +18,7 @@ class LockTokenFolderAction extends StatelessWidget { foregroundColor: Theme.of(context).extension()!.foregroundColor, onPressed: (context) async { if (await lockAuth(localizedReason: AppLocalizations.of(context)!.unlock) == false) return; - globalRef?.read(tokenFolderProvider.notifier).updateFolder(folder.copyWith(isLocked: !folder.isLocked)); + globalRef?.read(tokenFolderProvider.notifier).toggleFolderLock(folder); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart index 14cbed59f..2e927c822 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart @@ -75,18 +75,24 @@ class RenameTokenFolderAction extends StatelessWidget { overflow: TextOverflow.fade, softWrap: false, ), - onPressed: () { + onPressed: () async { final newLabel = nameInputController.text.trim(); if (newLabel.isEmpty) return; - globalRef?.read(tokenFolderProvider.notifier).updateFolder(folder.copyWith(label: newLabel)); - - Logger.info( - 'Renamed token:', - name: 'token_widget_base.dart#TextButton#renameClicked', - error: '\'${folder.label}\' changed to \'$newLabel\'', - ); - - Navigator.of(context).pop(); + final success = await globalRef?.read(tokenFolderProvider.notifier).updateLabel(folder, newLabel); + if (success == true) { + Logger.info( + 'Renamed token:', + name: 'token_widget_base.dart#TextButton#renameClicked', + error: '\'${folder.label}\' changed to \'$newLabel\'', + ); + } else { + Logger.warning( + 'Failed to rename token', + name: 'token_widget_base.dart#TextButton#renameClicked', + error: '\'${folder.label}\' to \'$newLabel\'', + ); + } + if (context.mounted) Navigator.of(context).pop(); }, ), ], diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart index 2d310ee07..4d191bf97 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; @@ -55,7 +56,7 @@ class _TokenFolderExpandableState extends ConsumerState w if (widget.expandOverride != null) return; if (widget.folder.isExpanded != expandableController.expanded) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - globalRef?.read(tokenFolderProvider.notifier).updateFolder(widget.folder.copyWith(isExpanded: expandableController.expanded)); + globalRef?.read(tokenFolderProvider.notifier).updateFolder(widget.folder, (p0) => p0.copyWith(isExpanded: expandableController.expanded)); }); } }); @@ -129,6 +130,7 @@ class _TokenFolderExpandableState extends ConsumerState w }, onLeave: (data) => _expandTimer?.cancel(), onAcceptWithDetails: (details) { + log('Moving token to folder ${widget.folder.label}', name: 'TokenFolderExpandable'); ref.read(tokenProvider.notifier).updateToken( details.data, (p0) => p0.copyWith(folderId: () => widget.folder.folderId, sortIndex: (widget.folder.sortIndex!) + 1), diff --git a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart index 933c04345..528d05e20 100644 --- a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart +++ b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart @@ -1,7 +1,8 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:privacyidea_authenticator/model/states/token_state.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import '../../../model/mixins/sortable_mixin.dart'; @@ -56,16 +57,29 @@ class _MainViewTokensListState extends ConsumerState { @override Widget build(BuildContext context) { - final tokenFolders = ref.watch(tokenFolderProvider).folders; - final tokenState = ref.watch(tokenProvider); - final allowToRefresh = tokenState.hasPushTokens; final draggingSortable = ref.watch(draggingSortableProvider); - bool filterPushTokens = ref.watch(settingsProvider).hidePushTokens && tokenState.hasOTPTokens; - var allSortables = [...tokenFolders, ...tokenState.tokens]; - final List tokens = allSortables.whereType().toList(); - final tokensWithNoFolder = tokens.withoutFolder(exclude: filterPushTokens ? [PushToken] : []); + final allSortables = ref.watch(sortableProvider); + final allowToRefresh = allSortables.any((element) => element is PushToken); + bool filterPushTokens = ref.watch(settingsProvider).hidePushTokens && allowToRefresh; + + log('Sortables: ${allSortables.length} '); + + final sortables = []; + + for (var element in allSortables) { + if (element is! Token) { + sortables.add(element); + continue; + } + if (filterPushTokens == false && element.folderId == null) { + sortables.add(element); + continue; + } + } + // final List tokens = allSortables.whereType().toList(); + // final tokensWithNoFolder = tokens.withoutFolder(exclude: filterPushTokens ? [PushToken] : []); - List sortables = [...tokenFolders, ...tokensWithNoFolder]; + // List sortables = [...tokenFolders, ...tokensWithNoFolder]; return Stack( children: [ if (sortables.isEmpty) const NoTokenScreen(), diff --git a/test/tests_app_wrapper.mocks.dart b/test/tests_app_wrapper.mocks.dart index ebbf49195..3d940a926 100644 --- a/test/tests_app_wrapper.mocks.dart +++ b/test/tests_app_wrapper.mocks.dart @@ -221,18 +221,15 @@ class MockSettingsRepository extends _i1.Mock class MockTokenFolderRepository extends _i1.Mock implements _i10.TokenFolderRepository { @override - _i7.Future> saveOrReplaceFolders( - List<_i11.TokenFolder>? folders) => + _i7.Future saveReplaceList(List<_i11.TokenFolder>? folders) => (super.noSuchMethod( Invocation.method( - #saveOrReplaceFolders, + #saveReplaceList, [folders], ), - returnValue: - _i7.Future>.value(<_i11.TokenFolder>[]), - returnValueForMissingStub: - _i7.Future>.value(<_i11.TokenFolder>[]), - ) as _i7.Future>); + returnValue: _i7.Future.value(false), + returnValueForMissingStub: _i7.Future.value(false), + ) as _i7.Future); @override _i7.Future> loadFolders() => (super.noSuchMethod( diff --git a/test/unit_test/model/processor_result_test.dart b/test/unit_test/model/processor_result_test.dart index e69de29bb..89b5d16b9 100644 --- a/test/unit_test/model/processor_result_test.dart +++ b/test/unit_test/model/processor_result_test.dart @@ -0,0 +1,12 @@ +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:privacyidea_authenticator/model/processor_result.dart'; + +// void main() { +// _testProcessorResult(); +// } + +// void _testProcessorResult() { +// group('Processor Result', () { +// group('crea +// }); +// } \ No newline at end of file diff --git a/test/unit_test/model/states/token_folder_state_test.dart b/test/unit_test/model/states/token_folder_state_test.dart index 2c08bad92..c137bb027 100644 --- a/test/unit_test/model/states/token_folder_state_test.dart +++ b/test/unit_test/model/states/token_folder_state_test.dart @@ -14,7 +14,7 @@ void _testTokenFolderState() { expect(state.folders.first.folderId, 1); }); test('withFolder', () { - final newState = state.withFolder('newFolder'); + final newState = state.addNewFolder('newFolder'); expect(state.folders.first.label, 'label'); expect(state.folders.first.folderId, 1); expect(newState.folders.length, 2); @@ -24,7 +24,7 @@ void _testTokenFolderState() { expect(newState.folders.last.folderId, 2); }); test('withUpdated', () { - final newState = state.withUpdated([const TokenFolder(label: 'labelUpdated', folderId: 1)]); + final newState = state.addOrReplaceFolders([const TokenFolder(label: 'labelUpdated', folderId: 1)]); expect(state.folders.first.label, 'label'); expect(state.folders.first.folderId, 1); expect(newState.folders.length, 1); @@ -32,7 +32,7 @@ void _testTokenFolderState() { expect(newState.folders.first.folderId, 1); }); test('withoutFolder', () { - final newState = state.withoutFolder(const TokenFolder(label: 'label', folderId: 1)); + final newState = state.removeFolder(const TokenFolder(label: 'label', folderId: 1)); expect(state.folders.first.label, 'label'); expect(state.folders.first.folderId, 1); expect(newState.folders.length, 0); diff --git a/test/unit_test/model/token_import/token_import_origin_test.dart b/test/unit_test/model/token_import/token_import_origin_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/model/token_import/token_import_origin_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/model/token_import/token_origin_data_test.dart b/test/unit_test/model/token_import/token_origin_data_test.dart index 8b1378917..643d311a8 100644 --- a/test/unit_test/model/token_import/token_origin_data_test.dart +++ b/test/unit_test/model/token_import/token_origin_data_test.dart @@ -1 +1,56 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/token_import/token_origin_data.dart'; +import 'package:privacyidea_authenticator/model/version.dart'; +void main() { + _testTokenOriginData(); +} + +void _testTokenOriginData() { + group('Token Origin Data', () { + TokenOriginData; + group('create', () { + test('constructor', () { + final tokenOriginData = TokenOriginData( + source: TokenOriginSourceType.manually, + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.now(), + piServerVersion: const Version(1, 0, 0), + ); + expect(tokenOriginData.source, TokenOriginSourceType.manually); + expect(tokenOriginData.data, 'data'); + expect(tokenOriginData.appName, 'appName'); + expect(tokenOriginData.isPrivacyIdeaToken, true); + expect(tokenOriginData.createdAt, isA()); + expect(tokenOriginData.piServerVersion, isA()); + }); + test('copyWith', () { + final tokenOriginData = TokenOriginData( + source: TokenOriginSourceType.manually, + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.now(), + piServerVersion: const Version(1, 0, 0), + ); + final copy = tokenOriginData.copyWith( + source: TokenOriginSourceType.qrScan, + data: 'data2', + appName: 'appName2', + isPrivacyIdeaToken: false, + createdAt: DateTime.now().add(const Duration(days: 1)), + piServerVersion: const Version(1, 0, 1), + ); + expect(copy.source, TokenOriginSourceType.qrScan); + expect(copy.data, 'data2'); + expect(copy.appName, 'appName2'); + expect(copy.isPrivacyIdeaToken, false); + expect(copy.createdAt, isA()); + expect(copy.piServerVersion, isA()); + }); + }); + }); +} diff --git a/test/unit_test/state_notifiers/token_folder_notifier_test.dart b/test/unit_test/state_notifiers/token_folder_notifier_test.dart index 163830bbc..b698ba594 100644 --- a/test/unit_test/state_notifiers/token_folder_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_folder_notifier_test.dart @@ -22,17 +22,16 @@ void _testTokenFolderNotifier() { const before = []; const after = [TokenFolder(label: 'test', folderId: 1, isExpanded: true, isLocked: false, sortIndex: null)]; when(mockRepo.loadFolders()).thenAnswer((_) async => before); - when(mockRepo.saveOrReplaceFolders(after)).thenAnswer((_) async => []); + when(mockRepo.saveReplaceList(after)).thenAnswer((_) async => true); final testProvider = StateNotifierProvider((ref) => TokenFolderNotifier( repository: mockRepo, )); final notifier = container.read(testProvider.notifier); - await notifier.isLoading; - notifier.addFolder('test'); - await notifier.isLoading; + await notifier.initState; + await notifier.addNewFolder('test'); final state = container.read(testProvider); expect(state.folders, after); - verify(mockRepo.saveOrReplaceFolders(after)).called(1); + verify(mockRepo.saveReplaceList(after)).called(1); }); test('removeFolder', () async { @@ -41,17 +40,16 @@ void _testTokenFolderNotifier() { const before = [TokenFolder(label: 'test', folderId: 1, isExpanded: true, isLocked: false, sortIndex: null)]; const after = []; when(mockRepo.loadFolders()).thenAnswer((_) async => before); - when(mockRepo.saveOrReplaceFolders(after)).thenAnswer((_) async => []); + when(mockRepo.saveReplaceList(after)).thenAnswer((_) async => true); final testProvider = StateNotifierProvider((ref) => TokenFolderNotifier( repository: mockRepo, )); final notifier = container.read(testProvider.notifier); - await notifier.isLoading; - notifier.removeFolder(const TokenFolder(label: 'test', folderId: 1)); - await notifier.isLoading; + await notifier.initState; + await notifier.removeFolder(const TokenFolder(label: 'test', folderId: 1)); final state = container.read(testProvider); expect(state.folders, after); - verify(mockRepo.saveOrReplaceFolders(after)).called(1); + verify(mockRepo.saveReplaceList(after)).called(1); }); test('updateFolder', () async { final mockRepo = MockTokenFolderRepository(); @@ -59,17 +57,16 @@ void _testTokenFolderNotifier() { const before = [TokenFolder(label: 'test', folderId: 1, isExpanded: true, isLocked: false, sortIndex: null)]; const after = [TokenFolder(label: 'testUpdated', folderId: 1, isExpanded: true, isLocked: false, sortIndex: null)]; when(mockRepo.loadFolders()).thenAnswer((_) async => before); - when(mockRepo.saveOrReplaceFolders(after)).thenAnswer((_) async => []); + when(mockRepo.saveReplaceList(after)).thenAnswer((_) async => true); final testProvider = StateNotifierProvider((ref) => TokenFolderNotifier( repository: mockRepo, )); final notifier = container.read(testProvider.notifier); - await notifier.isLoading; - notifier.updateFolder(after.first); - await notifier.isLoading; + await notifier.initState; + await notifier.updateFolder(before.first, (p0) => after.first); final state = container.read(testProvider); expect(state.folders, after); - verify(mockRepo.saveOrReplaceFolders(after)).called(1); + verify(mockRepo.saveReplaceList(after)).called(1); }); test('updateFolders', () async { final mockRepo = MockTokenFolderRepository(); @@ -83,17 +80,16 @@ void _testTokenFolderNotifier() { TokenFolder(label: 'test2Updated', folderId: 2, isExpanded: true, isLocked: false, sortIndex: null), ]; when(mockRepo.loadFolders()).thenAnswer((_) async => before); - when(mockRepo.saveOrReplaceFolders(after)).thenAnswer((_) async => []); + when(mockRepo.saveReplaceList(after)).thenAnswer((_) async => true); final testProvider = StateNotifierProvider((ref) => TokenFolderNotifier( repository: mockRepo, )); final notifier = container.read(testProvider.notifier); - await notifier.isLoading; - notifier.updateFolders(after); - await notifier.isLoading; + await notifier.initState; + await notifier.addOrReplaceFolders(after); final state = container.read(testProvider); expect(state.folders, after); - verify(mockRepo.saveOrReplaceFolders(after)).called(1); + verify(mockRepo.saveReplaceList(after)).called(1); }); }); } diff --git a/test/unit_test/state_notifiers/token_folder_notifier_test.mocks.dart b/test/unit_test/state_notifiers/token_folder_notifier_test.mocks.dart index a03f84137..aed7f00ea 100644 --- a/test/unit_test/state_notifiers/token_folder_notifier_test.mocks.dart +++ b/test/unit_test/state_notifiers/token_folder_notifier_test.mocks.dart @@ -33,16 +33,14 @@ class MockTokenFolderRepository extends _i1.Mock } @override - _i3.Future> saveOrReplaceFolders( - List<_i4.TokenFolder>? folders) => + _i3.Future saveReplaceList(List<_i4.TokenFolder>? folders) => (super.noSuchMethod( Invocation.method( - #saveOrReplaceFolders, + #saveReplaceList, [folders], ), - returnValue: - _i3.Future>.value(<_i4.TokenFolder>[]), - ) as _i3.Future>); + returnValue: _i3.Future.value(false), + ) as _i3.Future); @override _i3.Future> loadFolders() => (super.noSuchMethod( From c6323af0fde35e56aff5e8b0036d96018ec35f72 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:27:15 +0200 Subject: [PATCH 08/11] added more tests --- lib/model/extensions/sortable_list.dart | 85 ++++++++ lib/model/mixins/sortable_mixin.dart | 65 ------ lib/model/push_request_queue.dart | 119 ----------- lib/utils/riverpod_providers.dart | 8 +- .../drag_target_divider.dart | 5 +- .../main_view_tokens_list.dart | 4 - .../model/extensions/sortable_list_test.dart | 199 ++++++++++++++++++ .../model/mixins/sortable_mixin_test.dart | 91 ++++++++ .../model/processor_result_test.dart | 42 +++- .../model/push_request_queue_test.dart | 0 test/unit_test/model/push_request_test.dart | 73 +++++++ 11 files changed, 487 insertions(+), 204 deletions(-) create mode 100644 lib/model/extensions/sortable_list.dart delete mode 100644 lib/model/push_request_queue.dart create mode 100644 test/unit_test/model/extensions/sortable_list_test.dart delete mode 100644 test/unit_test/model/push_request_queue_test.dart diff --git a/lib/model/extensions/sortable_list.dart b/lib/model/extensions/sortable_list.dart new file mode 100644 index 000000000..1c32c896b --- /dev/null +++ b/lib/model/extensions/sortable_list.dart @@ -0,0 +1,85 @@ +import '../mixins/sortable_mixin.dart'; + +extension SortableList on List { + List get sorted { + var list = List.from(this); + list.sort((a, b) => a.compareTo(b)); + print('-----------------------------------'); + list.forEach((element) { + print('sorted: ${element.runtimeType} Sortindex: ${element.sortIndex}'); + }); + print('-----------------------------------'); + return list; + } + + List fillNullIndices() { + var list = List.from(this); + var highestIndex = fold(0, (previousValue, element) { + if (element.sortIndex == null) return previousValue; + if (previousValue > element.sortIndex!) return previousValue; + return element.sortIndex!; + }); + for (var i = 0; i < list.length; i++) { + if (list[i].sortIndex == null) { + highestIndex++; + list[i] = list[i].copyWith(sortIndex: highestIndex) as T; + } + } + print('-----------------------------------'); + list.forEach((element) { + print('fillNullIndices: ${element.runtimeType} Sortindex: ${element.sortIndex}'); + }); + print('-----------------------------------'); + return list; + } + + /// Moves the [movedItem] to the position after [moveAfter] or before [moveBefore]. + /// If both [moveAfter] and [moveBefore] are null, the [movedItem] will be moved to the end of the list. + /// If both is set, [moveBefore] will be prioritized. + List moveBetween({T? moveAfter, required T movedItem, T? moveBefore}) { + var list = List.from(this).sorted.withCurrentSortIndexSet(); + final success = list.remove(movedItem); + if (!success) return list; + final newIndex = moveBefore != null + ? list.indexOf(moveBefore) + : moveAfter != null && list.contains(moveAfter) + ? list.indexOf(moveAfter) + 1 + : list.length; + list.insert(newIndex, movedItem); + list = list.withCurrentSortIndexSet(); + print('-----------------------------------'); + list.forEach((element) { + print('moveBetween: ${element.runtimeType} Sortindex: ${element.sortIndex}'); + }); + print('-----------------------------------'); + return list; + } + + List moveAllBetween({T? moveAfter, required List movedItems, T? moveBefore}) { + var list = List.from(this).sorted.withCurrentSortIndexSet(); + List removedItems = []; + for (final movedItem in movedItems) { + final success = list.remove(movedItem); + if (success) removedItems.add(movedItem); + } + if (removedItems.isEmpty) return list; + final newIndex = moveBefore != null + ? list.indexOf(moveBefore) + : moveAfter != null && list.contains(moveAfter) + ? list.indexOf(moveAfter) + 1 + : list.length; + list.insertAll(newIndex, removedItems); + list = list.withCurrentSortIndexSet(); + return list; + } + + List withCurrentSortIndexSet() { + final list = List.from(this); + for (var i = 0; i < list.length; i++) { + if (list[i].sortIndex != i) { + list[i] = list[i].copyWith(sortIndex: i) as T; + } + } + return list; + } +} diff --git a/lib/model/mixins/sortable_mixin.dart b/lib/model/mixins/sortable_mixin.dart index ac1882eca..5274c67f0 100644 --- a/lib/model/mixins/sortable_mixin.dart +++ b/lib/model/mixins/sortable_mixin.dart @@ -14,68 +14,3 @@ mixin SortableMixin { return sortIndex!.compareTo(other.sortIndex!); } } - -extension SortableList on List { - List get sorted { - var list = List.from(this); - var highestIndex = 0; - for (var item in list) { - if (item.sortIndex != null && item.sortIndex! > highestIndex) { - highestIndex = item.sortIndex!; - } - } - - list.sort((a, b) => a.compareTo(b)); - for (var i = 0; i < list.length; i++) { - if (list[i].sortIndex == null) { - highestIndex++; - list[i] = list[i].copyWith(sortIndex: highestIndex) as T; - } else { - highestIndex = list[i].sortIndex!; - } - } - return list; - } - - List moveBetween({T? moveAfter, required T movedItem, T? moveBefore}) { - var list = List.from(this).sorted.withCurrentSortIndexSet(); - final success = list.remove(movedItem); - if (!success) return list; - final newIndex = moveBefore != null - ? list.indexOf(moveBefore) - : moveAfter != null && list.contains(moveAfter) - ? list.indexOf(moveAfter) + 1 - : list.length; - list.insert(newIndex, movedItem); - list = list.withCurrentSortIndexSet(); - return list; - } - - List moveAllBetween({T? moveAfter, required List movedItems, T? moveBefore}) { - var list = List.from(this).sorted.withCurrentSortIndexSet(); - List removedItems = []; - for (final movedItem in movedItems) { - final success = list.remove(movedItem); - if (success) removedItems.add(movedItem); - } - if (removedItems.isEmpty) return list; - final newIndex = moveBefore != null - ? list.indexOf(moveBefore) - : moveAfter != null && list.contains(moveAfter) - ? list.indexOf(moveAfter) + 1 - : list.length; - list.insertAll(newIndex, removedItems); - list = list.withCurrentSortIndexSet(); - return list; - } - - List withCurrentSortIndexSet() { - final list = List.from(this); - for (var i = 0; i < list.length; i++) { - if (list[i].sortIndex != i) { - list[i] = list[i].copyWith(sortIndex: i) as T; - } - } - return list; - } -} diff --git a/lib/model/push_request_queue.dart b/lib/model/push_request_queue.dart deleted file mode 100644 index a725b6fba..000000000 --- a/lib/model/push_request_queue.dart +++ /dev/null @@ -1,119 +0,0 @@ -// import 'dart:collection'; -// import 'package:json_annotation/json_annotation.dart'; - -// import 'push_request.dart'; - -// part 'push_request_queue.g.dart'; - -// @JsonSerializable() -// class PushRequestQueue implements Iterable { -// const PushRequestQueue({List? list}) : _list = list ?? const []; -// final List _list; -// List get list => _list; - -// @override -// bool operator ==(Object other) { -// if (identical(this, other)) return true; -// return other is PushRequestQueue && _listsAreEqual(list, other.list); -// } - -// @override -// int get hashCode => list.hashCode; - -// bool _listsAreEqual(List l1, List l2) { -// if (l1.length != l2.length) return false; -// for (int i = 0; i < l1.length - 1; i++) { -// if (l1[i] != l2[i]) return false; -// } -// return true; -// } - -// factory PushRequestQueue.fromJson(Map json) => _$PushRequestQueueFromJson(json); -// Map toJson() => _$PushRequestQueueToJson(this); - -// @override -// PushRequest get first => list.first; -// @override -// PushRequest get last => list.last; -// @override -// int get length => list.length; -// void add(PushRequest value) => list.add(value); -// bool replace(PushRequest value) { -// final index = list.indexWhere((element) => element.id == value.id); -// if (index == -1) return false; -// list[index] = value; -// return true; -// } - -// void addOrReplace(PushRequest value) { -// final index = list.indexWhere((element) => element.id == value.id); -// if (index != -1) { -// list[index] = value; -// } else { -// list.add(value); -// } -// } - -// void addAll(Iterable iterable) => list.addAll(iterable); -// void addFirst(PushRequest value) => list.insert(0, value); -// @override -// bool any(bool Function(PushRequest element) test) => list.any(test); -// @override -// Queue cast() => list.cast() as Queue; -// void clear() => list.clear(); -// @override -// bool contains(Object? element) => list.contains(element); -// @override -// PushRequest elementAt(int index) => list.elementAt(index); -// @override -// bool every(bool Function(PushRequest element) test) => list.every(test); -// @override -// Iterable expand(Iterable Function(PushRequest element) toElements) => list.expand(toElements); -// @override -// PushRequest firstWhere(bool Function(PushRequest element) test, {PushRequest Function()? orElse}) => list.firstWhere(test, orElse: orElse); -// @override -// T fold(T initialValue, T Function(T previousValue, PushRequest element) combine) => list.fold(initialValue, combine); -// @override -// Iterable followedBy(Iterable other) => list.followedBy(other); -// @override -// void forEach(void Function(PushRequest element) action) => list.forEach(action); -// @override -// bool get isEmpty => list.isEmpty; -// @override -// bool get isNotEmpty => list.isNotEmpty; -// @override -// Iterator get iterator => list.iterator; -// @override -// String join([String separator = ""]) => list.join(separator); -// @override -// PushRequest lastWhere(bool Function(PushRequest element) test, {PushRequest Function()? orElse}) => list.lastWhere(test, orElse: orElse); -// @override -// Iterable map(T Function(PushRequest e) toElement) => list.map(toElement); -// @override -// PushRequest reduce(PushRequest Function(PushRequest value, PushRequest element) combine) => list.reduce(combine); -// bool remove(Object? value) => list.remove(value); -// PushRequest? peek() => list.isNotEmpty ? list.first : null; -// PushRequest pop() => list.removeAt(0); -// PushRequest? tryPop() => list.isNotEmpty ? pop() : null; -// @override -// PushRequest get single => list.single; -// @override -// PushRequest singleWhere(bool Function(PushRequest element) test, {PushRequest Function()? orElse}) => list.singleWhere(test, orElse: orElse); -// @override -// Iterable skip(int count) => list.skip(count); -// @override -// Iterable skipWhile(bool Function(PushRequest value) test) => list.skipWhile(test); -// @override -// Iterable take(int count) => list.take(count); -// @override -// Iterable takeWhile(bool Function(PushRequest value) test) => list.takeWhile(test); -// @override -// List toList({bool growable = true}) => list.toList(growable: growable); -// PushRequestQueue copy() => PushRequestQueue(list: list.toList()); -// @override -// Set toSet() => list.toSet(); -// @override -// Iterable where(bool Function(PushRequest element) test) => list.where(test); -// @override -// Iterable whereType() => list.whereType(); -// } diff --git a/lib/utils/riverpod_providers.dart b/lib/utils/riverpod_providers.dart index eac4e554c..c8e2591f7 100644 --- a/lib/utils/riverpod_providers.dart +++ b/lib/utils/riverpod_providers.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/model/extensions/sortable_list.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:uni_links/uni_links.dart'; @@ -209,11 +210,10 @@ class SortableNotifier extends StateNotifier> { var newState = List.from(state); newState.removeWhere((element) => element is T); newState.addAll(newList); - newState = newState.sorted.withCurrentSortIndexSet(); - state = newState; + state = newState.sorted.fillNullIndices(); if (newList.any((element) => element.sortIndex == null)) { - globalRef?.read(tokenProvider.notifier).addOrReplaceTokens(newState.whereType().toList()); - globalRef?.read(tokenFolderProvider.notifier).addOrReplaceFolders(newState.whereType().toList()); + globalRef?.read(tokenProvider.notifier).addOrReplaceTokens(state.whereType().toList()); + globalRef?.read(tokenFolderProvider.notifier).addOrReplaceFolders(state.whereType().toList()); } } } diff --git a/lib/views/main_view/main_view_widgets/drag_target_divider.dart b/lib/views/main_view/main_view_widgets/drag_target_divider.dart index e2573ae67..a1f039a00 100644 --- a/lib/views/main_view/main_view_widgets/drag_target_divider.dart +++ b/lib/views/main_view/main_view_widgets/drag_target_divider.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/model/extensions/sortable_list.dart'; import '../../../model/mixins/sortable_mixin.dart'; import '../../../model/token_folder.dart'; @@ -121,8 +122,8 @@ void _onAccept({ final tokensInFolder = ref.read(tokenProvider).tokens.where((element) => element.folderId == dragedSortable.folderId).toList(); final allMovingItems = [dragedSortable, ...tokensInFolder]; allSortables = allSortables.moveAllBetween(moveAfter: previousSortable, movedItems: allMovingItems, moveBefore: nextSortable); - } else { - allSortables = allSortables.moveBetween(moveAfter: previousSortable, movedItem: dragedSortable as Token, moveBefore: nextSortable); + } else if (dragedSortable is Token) { + allSortables = allSortables.moveBetween(moveAfter: previousSortable, movedItem: dragedSortable, moveBefore: nextSortable); allSortables = allSortables.map((e) { return e is Token && e.id == dragedSortable.id ? e.copyWith(folderId: () => dependingFolder?.folderId) : e; }).toList(); diff --git a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart index 528d05e20..17a1c5050 100644 --- a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart +++ b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -62,8 +60,6 @@ class _MainViewTokensListState extends ConsumerState { final allowToRefresh = allSortables.any((element) => element is PushToken); bool filterPushTokens = ref.watch(settingsProvider).hidePushTokens && allowToRefresh; - log('Sortables: ${allSortables.length} '); - final sortables = []; for (var element in allSortables) { diff --git a/test/unit_test/model/extensions/sortable_list_test.dart b/test/unit_test/model/extensions/sortable_list_test.dart new file mode 100644 index 000000000..cde128675 --- /dev/null +++ b/test/unit_test/model/extensions/sortable_list_test.dart @@ -0,0 +1,199 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/extensions/sortable_list.dart'; +import 'package:privacyidea_authenticator/model/mixins/sortable_mixin.dart'; + +void main() { + _testSortableList(); +} + +class _SortableTestClass with SortableMixin { + @override + int? sortIndex; + String name; + _SortableTestClass({this.sortIndex, required this.name}); + + @override + SortableMixin copyWith({int? sortIndex, String? name}) => _SortableTestClass( + sortIndex: sortIndex ?? this.sortIndex, + name: name ?? this.name, + ); + @override + operator ==(Object other) => other is _SortableTestClass && other.name == name; + @override + int get hashCode => name.hashCode; + + @override + String toString() => "_SortableTestClass(sortIndex: $sortIndex, name: '$name')"; +} + +void _testSortableList() { + group('Sortable List', () { + group('sorted', () { + // Arrange + test('1-5', () { + final list = [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + ]; + // Act + final result = list.sorted; + // Assert + expect(result, [ + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 5, name: '5'), + ]); + }); + test('with gaps', () { + final list = [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 12, name: '12'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 8, name: '8'), + ]; + // Act + final result = list.sorted; + // Assert + expect(result, [ + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: 8, name: '8'), + _SortableTestClass(sortIndex: 12, name: '12'), + _SortableTestClass(sortIndex: null, name: 'null'), + ]); + }); + test('1-5 and multible nulls', () { + final list = [ + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + ]; + // Act + final result = list.sorted; + // Assert + expect(result, [ + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: null, name: 'null'), + ]); + }); + }); + + group('fill Null Indices', () { + test('1-5', () { + final result = [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + ].fillNullIndices(); + expect(result, [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 5, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + ]); + }); + test('with gaps', () { + final result = [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 12, name: '12'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 8, name: '8'), + ].fillNullIndices(); + expect(result, [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 12, name: '12'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: 13, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 8, name: '8'), + ]); + }); + }); + group('move between', () { + test('1-5', () { + final movedItem = _SortableTestClass(sortIndex: 2, name: '2'); + final moveBefore = _SortableTestClass(sortIndex: 3, name: '3'); + final list = [ + moveBefore, + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 5, name: '5'), + movedItem, + _SortableTestClass(sortIndex: 4, name: '4'), + ]; + // Act + final result = list.moveBetween(moveAfter: null, movedItem: movedItem, moveBefore: list[1]); + // Assert + expect(result, [ + _SortableTestClass(sortIndex: 0, name: '2'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 2, name: '3'), + _SortableTestClass(sortIndex: 3, name: '4'), + _SortableTestClass(sortIndex: 4, name: '5'), + ]); + }); + test('with gaps', () { + final moveAfter = _SortableTestClass(sortIndex: 3, name: '3'); + final movedItem = _SortableTestClass(sortIndex: 12, name: '12'); + final moveBefore = _SortableTestClass(sortIndex: 5, name: '5'); + final list = [ + moveAfter, + _SortableTestClass(sortIndex: 1, name: '1'), + movedItem, + moveBefore, + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 8, name: '8'), + ]; + // Act + final result = list.moveBetween(moveAfter: moveAfter, movedItem: movedItem, moveBefore: moveBefore); + // Assert + expect(result, [ + _SortableTestClass(sortIndex: 0, name: '1'), + _SortableTestClass(sortIndex: 1, name: '2'), + _SortableTestClass(sortIndex: 2, name: '3'), + _SortableTestClass(sortIndex: 3, name: '4'), + _SortableTestClass(sortIndex: 4, name: '12'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: 6, name: '8'), + _SortableTestClass(sortIndex: 7, name: 'null'), + ]); + }); + }); +/* +List moveBetween({T? moveAfter, required T movedItem, T? moveBefore}) { +List moveAllBetween({T? moveAfter, required List movedItems, T? moveBefore}) { +List withCurrentSortIndexSet() { +*/ + }); +} diff --git a/test/unit_test/model/mixins/sortable_mixin_test.dart b/test/unit_test/model/mixins/sortable_mixin_test.dart index e69de29bb..83861ab47 100644 --- a/test/unit_test/model/mixins/sortable_mixin_test.dart +++ b/test/unit_test/model/mixins/sortable_mixin_test.dart @@ -0,0 +1,91 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/mixins/sortable_mixin.dart'; + +void main() { + _testSortableMixin(); +} + +class _SortableTestClass with SortableMixin { + @override + int? sortIndex; + String name; + _SortableTestClass({this.sortIndex, required this.name}); + + @override + SortableMixin copyWith({int? sortIndex, String? name}) => _SortableTestClass( + sortIndex: sortIndex ?? this.sortIndex, + name: name ?? this.name, + ); + @override + operator ==(Object other) => other is _SortableTestClass && other.name == name; + @override + int get hashCode => name.hashCode; + + @override + String toString() => "_SortableTestClass(sortIndex: $sortIndex, name: '$name')"; +} + +void _testSortableMixin() { + group('SortableMixin', () { + group('compareTo equal', () { + test('number', () { + // Arrange + final a = _SortableTestClass(sortIndex: 1, name: '1'); + final b = _SortableTestClass(sortIndex: 1, name: '1'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, 0); + }); + test('null', () { + // Arrange + final a = _SortableTestClass(sortIndex: null, name: 'null'); + final b = _SortableTestClass(sortIndex: null, name: 'null'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, 0); + }); + }); + group('compareTo a < b', () { + test('a < b', () { + // Arrange + final a = _SortableTestClass(sortIndex: 1, name: '1'); + final b = _SortableTestClass(sortIndex: 2, name: '2'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, -1); + }); + test('a = 1, b = null', () { + // Arrange + final a = _SortableTestClass(sortIndex: 1, name: '1'); + final b = _SortableTestClass(sortIndex: null, name: 'null'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, -1); + }); + }); + group('compareTo a > b', () { + test('a > b', () { + // Arrange + final a = _SortableTestClass(sortIndex: 2, name: '2'); + final b = _SortableTestClass(sortIndex: 1, name: '1'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, 1); + }); + test('a = null, b = 1', () { + // Arrange + final a = _SortableTestClass(sortIndex: null, name: 'null'); + final b = _SortableTestClass(sortIndex: 1, name: '1'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, 1); + }); + }); + }); +} diff --git a/test/unit_test/model/processor_result_test.dart b/test/unit_test/model/processor_result_test.dart index 89b5d16b9..49ae3ca5d 100644 --- a/test/unit_test/model/processor_result_test.dart +++ b/test/unit_test/model/processor_result_test.dart @@ -1,12 +1,34 @@ -// import 'package:flutter_test/flutter_test.dart'; -// import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; -// void main() { -// _testProcessorResult(); -// } +void main() { + _testProcessorResult(); +} -// void _testProcessorResult() { -// group('Processor Result', () { -// group('crea -// }); -// } \ No newline at end of file +void _testProcessorResult() { + group('Processor Result', () { + group('constructors', () { + test('success', () { + const result = ProcessorResultSuccess('data'); + expect(result.resultData, 'data'); + }); + test('error', () { + const result = ProcessorResultError('error'); + expect(result, isA()); + expect(result.errorMessage, 'error'); + }); + }); + group('factories', () { + test('success', () { + final result = ProcessorResult.success('data'); + expect(result, isA()); + expect((result as ProcessorResultSuccess).resultData, 'data'); + }); + test('error', () { + final result = ProcessorResult.error('error'); + expect(result, isA()); + expect((result as ProcessorResultError).errorMessage, 'error'); + }); + }); + }); +} diff --git a/test/unit_test/model/push_request_queue_test.dart b/test/unit_test/model/push_request_queue_test.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/unit_test/model/push_request_test.dart b/test/unit_test/model/push_request_test.dart index e69de29bb..2d774a1ff 100644 --- a/test/unit_test/model/push_request_test.dart +++ b/test/unit_test/model/push_request_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/push_request.dart'; + +void main() { + _testPushRequest(); +} + +void _testPushRequest() { + group('Push Request', () { + group('creation', () { + test('constructor', () { + // Arrange + final request = PushRequest( + title: 'title', + question: 'question', + uri: Uri.parse('https://example.com'), + nonce: 'nonce', + sslVerify: true, + id: 1, + expirationDate: DateTime.now(), + ); + // Assert + expect(request.title, 'title'); + expect(request.question, 'question'); + expect(request.uri, Uri.parse('https://example.com')); + expect(request.nonce, 'nonce'); + expect(request.sslVerify, true); + expect(request.id, 1); + expect(request.expirationDate, isA()); + expect(request.serial, ''); + expect(request.signature, ''); + expect(request.accepted, null); + }); + test('copyWith', () { + final dateTimeAfter = DateTime.now().add(const Duration(days: 1)); + // Arrange + final request = PushRequest( + title: 'title', + question: 'question', + uri: Uri.parse('https://example.com'), + nonce: 'nonce', + sslVerify: true, + id: 1, + expirationDate: DateTime.now(), + ); + // Act + final copy = request.copyWith( + title: 'new title', + question: 'new question', + uri: Uri.parse('https://new.example.com'), + nonce: 'new nonce', + sslVerify: false, + id: 2, + expirationDate: dateTimeAfter, + serial: 'serial', + signature: 'signature', + accepted: true, + ); + // Assert + expect(copy.title, 'new title'); + expect(copy.question, 'new question'); + expect(copy.uri, Uri.parse('https://new.example.com')); + expect(copy.nonce, 'new nonce'); + expect(copy.sslVerify, false); + expect(copy.id, 2); + expect(copy.expirationDate, equals(dateTimeAfter)); + expect(copy.serial, 'serial'); + expect(copy.signature, 'signature'); + expect(copy.accepted, true); + }); + }); + }); +} From 67d8f2ebbf5e2885d2a6be6b9e7b78df6049ae68 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:28:00 +0200 Subject: [PATCH 09/11] added localized errors to otpauth processor --- lib/l10n/app_en.arb | 44 +++- lib/l10n/app_localizations.dart | 24 +- lib/l10n/app_localizations_cs.dart | 18 +- lib/l10n/app_localizations_de.dart | 18 +- lib/l10n/app_localizations_en.dart | 18 +- lib/l10n/app_localizations_es.dart | 18 +- lib/l10n/app_localizations_fr.dart | 18 +- lib/l10n/app_localizations_nl.dart | 18 +- lib/l10n/app_localizations_pl.dart | 18 +- lib/model/processor_result.dart | 12 +- lib/model/push_request.dart | 2 +- lib/model/serializable_RSA_private_key.dart | 15 -- lib/model/serializable_RSA_private_key.g.dart | 25 -- lib/model/serializable_RSA_public_key.dart | 15 -- lib/model/serializable_RSA_public_key.g.dart | 21 -- lib/model/tokens/push_token.dart | 1 + .../free_otp_plus_qr_processor.dart | 24 +- .../google_authenticator_qr_processor.dart | 2 +- .../otp_auth_processor.dart | 121 ++++++---- ...rivacyidea_authenticator_qr_processor.dart | 2 +- .../aegis_import_file_processor.dart | 4 +- ...thenticator_pro_import_file_processor.dart | 12 +- .../free_otp_plus_file_processor.dart | 6 +- ...a_authenticator_import_file_processor.dart | 2 +- .../two_fas_import_file_processor.dart | 4 +- lib/utils/globals.dart | 9 +- lib/utils/image_converter.dart | 7 + lib/utils/push_provider.dart | 3 + .../pages/import_plain_tokens_page.dart | 2 +- .../model/extensions/sortable_list_test.dart | 5 - .../model/processor_result_test.dart | 12 +- test/unit_test/model/push_request_test.dart | 215 ++++++++++++++++++ .../serializable_RSA_private_key_test.dart | 0 .../serializable_RSA_public_key_test.dart | 0 test/unit_test/model/version_test.dart | 28 +++ .../mixins/token_import_processor_test.dart | 1 - .../home_widget_navigate_processor_test.dart | 1 - ...ation_scheme_processor_interface_test.dart | 1 - .../free_otp_plus_qr_processor_test.dart | 51 +++++ 39 files changed, 562 insertions(+), 235 deletions(-) delete mode 100644 lib/model/serializable_RSA_private_key.dart delete mode 100644 lib/model/serializable_RSA_private_key.g.dart delete mode 100644 lib/model/serializable_RSA_public_key.dart delete mode 100644 lib/model/serializable_RSA_public_key.g.dart delete mode 100644 test/unit_test/model/serializable_RSA_private_key_test.dart delete mode 100644 test/unit_test/model/serializable_RSA_public_key_test.dart delete mode 100644 test/unit_test/processors/mixins/token_import_processor_test.dart delete mode 100644 test/unit_test/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor_test.dart delete mode 100644 test/unit_test/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface_test.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cf6794884..2937cd9e0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -608,17 +608,6 @@ } }, "thisAppIsOpenSource": "This Application is Open Source\nVisit us on GitHub", - "invalidArgument": "{argument} is not a valid value for {type}", - "@invalidArgument": { - "placeholders": { - "argument": { - "example": "abc" - }, - "type": { - "example": "number" - } - } - }, "importExportTokens": "Import/Export tokens", "exportNonPrivacyIDEATokens": "Export non-privacyIDEA tokens", "selectTokensToExport": "{count, plural, zero{} one{Select token to export} other{Select tokens to export}}", @@ -637,5 +626,36 @@ "oneMore": "One more", "done": "Done", "confirmPassword": "Confirm password", - "secretIsRequired": "Secret is required" + "secretIsRequired": "Secret is required", + "tokenDataParseError": "Token data could not be parsed", + "missingRequiredParameter": "Value for parameter [{counter}] is required and is missing", + "@missingRequiredParameter": { + "placeholders": { + "counter": { + "example": "counter" + } + } + }, + "invalidValueForParameter": "[{value}] is not a valid value for uri parameter [parameter].", + "@invalidValueForParameter": { + "placeholders": { + "value": { + "example": "abc" + }, + "parameter": { + "example": "number" + } + } + }, + "unsupported": "The {name} [{value}] is not supported by this version of the app.", + "@unsupported": { + "placeholders": { + "name": { + "example": "piauth version" + }, + "value": { + "example": "5" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 6d4f1755f..b4ef15da1 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1531,12 +1531,6 @@ abstract class AppLocalizations { /// **'This Application is Open Source\nVisit us on GitHub'** String get thisAppIsOpenSource; - /// No description provided for @invalidArgument. - /// - /// In en, this message translates to: - /// **'{argument} is not a valid value for {type}'** - String invalidArgument(Object argument, Object type); - /// No description provided for @importExportTokens. /// /// In en, this message translates to: @@ -1650,6 +1644,24 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Secret is required'** String get secretIsRequired; + + /// No description provided for @tokenDataParseError. + /// + /// In en, this message translates to: + /// **'Token data could not be parsed'** + String get tokenDataParseError; + + /// No description provided for @missingRequiredParameter. + /// + /// In en, this message translates to: + /// **'Value for parameter [{counter}] is required and is missing'** + String missingRequiredParameter(Object counter); + + /// No description provided for @invalidValueForParameter. + /// + /// In en, this message translates to: + /// **'[{value}] is not a valid value for uri parameter [parameter].'** + String invalidValueForParameter(Object value, Object parameter); } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_cs.dart b/lib/l10n/app_localizations_cs.dart index 0b43f02fa..e8e93e82d 100644 --- a/lib/l10n/app_localizations_cs.dart +++ b/lib/l10n/app_localizations_cs.dart @@ -792,11 +792,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get thisAppIsOpenSource => 'Tato aplikace má otevřený zdrojový kód\nNavštivte nás na GitHub'; - @override - String invalidArgument(Object argument, Object type) { - return '$argument není platná hodnota pro $type'; - } - @override String get importExportTokens => 'Import/Exportovat žetony'; @@ -862,4 +857,17 @@ class AppLocalizationsCs extends AppLocalizations { @override String get secretIsRequired => 'Secret is required'; + + @override + String get tokenDataParseError => 'Token data could not be parsed'; + + @override + String missingRequiredParameter(Object counter) { + return 'Value for parameter [$counter] is required and is missing'; + } + + @override + String invalidValueForParameter(Object value, Object parameter) { + return '[$value] is not a valid value for uri parameter [parameter].'; + } } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 448c475fa..f2c4133e1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -792,11 +792,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get thisAppIsOpenSource => 'Diese App ist Open Source\nBesuchen Sie uns auf GitHub'; - @override - String invalidArgument(Object argument, Object type) { - return '$argument ist kein gültiger Wert für $type'; - } - @override String get importExportTokens => 'Token importieren/exportieren'; @@ -862,4 +857,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String get secretIsRequired => 'Secret is required'; + + @override + String get tokenDataParseError => 'Token data could not be parsed'; + + @override + String missingRequiredParameter(Object counter) { + return 'Value for parameter [$counter] is required and is missing'; + } + + @override + String invalidValueForParameter(Object value, Object parameter) { + return '[$value] is not a valid value for uri parameter [parameter].'; + } } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index e0318362a..0aad88a5f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -792,11 +792,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get thisAppIsOpenSource => 'This Application is Open Source\nVisit us on GitHub'; - @override - String invalidArgument(Object argument, Object type) { - return '$argument is not a valid value for $type'; - } - @override String get importExportTokens => 'Import/Export tokens'; @@ -862,4 +857,17 @@ class AppLocalizationsEn extends AppLocalizations { @override String get secretIsRequired => 'Secret is required'; + + @override + String get tokenDataParseError => 'Token data could not be parsed'; + + @override + String missingRequiredParameter(Object counter) { + return 'Value for parameter [$counter] is required and is missing'; + } + + @override + String invalidValueForParameter(Object value, Object parameter) { + return '[$value] is not a valid value for uri parameter [parameter].'; + } } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 479edee8b..194192b9a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -792,11 +792,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get thisAppIsOpenSource => 'Esta aplicación es de código abierto\nVisítanos en GitHub'; - @override - String invalidArgument(Object argument, Object type) { - return '$argument no es un valor válido para $type'; - } - @override String get importExportTokens => 'Importar/Exportar tokens'; @@ -862,4 +857,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get secretIsRequired => 'Secret is required'; + + @override + String get tokenDataParseError => 'Token data could not be parsed'; + + @override + String missingRequiredParameter(Object counter) { + return 'Value for parameter [$counter] is required and is missing'; + } + + @override + String invalidValueForParameter(Object value, Object parameter) { + return '[$value] is not a valid value for uri parameter [parameter].'; + } } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 7935996b1..9ab2c39ff 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -792,11 +792,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get thisAppIsOpenSource => 'Cette application est open source\nRendez-nous visite sur GitHub'; - @override - String invalidArgument(Object argument, Object type) { - return '$argument n\'est pas une valeur valide pour $type'; - } - @override String get importExportTokens => 'Importer/Exporter les jetons'; @@ -862,4 +857,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String get secretIsRequired => 'Secret is required'; + + @override + String get tokenDataParseError => 'Token data could not be parsed'; + + @override + String missingRequiredParameter(Object counter) { + return 'Value for parameter [$counter] is required and is missing'; + } + + @override + String invalidValueForParameter(Object value, Object parameter) { + return '[$value] is not a valid value for uri parameter [parameter].'; + } } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index f104e8ae2..b3db085ce 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -792,11 +792,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get thisAppIsOpenSource => 'Deze app is open source\nBezoek ons op GitHub'; - @override - String invalidArgument(Object argument, Object type) { - return '$argument is geen geldige waarde voor $type'; - } - @override String get importExportTokens => 'Tokens importeren/exporteren'; @@ -862,4 +857,17 @@ class AppLocalizationsNl extends AppLocalizations { @override String get secretIsRequired => 'Secret is required'; + + @override + String get tokenDataParseError => 'Token data could not be parsed'; + + @override + String missingRequiredParameter(Object counter) { + return 'Value for parameter [$counter] is required and is missing'; + } + + @override + String invalidValueForParameter(Object value, Object parameter) { + return '[$value] is not a valid value for uri parameter [parameter].'; + } } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 4d0db1071..34df8103e 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -792,11 +792,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get thisAppIsOpenSource => 'Ta aplikacja jest open source\nOdwiedź nas na GitHub'; - @override - String invalidArgument(Object argument, Object type) { - return '$argument nie jest prawidłową wartością dla $type'; - } - @override String get importExportTokens => 'Importuj/Eksportuj tokeny'; @@ -862,4 +857,17 @@ class AppLocalizationsPl extends AppLocalizations { @override String get secretIsRequired => 'Secret is required'; + + @override + String get tokenDataParseError => 'Token data could not be parsed'; + + @override + String missingRequiredParameter(Object counter) { + return 'Value for parameter [$counter] is required and is missing'; + } + + @override + String invalidValueForParameter(Object value, Object parameter) { + return '[$value] is not a valid value for uri parameter [parameter].'; + } } diff --git a/lib/model/processor_result.dart b/lib/model/processor_result.dart index 62363792f..67a5495cd 100644 --- a/lib/model/processor_result.dart +++ b/lib/model/processor_result.dart @@ -1,7 +1,7 @@ abstract class ProcessorResult { const ProcessorResult(); factory ProcessorResult.success(T data) => ProcessorResultSuccess(data); - factory ProcessorResult.error(String errorMessage) => ProcessorResultError(errorMessage); + factory ProcessorResult.failed(String errorMessage) => ProcessorResultFailed(errorMessage); } class ProcessorResultSuccess implements ProcessorResult { @@ -14,12 +14,10 @@ class ProcessorResultSuccess implements ProcessorResult { } } -class ProcessorResultError implements ProcessorResult { - final String errorMessage; - const ProcessorResultError(this.errorMessage); +class ProcessorResultFailed implements ProcessorResult { + final String message; + const ProcessorResultFailed(this.message); @override - String toString() { - return 'ProcessorResultError(errorMessage: $errorMessage)'; - } + String toString() => '$runtimeType(message: $message)'; } diff --git a/lib/model/push_request.dart b/lib/model/push_request.dart index 677040d29..6c7fa0dee 100644 --- a/lib/model/push_request.dart +++ b/lib/model/push_request.dart @@ -65,7 +65,7 @@ class PushRequest { } @override - bool operator ==(Object other) => identical(this, other) || other is PushRequest && runtimeType == other.runtimeType && id == other.id; + bool operator ==(Object other) => other is PushRequest && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => Object.hash(runtimeType, id); diff --git a/lib/model/serializable_RSA_private_key.dart b/lib/model/serializable_RSA_private_key.dart deleted file mode 100644 index a83f338d1..000000000 --- a/lib/model/serializable_RSA_private_key.dart +++ /dev/null @@ -1,15 +0,0 @@ -// ignore_for_file: file_names - -import 'package:json_annotation/json_annotation.dart'; -import 'package:pointycastle/asymmetric/api.dart'; - -part 'serializable_RSA_private_key.g.dart'; - -@JsonSerializable() -class SerializableRSAPrivateKey extends RSAPrivateKey { - SerializableRSAPrivateKey(super.modulus, super.exponent, BigInt super.p, BigInt super.q); - - factory SerializableRSAPrivateKey.fromJson(Map json) => _$SerializableRSAPrivateKeyFromJson(json); - - Map toJson() => _$SerializableRSAPrivateKeyToJson(this); -} diff --git a/lib/model/serializable_RSA_private_key.g.dart b/lib/model/serializable_RSA_private_key.g.dart deleted file mode 100644 index 31ac2fb60..000000000 --- a/lib/model/serializable_RSA_private_key.g.dart +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'serializable_RSA_private_key.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SerializableRSAPrivateKey _$SerializableRSAPrivateKeyFromJson( - Map json) => - SerializableRSAPrivateKey( - BigInt.parse(json['modulus'] as String), - BigInt.parse(json['exponent'] as String), - BigInt.parse(json['p'] as String), - BigInt.parse(json['q'] as String), - ); - -Map _$SerializableRSAPrivateKeyToJson( - SerializableRSAPrivateKey instance) => - { - 'modulus': instance.modulus?.toString(), - 'exponent': instance.exponent?.toString(), - 'p': instance.p?.toString(), - 'q': instance.q?.toString(), - }; diff --git a/lib/model/serializable_RSA_public_key.dart b/lib/model/serializable_RSA_public_key.dart deleted file mode 100644 index 3096d6a6a..000000000 --- a/lib/model/serializable_RSA_public_key.dart +++ /dev/null @@ -1,15 +0,0 @@ -// ignore_for_file: file_names - -import 'package:json_annotation/json_annotation.dart'; -import 'package:pointycastle/asymmetric/api.dart'; - -part 'serializable_RSA_public_key.g.dart'; - -@JsonSerializable() -class SerializableRSAPublicKey extends RSAPublicKey { - SerializableRSAPublicKey(super.modulus, super.exponent); - - factory SerializableRSAPublicKey.fromJson(Map json) => _$SerializableRSAPublicKeyFromJson(json); - - Map toJson() => _$SerializableRSAPublicKeyToJson(this); -} diff --git a/lib/model/serializable_RSA_public_key.g.dart b/lib/model/serializable_RSA_public_key.g.dart deleted file mode 100644 index 6350252c0..000000000 --- a/lib/model/serializable_RSA_public_key.g.dart +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'serializable_RSA_public_key.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SerializableRSAPublicKey _$SerializableRSAPublicKeyFromJson( - Map json) => - SerializableRSAPublicKey( - BigInt.parse(json['modulus'] as String), - BigInt.parse(json['exponent'] as String), - ); - -Map _$SerializableRSAPublicKeyToJson( - SerializableRSAPublicKey instance) => - { - 'modulus': instance.modulus?.toString(), - 'exponent': instance.exponent?.toString(), - }; diff --git a/lib/model/tokens/push_token.dart b/lib/model/tokens/push_token.dart index a84b94d4f..3239f2191 100644 --- a/lib/model/tokens/push_token.dart +++ b/lib/model/tokens/push_token.dart @@ -159,6 +159,7 @@ class PushToken extends Token { 'isRolledOut: $isRolledOut, ' 'rolloutState: $rolloutState, ' 'publicServerKey: $publicServerKey, ' + 'privateTokenKey: $privateTokenKey, ' 'publicTokenKey: $publicTokenKey}'; } diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart index f6fddb5e8..c2ea1ff5c 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart @@ -7,8 +7,6 @@ import '../../../utils/token_import_origins.dart'; import 'otp_auth_processor.dart'; class FreeOtpPlusQrProcessor extends OtpAuthProcessor { - static const String _steamTokenIssuer = "Steam"; - static const String _steamTokenHost = "steam"; const FreeOtpPlusQrProcessor(); @override @@ -17,10 +15,6 @@ class FreeOtpPlusQrProcessor extends OtpAuthProcessor { Future>> _processOtpAuth(Uri uri) async { final results = >[]; - final issuer = _parseIssuer(uri); - if (issuer == _steamTokenIssuer) { - uri = uri.replace(host: _steamTokenHost); - } final result = await super.processUri(uri); results.addAll(result); @@ -31,28 +25,12 @@ class FreeOtpPlusQrProcessor extends OtpAuthProcessor { appName: TokenImportOrigins.freeOtpPlus.appName, token: t.resultData, isPrivacyIdeaToken: false, - data: t.resultData.origin!.data, + data: uri.toString(), ), ); }).toList(); } - /// Parse the label and the issuer (if it exists) from the url. - String _parseIssuer(Uri uri) { - String param = uri.path.substring(1); - param = Uri.decodeFull(param); - try { - if (param.contains(':')) { - List split = param.split(':'); - return split[0]; - } else { - return _parseIssuer(uri); - } - } catch (_) { - return ''; - } - } - @override Set get supportedSchemes => const OtpAuthProcessor().supportedSchemes; } diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart index 2e55a0a06..b0ee10ba6 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart @@ -104,7 +104,7 @@ class GoogleAuthenticatorQrProcessor extends TokenImportSchemeProcessor { error: e, stackTrace: StackTrace.current, ); - results.add(ProcessorResultError(e.toString())); + results.add(ProcessorResultFailed(e.toString())); continue; } } diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart index 182e7742e..e9c94bacb 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart @@ -25,15 +25,24 @@ class OtpAuthProcessor extends TokenImportSchemeProcessor { @override Future>> processUri(Uri uri, {bool fromInit = false}) async { - if (!supportedSchemes.contains(uri.scheme)) return [ProcessorResultError('The scheme [${uri.scheme}] not supported')]; + if (!supportedSchemes.contains(uri.scheme)) return [ProcessorResultFailed('The scheme [${uri.scheme}] not supported')]; Logger.info('Try to handle otpAuth:', name: 'token_notifier.dart#addTokenFromOtpAuth'); Map uriMap; try { uriMap = _parseOtpToken(uri); - } on LocalizedException catch (e) { - Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e); - final message = e.localizedMessage(AppLocalizations.of(await globalContext)!); - return [ProcessorResultError(message)]; + } catch (e, s) { + if (e is LocalizedException) { + Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e.unlocalizedMessage, stackTrace: s); + final message = e.localizedMessage(AppLocalizations.of(await globalContext)!); + return [ProcessorResult.failed(message)]; + } + String? message; + if (e is ArgumentError) { + Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e.message, stackTrace: s); + message = e.message; + } + message ??= 'An error occurred while parsing the QR code.'; + return [ProcessorResult.failed(globalContextSync != null ? AppLocalizations.of(globalContextSync!)?.tokenDataParseError ?? message : message)]; } if (_is2StepURI(uri)) { validateMap(uriMap, [URI_SECRET, URI_ITERATIONS, URI_OUTPUT_LENGTH_IN_BYTES, URI_SALT_LENGTH]); @@ -59,11 +68,11 @@ class OtpAuthProcessor extends TokenImportSchemeProcessor { newToken = Token.fromUriMap(uriMap); } on FormatException catch (e) { Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e); - return [ProcessorResultError(e.message)]; + return [ProcessorResultFailed(e.message)]; } catch (e, s) { Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e, stackTrace: s); showMessage(message: 'An error occurred while parsing the QR code.', duration: const Duration(seconds: 3)); - return [const ProcessorResultError('An error occurred while parsing the QR code.')]; + return [const ProcessorResultFailed('An error occurred while parsing the QR code.')]; } return [ProcessorResultSuccess(newToken)]; } @@ -87,6 +96,7 @@ Map _parseOtpToken(Uri uri) { ); } +const String _steamTokenIssuer = "Steam"; Map _parseOtpAuth(Uri uri) { // otpauth://TYPE/LABEL?PARAMETERS Map uriMap = {}; @@ -110,6 +120,9 @@ Map _parseOtpAuth(Uri uri) { final (label, issuer) = _parseLabelAndIssuer(uri); uriMap[URI_LABEL] = label; uriMap[URI_ISSUER] = issuer; + if (issuer == _steamTokenIssuer) { + uriMap[URI_TYPE] = TokenTypes.STEAM.name; + } // parse pin from response 'True' if (uri.queryParameters['pin'] == 'True') { @@ -142,7 +155,7 @@ Map _parseOtpAuth(Uri uri) { // Parse secret. String? secretAsString = uri.queryParameters['secret']; - ArgumentError.checkNotNull(secretAsString); + ArgumentError.checkNotNull(secretAsString, 'secret'); // This is a fix for omitted padding in base32 encoded secrets. // @@ -166,20 +179,22 @@ Map _parseOtpAuth(Uri uri) { if (uriMap[URI_TYPE] == 'hotp') { // Parse counter. String? counterAsString = uri.queryParameters['counter']; + if (counterAsString == null) { + throw LocalizedArgumentError( + localizedMessage: (localizations, value, name) => localizations.missingRequiredParameter(name), + unlocalizedMessage: 'Value for parameter [counter] is required and is missing.', + invalidValue: counterAsString, + name: 'counter', + ); + } try { - if (counterAsString == null) { - throw ArgumentError.value( - uri, - 'uri', - 'Value for parameter [counter] is not optional and is missing.', - ); - } uriMap[URI_COUNTER] = int.parse(counterAsString); } on FormatException { - throw ArgumentError.value( - uri, - 'uri', - '[$counterAsString] is not a valid value for uri parameter [counter].', + throw LocalizedArgumentError( + localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter), + unlocalizedMessage: '[$counterAsString] is not a valid value for uri parameter [counter].', + invalidValue: counterAsString, + name: 'counter', ); } } @@ -205,28 +220,31 @@ Map _parseOtpAuth(Uri uri) { try { uriMap[URI_SALT_LENGTH] = int.parse(saltLengthAsString); } on FormatException { - throw ArgumentError.value( - uri, - 'uri', - '[$saltLengthAsString] is not a valid value for parameter [2step_salt].', + throw LocalizedArgumentError( + localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter), + unlocalizedMessage: '[$saltLengthAsString] is not a valid value for parameter [2step_salt].', + invalidValue: saltLengthAsString, + name: '2step_salt', ); } try { uriMap[URI_OUTPUT_LENGTH_IN_BYTES] = int.parse(outputLengthInByteAsString); } on FormatException { - throw ArgumentError.value( - uri, - 'uri', - '[$outputLengthInByteAsString] is not a valid value for parameter [2step_output].', + throw LocalizedArgumentError( + localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter), + unlocalizedMessage: '[$outputLengthInByteAsString] is not a valid value for parameter [2step_output].', + invalidValue: outputLengthInByteAsString, + name: '2step_output', ); } try { uriMap[URI_ITERATIONS] = int.parse(iterationsAsString); } on FormatException { - throw ArgumentError.value( - uri, - 'uri', - '[$iterationsAsString] is not a valid value for parameter [2step_difficulty].', + throw LocalizedArgumentError( + localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter), + unlocalizedMessage: '[$iterationsAsString] is not a valid value for parameter [2step_difficulty].', + invalidValue: iterationsAsString, + name: '2step_difficulty', ); } } @@ -252,7 +270,12 @@ Map _parsePiPushToken(Uri uri) { String? pushVersionAsString = uri.queryParameters['v']; if (pushVersionAsString == null) { - throw ArgumentError.value(uri, 'uri', 'Parameter [v] is not an optional parameter and is missing.'); + throw LocalizedArgumentError( + localizedMessage: (localizations, value, name) => localizations.missingRequiredParameter(name), + unlocalizedMessage: 'Parameter [v] is not an optional parameter and is missing.', + invalidValue: pushVersionAsString, + name: 'v', + ); } try { @@ -261,17 +284,19 @@ Map _parsePiPushToken(Uri uri) { Logger.info('Parsing push token with version: $pushVersion', name: 'parsing_utils.dart#parsePiAuth'); if (pushVersion > maxPushTokenVersion) { - throw ArgumentError.value( - 'Unsupported version: $pushVersion', - 'QrParser#_parsePiAuth', - 'The piauth version [$pushVersionAsString] is not supported by this version of the app.', + throw LocalizedArgumentError( + localizedMessage: (localizations, value, name) => localizations.unsupported(value, name), + unlocalizedMessage: 'The piauth version [$pushVersionAsString] is not supported by this version of the app.', + invalidValue: pushVersionAsString, + name: 'piauth version', ); } } on FormatException { - throw ArgumentError.value( - 'Invalid version: $pushVersionAsString', - 'QrParser#_parsePiAuth', - '[$pushVersionAsString] is not a valid value for parameter [v].', + throw LocalizedArgumentError( + localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter), + unlocalizedMessage: '[$pushVersionAsString] is not a valid value for parameter [v].', + invalidValue: pushVersionAsString, + name: 'v', ); } @@ -285,19 +310,29 @@ Map _parsePiPushToken(Uri uri) { uriMap[URI_SERIAL] = uri.queryParameters['serial']; ArgumentError.checkNotNull(uriMap[URI_SERIAL], 'serial'); - String? url = uri.queryParameters['url']; + final String? url = uri.queryParameters['url']; ArgumentError.checkNotNull(url, 'url'); try { uriMap[URI_ROLLOUT_URL] = Uri.parse(url!); } on FormatException { - throw ArgumentError.value(uri, 'uri', '[$url] is not a valid Uri.'); + throw LocalizedArgumentError( + localizedMessage: (localizations, value, name) => localizations.invalidValueForParameter(value, name), + unlocalizedMessage: '[$url] is not a valid Uri.', + invalidValue: url!, + name: 'url', + ); } String ttlAsString = uri.queryParameters['ttl'] ?? '10'; try { uriMap[URI_TTL] = int.parse(ttlAsString); } on FormatException { - throw ArgumentError.value('Invalid ttl: $ttlAsString', 'QrParser#_parsePiAuth', '[$ttlAsString] is not a valid value for parameter [ttl].'); + throw LocalizedArgumentError( + localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter), + unlocalizedMessage: '[$ttlAsString] is not a valid value for parameter [ttl].', + invalidValue: ttlAsString, + name: 'ttl', + ); } uriMap[URI_ENROLLMENT_CREDENTIAL] = uri.queryParameters['enrollment_credential']; diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart index 56e564f46..e2c13b161 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart @@ -27,7 +27,7 @@ class PrivacyIDEAAuthenticatorQrProcessor extends TokenImportSchemeProcessor { return [ProcessorResult.success(token)]; } catch (e) { - return [ProcessorResult.error('Invalid URI')]; + return [ProcessorResult.failed('Invalid URI')]; } } } diff --git a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart index 99bc66929..003a9b0fd 100644 --- a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart @@ -145,10 +145,10 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { }; results.add(ProcessorResultSuccess(Token.fromUriMap(entryUriMap))); } on LocalizedException catch (e) { - results.add(ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!))); + results.add(ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!))); } catch (e) { Logger.error('Failed to parse token.', name: 'AegisImportFileProcessor#_processPlain', error: e, stackTrace: StackTrace.current); - results.add(ProcessorResultError(e.toString())); + results.add(ProcessorResultFailed(e.toString())); } } return results; diff --git a/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart index be4c40ba6..bfce3aa69 100644 --- a/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart @@ -220,10 +220,10 @@ class AuthenticatorProImportFileProcessor extends TokenImportFileProcessor { final newResults = await const OtpAuthProcessor().processUri(uri); results.addAll(newResults); } on LocalizedException catch (e) { - results.add(ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!))); + results.add(ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!))); } catch (e) { Logger.error('Failed to parse token.', name: 'authenticator_pro_import_file_processor#_processUriList', error: e, stackTrace: StackTrace.current); - results.add(ProcessorResultError(e.toString())); + results.add(ProcessorResultFailed(e.toString())); } } return results; @@ -255,10 +255,10 @@ class AuthenticatorProImportFileProcessor extends TokenImportFileProcessor { } } } on LocalizedException catch (e) { - results.add(ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!))); + results.add(ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!))); } catch (e) { Logger.error('Failed to parse token.', name: 'authenticator_pro_import_file_processor#_processHtml', error: e, stackTrace: StackTrace.current); - results.add(ProcessorResultError(e.toString())); + results.add(ProcessorResultFailed(e.toString())); } return results; } @@ -293,10 +293,10 @@ class AuthenticatorProImportFileProcessor extends TokenImportFileProcessor { final token = Token.fromUriMap(uriMap); result.add(ProcessorResultSuccess(token)); } on LocalizedException catch (e) { - result.add(ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!))); + result.add(ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!))); } catch (e) { Logger.error('Failed to parse token.', name: 'authenticator_pro_import_file_processor#_processAuthPro', error: e, stackTrace: StackTrace.current); - result.add(ProcessorResultError(e.toString())); + result.add(ProcessorResultFailed(e.toString())); } } return result; diff --git a/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart b/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart index f9f28d70a..8c3d277cb 100644 --- a/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart +++ b/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart @@ -73,7 +73,7 @@ class FreeOtpPlusFileProcessor extends TokenImportFileProcessor { results.addAll(await const FreeOtpPlusQrProcessor().processUri(uri)); } catch (e) { Logger.error('Failed to process line: $line', name: 'FreeOtpPlusFileProcessor#processFile', error: e, stackTrace: StackTrace.current); - results.add(ProcessorResultError(e.toString())); + results.add(ProcessorResultFailed(e.toString())); } } return results.map((t) { @@ -103,10 +103,10 @@ class FreeOtpPlusFileProcessor extends TokenImportFileProcessor { try { return ProcessorResultSuccess(Token.fromUriMap(_jsonToUriMap(tokenJson))); } on LocalizedException catch (e) { - return ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!)); + return ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!)); } catch (e) { Logger.error('Failed to parse token.', name: 'FreeOtpPlusFileProcessor#_processJsonToken', error: e, stackTrace: StackTrace.current); - return ProcessorResultError(e.toString()); + return ProcessorResultFailed(e.toString()); } } diff --git a/lib/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor.dart b/lib/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor.dart index 7bec5d8b2..0e04f5b84 100644 --- a/lib/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor.dart @@ -44,7 +44,7 @@ class PrivacyIDEAAuthenticatorImportFileProcessor extends TokenImportFileProcess return results; } catch (e) { Logger.error('Failed to process file', name: 'PrivacyIDEAAuthenticatorImportFileProcessor#processFile', error: e, stackTrace: StackTrace.current); - return [ProcessorResultError(e.toString())]; + return [ProcessorResultFailed(e.toString())]; } } } diff --git a/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart b/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart index bf8cd425a..0f2a863b2 100644 --- a/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart @@ -126,10 +126,10 @@ class TwoFasFileImportProcessor extends TokenImportFileProcessor { try { results.add(ProcessorResultSuccess(Token.fromUriMap(_twoFasToUriMap(twoFasToken)))); } on LocalizedException catch (e) { - results.add(ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!))); + results.add(ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!))); } catch (e) { Logger.error('Failed to parse token.', name: 'two_fas_import_file_processor.dart#_processPlainTokens', error: e, stackTrace: StackTrace.current); - results.add(ProcessorResultError(e.toString())); + results.add(ProcessorResultFailed(e.toString())); } } Logger.info('successfully imported ${results.length} tokens', name: 'two_fas_import_file_processor.dart#processPlainTokens'); diff --git a/lib/utils/globals.dart b/lib/utils/globals.dart index 165a807e2..274ed708e 100644 --- a/lib/utils/globals.dart +++ b/lib/utils/globals.dart @@ -50,7 +50,14 @@ Map>> getLocalizedPatchNotes(AppLocaliz final globalSnackbarKey = GlobalKey(); final globalNavigatorKey = GlobalKey(); final Future> contextedGlobalNavigatorKey = Future(() async => await _getContextedGlobalNavigatorKey()); -BuildContext? globalContextSync = globalNavigatorKey.currentContext; +BuildContext? get globalContextSync { + try { + return globalNavigatorKey.currentContext; + } catch (e) { + return null; + } +} + final Future globalContext = Future(() async => await _getContextedGlobalNavigatorKey()).then((value) => value.currentContext!); Future> _getContextedGlobalNavigatorKey() async { if (globalNavigatorKey.currentContext != null) { diff --git a/lib/utils/image_converter.dart b/lib/utils/image_converter.dart index 2639ea5c5..ecadd41c6 100644 --- a/lib/utils/image_converter.dart +++ b/lib/utils/image_converter.dart @@ -5,6 +5,8 @@ import 'dart:ui'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:image/image.dart' as imglib; +import 'package:image/image.dart'; +import 'package:privacyidea_authenticator/utils/logger.dart'; class ImageConverter { final imglib.Image image; @@ -27,6 +29,7 @@ class ImageConverter { } factory ImageConverter._fromNV21(CameraImage image) { + Logger.info('Converting NV21 image to Image'); final width = image.width.toInt(); final height = image.height.toInt(); Uint8List yuv420sp = image.planes[0].bytes; @@ -65,10 +68,12 @@ class ImageConverter { } factory ImageConverter._fromJPEG(CameraImage image) { + Logger.info('Converting JPEG image to Image'); return ImageConverter(image: imglib.decodeJpg(image.planes[0].bytes)!); } factory ImageConverter._fromBGRA8888(CameraImage image, int rotation, bool mirror, int cropLeft, int cropRight, int cropTop, int cropBottom) { + Logger.info('Converting BGRA8888 image to Image'); rotation = 360 - (rotation % 360); // if the image is rotated by 90, we need to rotate by another 270 to get the correct rotation (0/360) const numChannels = 4; // 1 for alpha, 3 for RGB var img = imglib.Image.fromBytes( @@ -78,6 +83,7 @@ class ImageConverter { numChannels: numChannels, bytesOffset: numChannels * 7, // i don't know why 7 pixels, but it works bytes: (image.planes[0].bytes).buffer, + order: ChannelOrder.bgra, ); img = imglib.copyRotate(img, angle: rotation); if (mirror) { @@ -102,6 +108,7 @@ class ImageConverter { int chropTop = 0, int chropBottom = 0, ]) { + Logger.info('Converting YUV420 image to Image'); rotation = 360 - (rotation % 360); // if the rotation is 90, we need to rotate by 270 to get the correct rotation const alpha = 0xFF; diff --git a/lib/utils/push_provider.dart b/lib/utils/push_provider.dart index f2782cc69..c863cbd3c 100644 --- a/lib/utils/push_provider.dart +++ b/lib/utils/push_provider.dart @@ -20,6 +20,7 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; @@ -204,6 +205,8 @@ class PushProvider { Logger.warning('No token found for serial ${pushRequest.serial}.', name: 'push_provider.dart#_handleIncomingRequestForeground'); return; } + log('token:' + jsonEncode(pushToken.toJson())); + log('pushRequest:' + jsonEncode(pushRequest.toJson())); if (!await pushRequest.verifySignature(pushToken, rsaUtils: _rsaUtils, legacyUtils: _legacyUtils)) { Logger.warning('Signature verification failed.', name: 'push_provider.dart#_handleIncomingRequestForeground'); return; diff --git a/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart b/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart index ae626f1bb..33f3ed41d 100644 --- a/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart +++ b/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart @@ -25,7 +25,7 @@ class ImportPlainTokensPage extends ConsumerStatefulWidget { required TokenImportType selectedType, }) { final importedTokens = processorResults.whereType>().map((e) => e.resultData).toList(); - final failedImports = processorResults.whereType().map((e) => e.errorMessage).toList(); + final failedImports = processorResults.whereType().map((e) => e.message).toList(); return ImportPlainTokensPage._( key: key, importedTokens: importedTokens, diff --git a/test/unit_test/model/extensions/sortable_list_test.dart b/test/unit_test/model/extensions/sortable_list_test.dart index cde128675..503e4e49f 100644 --- a/test/unit_test/model/extensions/sortable_list_test.dart +++ b/test/unit_test/model/extensions/sortable_list_test.dart @@ -190,10 +190,5 @@ void _testSortableList() { ]); }); }); -/* -List moveBetween({T? moveAfter, required T movedItem, T? moveBefore}) { -List moveAllBetween({T? moveAfter, required List movedItems, T? moveBefore}) { -List withCurrentSortIndexSet() { -*/ }); } diff --git a/test/unit_test/model/processor_result_test.dart b/test/unit_test/model/processor_result_test.dart index 49ae3ca5d..b510c8f99 100644 --- a/test/unit_test/model/processor_result_test.dart +++ b/test/unit_test/model/processor_result_test.dart @@ -13,9 +13,9 @@ void _testProcessorResult() { expect(result.resultData, 'data'); }); test('error', () { - const result = ProcessorResultError('error'); - expect(result, isA()); - expect(result.errorMessage, 'error'); + const result = ProcessorResultFailed('error'); + expect(result, isA()); + expect(result.message, 'error'); }); }); group('factories', () { @@ -25,9 +25,9 @@ void _testProcessorResult() { expect((result as ProcessorResultSuccess).resultData, 'data'); }); test('error', () { - final result = ProcessorResult.error('error'); - expect(result, isA()); - expect((result as ProcessorResultError).errorMessage, 'error'); + final result = ProcessorResult.failed('error'); + expect(result, isA()); + expect((result as ProcessorResultFailed).message, 'error'); }); }); }); diff --git a/test/unit_test/model/push_request_test.dart b/test/unit_test/model/push_request_test.dart index 2d774a1ff..950ea7a80 100644 --- a/test/unit_test/model/push_request_test.dart +++ b/test/unit_test/model/push_request_test.dart @@ -1,5 +1,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/push_request.dart'; +import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; + +import '../utils/identifiers_test.dart'; void main() { _testPushRequest(); @@ -68,6 +71,218 @@ void _testPushRequest() { expect(copy.signature, 'signature'); expect(copy.accepted, true); }); + test('fromMessageData', () { + // Arrange + final data = { + PUSH_REQUEST_TITLE: 'title', + PUSH_REQUEST_QUESTION: 'question', + PUSH_REQUEST_URL: 'https://example.com', + PUSH_REQUEST_NONCE: 'nonce', + PUSH_REQUEST_SSL_VERIFY: '1', + PUSH_REQUEST_SERIAL: 'serial', + PUSH_REQUEST_SIGNATURE: 'signature', + }; + // Act + final request = PushRequest.fromMessageData(data); + // Assert + expect(request.title, 'title'); + expect(request.question, 'question'); + expect(request.uri, Uri.parse('https://example.com')); + expect(request.nonce, 'nonce'); + expect(request.sslVerify, true); + expect(request.id, 'nonce'.hashCode); + expect(request.serial, 'serial'); + expect(request.signature, 'signature'); + }); + }); + group('serilization', () { + test('toJson', () { + // Arrange + final request = PushRequest( + title: 'title', + question: 'question', + uri: Uri.parse('https://example.com'), + nonce: 'nonce', + sslVerify: true, + id: 1, + expirationDate: DateTime.now(), + serial: 'serial', + signature: 'signature', + accepted: true, + ); + // Act + final json = request.toJson(); + // Assert + expect(json['title'], 'title'); + expect(json['question'], 'question'); + expect(json['uri'], 'https://example.com'); + expect(json['nonce'], 'nonce'); + expect(json['sslVerify'], true); + expect(json['id'], 1); + expect(json['expirationDate'], isA()); + expect(json['serial'], 'serial'); + expect(json['signature'], 'signature'); + expect(json['accepted'], true); + }); + + test('fromJson', () { + // Arrange + final json = { + 'title': 'title', + 'question': 'question', + 'uri': 'https://example.com', + 'nonce': 'nonce', + 'sslVerify': true, + 'id': 1, + 'expirationDate': DateTime.now().toIso8601String(), + 'serial': 'serial', + 'signature': 'signature', + 'accepted': true, + }; + // Act + final request = PushRequest.fromJson(json); + // Assert + expect(request.title, 'title'); + expect(request.question, 'question'); + expect(request.uri, Uri.parse('https://example.com')); + expect(request.nonce, 'nonce'); + expect(request.sslVerify, true); + expect(request.id, 1); + expect(request.expirationDate, isA()); + expect(request.serial, 'serial'); + expect(request.signature, 'signature'); + expect(request.accepted, true); + }); + }); + + test('verifyData', () { + // Arrange + final data = { + PUSH_REQUEST_TITLE: 'title', + PUSH_REQUEST_QUESTION: 'question', + PUSH_REQUEST_URL: 'https://example.com', + PUSH_REQUEST_NONCE: 'nonce', + PUSH_REQUEST_SSL_VERIFY: '1', + PUSH_REQUEST_SERIAL: 'serial', + PUSH_REQUEST_SIGNATURE: 'signature', + }; + // Assert + expect(() => PushRequest.verifyData(data), returnsNormally); + }); + + test('verifySignature true', () async { + // Arrange + final token = PushToken.fromJson(const { + "label": "PIPU00064CF0", + "issuer": "privacyIDEA", + "id": "94a40d5a-1dba-4985-95ce-5ce9bb36d32a", + "pin": false, + "isLocked": false, + "isHidden": false, + "tokenImage": "", + "folderId": null, + "sortIndex": 2, + "origin": { + "source": "qrScan", + "appName": "privacyIDEA Authenticator", + "data": + "otpauth://pipush/PIPU00064CF0?url=https%3A//192.168.56.103/ttype/push&ttl=10&issuer=privacyIDEA&enrollment_credential=9d3100908d3c76a948b6041c8338def8b15ec06a&v=1&serial=PIPU00064CF0&sslverify=0", + "isPrivacyIdeaToken": null, + "createdAt": "2024-04-11T15:19:53.567296", + "piServerVersion": null + }, + "type": "PIPUSH", + "expirationDate": "2024-04-11T15:29:53.562967", + "serial": "PIPU00064CF0", + "fbToken": + "ffhpC7m3R7GEsTimtuA7u1:APA91bF3KB7MD2HEuS3bEB2XrLApgD7XksB1tcaDvA8HReQyFLCF7rI2U8i57dVkfqIAEmBDPYqzszkKD3lGKk6ihEgyCJzk4NC2tCetN3li-p7sRRbnePm34xxK5Se72rl9CKtMZsKX", + "sslVerify": false, + "enrollmentCredentials": "9d3100908d3c76a948b6041c8338def8b15ec06a", + "url": "https://192.168.2.169/ttype/push", + "isRolledOut": true, + "rolloutState": "rolloutComplete", + "publicServerKey": + "MIICCgKCAgEA6u3K9x1poOy8upX7WjWldNdV883T+XQTxCIlqdFod3xA7uyA3tdnI+ahPB/ZSeTrCh4jzJjw9kSCN77I0c6TgyfeXHDQJu5nZ8eSbChnrLNGxaxf24LY7RukLEBdumeHbbuc3EozRCdTMDPEYnWH/ct3zMBuIBk22gIuxS979Htcc1SgmKBfiEOVG5D7/qTvg3/EttazoIfQUllY6vWLk3vdGurXj9CD9UVc5qhYI54dLSKnR0KXjXsQ1GwBivf1BFeR/NkaSsTGxDvucJdJDI9d7aWfNXQaYln6SyuSJN8FYEmfhldQyU2dgi7jfToLO5GZPdzJhWKaUG0HqCv0cYLC1fP72+KR8DVUU4yAU/npR9inwBPnqmCxE+pt6WZeLWIhk3B34WUkKsQ0MRxyds1aNP5AACQATDYmRRu05nYD/kLM9aw03dmxwOUGkIVlLkm5quucvWJ6GGfrkG0G+lq0RjG3Ra8tPda8P4wq7MZ6J1HCrtiXFeniShPZwH/jhiHzqjfHb6AEHVyv56Ycx941KXRv6LaH3LHHhiMapY0NbKdid0WFBqutFUvBVoaT0ma3gKMMbGw9zCi5PdVHk9WyhMF7IvmcTuAuFL1xc25dc7kYrp+jrKVlJdiTD9vEOODI3ZxVdbTlGI6HH3NejlQvVd0S30vNu0sMZ55k9TECAwEAAQ==", + "privateTokenKey": + "MIILKgIBAAKCAgEAitdUL+H0M+hIddD+2cXJ58sB77TkygmmiNft1o0gIx/1jlYk9tpy05TRuKzZT7A5qQLfwIb8qYoibvrn2qG3Vx1xM9X45NkyqnktReA8d/E7W9+WdN8x7hCLGROTI9y8cEUqJV/l4O4JcAM4CN9sn2KLBVxDkzfUyBTNNgU6v2rWonCJGSYuiWdVGwrcM6bxwHSMgIvZLdMSl7nPl0VHan+CtFarikuIN4J6YES00musVO4Ss6/nb4Z28umyGyGGXGv/nV5hABPipyDmLGxTChLn0lDYvTDhKgiQYwf+6bwnXmGUPPlUGHnlRQ0xnPt1BSHBtDnEVKkLXy8rJ1nMoBc7/tHZl7gMIPET4/WFi4/9unopLkObWZs6p+8mssC9HlbFNteMkkegc5uha5chxA/jxjRgbdEQNi9kr3Y46qJMZ+tSbioOXmCXYm9DdJGfjyAU78LJUGle1xEfiQDX6Egr6PvCyvhU2QW9aBJFKzDbEitp4cVLQ3Jz0X/yLrigLe+dVis3U3DcCe3l0dFcI6yAsVx1RG6DtgO9Wr2vzO/u8JUARYoVWroNGBg+Xr3Il0nloVsnJD7l+7vh/5JZmObWz0bTOHwZhqgagB+h/Y5ill9zqqMWIGSuuJx4tGN4iBITNM/wy8bzZc2LLzSNDsuWSBlcchF/2Ko5qz4uw3MCggIBAIeCxUN62Rt0ooyDcMpilr8qz/gC6a8cHjO/9P4MfgpqXgQPpkJdUeeaf9sssWlqycxg5ryFw8NB9Hu3XmZwon5fWSbLuAK8zwu9++cQhlRW2aXBI5tsYNJHyUuvZFSDVFSL3SffYoHYMtPmAGIAl4c06hVQSRA+VOr4CpaoIf/EKjsfdWBbeN5ZQaKERgoP7jL2TlAErvNf6Oy3yZqOXrbeP1b+pKz1Lb1oF0MwrAAYYXn/Z7eDf3LFngFXAKQkY3x/WFxjSm+F+RlALge7rozNiQAoMnLta0bYVvyhSDcfkpk2yjV88Ix3JMPxrbPXZg6dNO9P1oT+/P9pDUh6CBHU7CPBQIl+FkcEhVErqMXHkedfApjFNvejkEoyBdehojDTdP8t1Xk7T9IEoXvF1DB5H2TpiJlhe3IDSZ0k+S76OweJmxq3mjpfpo+W8ECML/Pl3eZPZ9qkwJlbEkC5EGfaJg00N6eqJQcBSkCwURhY3b2ynN0Jcs1ede/Xx+Jf6DTbop9S5bW9/E0epbNBWLFPIr3YAZ2t45sJ1tjj/FzdBYxDA4Dznu6k5fuxg/ps3PcoyJ7kmMDjc0WSDFrTVdY4DqJhy0fyJIlpaibXZU76z8H6rg0y7EPnOLw26aIPSw8qdKcUvIKVtls/pJXNNjsXmlZp5xVRDPSdxCLY80sRAoICAQCHgsVDetkbdKKMg3DKYpa/Ks/4AumvHB4zv/T+DH4Kal4ED6ZCXVHnmn/bLLFpasnMYOa8hcPDQfR7t15mcKJ+X1kmy7gCvM8LvfvnEIZUVtmlwSObbGDSR8lLr2RUg1RUi90n32KB2DLT5gBiAJeHNOoVUEkQPlTq+AqWqCH/xCo7H3VgW3jeWUGihEYKD+4y9k5QBK7zX+jst8majl623j9W/qSs9S29aBdDMKwAGGF5/2e3g39yxZ4BVwCkJGN8f1hcY0pvhfkZQC4Hu66MzYkAKDJy7WtG2Fb8oUg3H5KZNso1fPCMdyTD8a2z12YOnTTvT9aE/vz/aQ1IeggR1OwjwUCJfhZHBIVRK6jFx5HnXwKYxTb3o5BKMgXXoaIw03T/LdV5O0/SBKF7xdQweR9k6YiZYXtyA0mdJPku+jsHiZsat5o6X6aPlvBAjC/z5d3mT2fapMCZWxJAuRBn2iYNNDenqiUHAUpAsFEYWN29spzdCXLNXnXv18fiX+g026KfUuW1vfxNHqWzQVixTyK92AGdreObCdbY4/xc3QWMQwOA857upOX7sYP6bNz3KMie5JjA43NFkgxa01XWOA6iYctH8iSJaWom12VO+s/B+q4NMuxD5zi8NumiD0sPKnSnFLyClbZbP6SVzTY7F5pWaecVUQz0ncQi2PNLEQKCAQEA/G9PEH0qrMia7UvI4CRgW0KLMHFPKulzmD8DLJFuLbFDg/d+9v/jBVA13ZrFW1rAOEh8Oz7T2ujQwFw0RGkewYqaWsTI5jYB9ddEKfN3TPb2BHA1wxXSvlrR6T1Fp88aT9KFXDPlHur/q9qBCvkvP7n/1rd6Okx1wsy/zloJKrkyjTPNXjFC8zpGQurRo1t1tkcoOl7fLE2E8qC7I7XtNtZv3Wk0AISlWXDDw3UvRy+jV7ieclKKwaCxMHyAZJ0Op1xS1q/wG24ymafb8d0+h+4vd2iE7F0Kj7EQchwWBHhNnsnG1EtTVB8n0bP8U/T4Hc5D4q16jT71Zg23U5xEyQKCAQEAjM1Q8SBBCpu7N3f1jhoQav+Fty+tiLu1W+JGUbaUWBhDg5zsz47B6gJKCw/llDKqJ+pAgyUKJYU+3Z59UkaxkN+Se1fXxRJx4p3EOTfuI17RQFxADg9x6kkLovwXC9oG/r216kqoMT4j+YM56Vt4T2FgY1vRiifPhKHByol8bj8jnXRhZrg4Iz0ANIhMYZVEJABnjkjplLtPCpzayl6Y/J98y/herty0e0hhkouDDLO2IhlSZ3L56tFkSAQTdgNrquv7fmo8DJbe9emCYdIGIbHUEobSzO7rXu6F2yrGhn1xZfjnR58eJGwkL4aKdUFZEmHy1cvm3Ou+FkqlqkfQWwKCAQEA7clUKu4c0uGsvrbSpADgG1cVki5KKtv5nYJN1R+xL615MchjevwTt5+U/giau7FCvEHbdFt8aQtCCNFSEtcKt7l+KN6Rd/mL4y5B8Vp8GK3RlOC2Y+wctl8KuLCU+rvlxydBpFbmDzfCWvna8KFF1ru4uWPf6Sa5DySb0R+S3wHREp2naIDy1fcg1Ewp6b1vpqJkzIctpqfnAj5RyhPHPg7FFUXSTGKm9xd38JhkTqQbM7ie2IXUWwypnEjLEPu5IAGhrsXQYaZuV7t9Pdnw206Mu+hivdvu5Ogf272FJ/TC+T6M4tGJzwYCFlF68QMi7cCsxcwwUvjpZJarCEF9sQKCAQBex8b4ydF+pp48FJBDe+AZZrBIQ9v48wJ+O69CSjlJo+uuqO/wOBToxWm6UJUmUYShIdsTbNeLskpDPPD3dYcKErW0OcmRa30mIzV3nuK7BJSvUmn8DQGNyGYA7NlGrRmQWXwfnunhXAczataM83nlVZNgzuoaqfnTOmANSsdsHyyGTVVTpCaF8gY1Vpq0BZq88VjEOuihqgTnC/dryooJZALJ+wMhiogjhPHJiAhLgJ3WDl2eLZN2MkXjBHtlMaBEil3dFv4dK2Ii/3E5D/v4qpAreH5mXV4rpTyN8Bl7Zu3yyr5FRCMyOWmSZGrHy5l9+llQ+dUKWda3gsBKA9WJAoIBAQC+3ybL8bHxhM4FqXVxEBMxsavxsdEBHvUSqi6D8wSLpwnKhP+487ZaRw0Ez0hcbQjXzitbKGHpFKZccN+e1vVBsGra0uZHo8GV0AERTTOZs9pHNjYva8dJ4Kg+G1qT5xqM3c4kWa9w44ExgS8lVcKlXrestO30HLXRZOgf8wmaCy5ybvVqRlARgUcUPMltPdevOMfJnFXO6YmcqRV1ugCauCqPydp6TC1cy1uanVs3K6H/D2jB2JLTdLQUtrwugHVR1GMhyeiPpstdxmrWVfZNPfUF7WRq5KXloAiC7XHIl0frO+KM7D6lXxQYxSwS1lTnXLwuvQ9yQLZOeCcwkjD0", + "publicTokenKey": + "MIICCgKCAgEAitdUL+H0M+hIddD+2cXJ58sB77TkygmmiNft1o0gIx/1jlYk9tpy05TRuKzZT7A5qQLfwIb8qYoibvrn2qG3Vx1xM9X45NkyqnktReA8d/E7W9+WdN8x7hCLGROTI9y8cEUqJV/l4O4JcAM4CN9sn2KLBVxDkzfUyBTNNgU6v2rWonCJGSYuiWdVGwrcM6bxwHSMgIvZLdMSl7nPl0VHan+CtFarikuIN4J6YES00musVO4Ss6/nb4Z28umyGyGGXGv/nV5hABPipyDmLGxTChLn0lDYvTDhKgiQYwf+6bwnXmGUPPlUGHnlRQ0xnPt1BSHBtDnEVKkLXy8rJ1nMoBc7/tHZl7gMIPET4/WFi4/9unopLkObWZs6p+8mssC9HlbFNteMkkegc5uha5chxA/jxjRgbdEQNi9kr3Y46qJMZ+tSbioOXmCXYm9DdJGfjyAU78LJUGle1xEfiQDX6Egr6PvCyvhU2QW9aBJFKzDbEitp4cVLQ3Jz0X/yLrigLe+dVis3U3DcCe3l0dFcI6yAsVx1RG6DtgO9Wr2vzO/u8JUARYoVWroNGBg+Xr3Il0nloVsnJD7l+7vh/5JZmObWz0bTOHwZhqgagB+h/Y5ill9zqqMWIGSuuJx4tGN4iBITNM/wy8bzZc2LLzSNDsuWSBlcchF/2Ko5qz4uw3MCAwEAAQ==" + }); + + final request = PushRequest.fromJson({ + "title": "privacyIDEA", + "question": "Do you want to confirm the login?", + "id": 134382661, + "uri": "https://192.168.56.103/ttype/push", + "nonce": "DIHEUYEDNJ6AC5FSGM7T3OTHTD6T5NK4", + "sslVerify": false, + "expirationDate": "2024-04-11T15:52:00.136352", + "serial": "PIPU00064CF0", + "signature": + "LNIZZSTEFVECXOFBHT4ANPLJXXUJA2S7CQ6S52KMWGE22LUBMWMZSF6BSQHV3NAI2RTHVUVFAPYALQ3A4W3Z4H7S26QZPRSEVHT4EMX2JPJOWNH5A6SSXLLPQJAZ3MDMKKXYSJOU27KHHHL56YQNKHOJQZPH5TPLFC6NPMQ4IGZB6TX4MLA2PIHZGGHIJM2TOXE4NFWNLDR5YKQ6JH7WO4G24VCACK7KKQRTZYXZFMSYAMO4ERBAYYQDS7SL6Y7CDKA4MBKSR2BKGYUVTR5AZUNVNHFWO7KPF3Y2THIXOSSMQ7VHHDCUQN6NGV63A27V7IX4EP6JRIDMHNOVEAVEIPFHKK55QCBFX2Y6HO4EZBP2X3ZXI5NEI7FO3CJ2VIC4ZFXOT4HKYTZRGTENAMLTAP56XCTDSKPNEUSZZMO6UQCCWGTQ5QTST47OIML4BLZJQOESXJ3OVUUWCZHCS6V46OMIRAQDIGRHGS7KQY7ZY4MKRRW4RDW7J4IYQ3EZWP777IKZCFNMQ6WV2KDA6W7T6O7VJB5NQ7VFQ3JGYPR6STX52H2RIEFKMNLMNW7UFDPZWDVCRLHI7FHOROHSKOECEC3T3LP7GLBZHHTGX46DGCOETLLEF67HU62DZHAUCOUPWHF6TY7KKKTQ3XNMDF5H4TWO7C5JTL46QC4PYFFOUDEUULTY2DVJBCMIXXF63PZ4YGAYFU4BPW3LTMBTM3PT6YBNJ6EQFUBR6N3KYAAUENZQBK3J5VZS6UWEBFL33AW3AFVV6TMVZQU4UJOJMSPL7T46F2VRRS27TA4FFE4JA5O6AVNETRYA====", + "accepted": null + }); + + final success = await request.verifySignature(token); + // Assert + expect(success, true); + }); + test('verifySignature false', () async { + // Arrange + final token = PushToken.fromJson(const { + "label": "PIPU00064CF0", + "issuer": "privacyIDEA", + "id": "94a40d5a-1dba-4985-95ce-5ce9bb36d32a", + "pin": false, + "isLocked": false, + "isHidden": false, + "tokenImage": "", + "folderId": null, + "sortIndex": 2, + "origin": { + "source": "qrScan", + "appName": "privacyIDEA Authenticator", + "data": + "otpauth://pipush/PIPU00064CF0?url=https%3A//192.168.56.103/ttype/push&ttl=10&issuer=privacyIDEA&enrollment_credential=9d3100908d3c76a948b6041c8338def8b15ec06a&v=1&serial=PIPU00064CF0&sslverify=0", + "isPrivacyIdeaToken": null, + "createdAt": "2024-04-11T15:19:53.567296", + "piServerVersion": null + }, + "type": "PIPUSH", + "expirationDate": "2024-04-11T15:29:53.562967", + "serial": "FALSCHER SERIAL", + "fbToken": + "ffhpC7m3R7GEsTimtuA7u1:APA91bF3KB7MD2HEuS3bEB2XrLApgD7XksB1tcaDvA8HReQyFLCF7rI2U8i57dVkfqIAEmBDPYqzszkKD3lGKk6ihEgyCJzk4NC2tCetN3li-p7sRRbnePm34xxK5Se72rl9CKtMZsKX", + "sslVerify": false, + "enrollmentCredentials": "9d3100908d3c76a948b6041c8338def8b15ec06a", + "url": "https://192.168.2.169/ttype/push", + "isRolledOut": true, + "rolloutState": "rolloutComplete", + "publicServerKey": + "MIICCgKCAgEA6u3K9x1poOy8upX7WjWldNdV883T+XQTxCIlqdFod3xA7uyA3tdnI+ahPB/ZSeTrCh4jzJjw9kSCN77I0c6TgyfeXHDQJu5nZ8eSbChnrLNGxaxf24LY7RukLEBdumeHbbuc3EozRCdTMDPEYnWH/ct3zMBuIBk22gIuxS979Htcc1SgmKBfiEOVG5D7/qTvg3/EttazoIfQUllY6vWLk3vdGurXj9CD9UVc5qhYI54dLSKnR0KXjXsQ1GwBivf1BFeR/NkaSsTGxDvucJdJDI9d7aWfNXQaYln6SyuSJN8FYEmfhldQyU2dgi7jfToLO5GZPdzJhWKaUG0HqCv0cYLC1fP72+KR8DVUU4yAU/npR9inwBPnqmCxE+pt6WZeLWIhk3B34WUkKsQ0MRxyds1aNP5AACQATDYmRRu05nYD/kLM9aw03dmxwOUGkIVlLkm5quucvWJ6GGfrkG0G+lq0RjG3Ra8tPda8P4wq7MZ6J1HCrtiXFeniShPZwH/jhiHzqjfHb6AEHVyv56Ycx941KXRv6LaH3LHHhiMapY0NbKdid0WFBqutFUvBVoaT0ma3gKMMbGw9zCi5PdVHk9WyhMF7IvmcTuAuFL1xc25dc7kYrp+jrKVlJdiTD9vEOODI3ZxVdbTlGI6HH3NejlQvVd0S30vNu0sMZ55k9TECAwEAAQ==", + "privateTokenKey": + "MIILKgIBAAKCAgEAitdUL+H0M+hIddD+2cXJ58sB77TkygmmiNft1o0gIx/1jlYk9tpy05TRuKzZT7A5qQLfwIb8qYoibvrn2qG3Vx1xM9X45NkyqnktReA8d/E7W9+WdN8x7hCLGROTI9y8cEUqJV/l4O4JcAM4CN9sn2KLBVxDkzfUyBTNNgU6v2rWonCJGSYuiWdVGwrcM6bxwHSMgIvZLdMSl7nPl0VHan+CtFarikuIN4J6YES00musVO4Ss6/nb4Z28umyGyGGXGv/nV5hABPipyDmLGxTChLn0lDYvTDhKgiQYwf+6bwnXmGUPPlUGHnlRQ0xnPt1BSHBtDnEVKkLXy8rJ1nMoBc7/tHZl7gMIPET4/WFi4/9unopLkObWZs6p+8mssC9HlbFNteMkkegc5uha5chxA/jxjRgbdEQNi9kr3Y46qJMZ+tSbioOXmCXYm9DdJGfjyAU78LJUGle1xEfiQDX6Egr6PvCyvhU2QW9aBJFKzDbEitp4cVLQ3Jz0X/yLrigLe+dVis3U3DcCe3l0dFcI6yAsVx1RG6DtgO9Wr2vzO/u8JUARYoVWroNGBg+Xr3Il0nloVsnJD7l+7vh/5JZmObWz0bTOHwZhqgagB+h/Y5ill9zqqMWIGSuuJx4tGN4iBITNM/wy8bzZc2LLzSNDsuWSBlcchF/2Ko5qz4uw3MCggIBAIeCxUN62Rt0ooyDcMpilr8qz/gC6a8cHjO/9P4MfgpqXgQPpkJdUeeaf9sssWlqycxg5ryFw8NB9Hu3XmZwon5fWSbLuAK8zwu9++cQhlRW2aXBI5tsYNJHyUuvZFSDVFSL3SffYoHYMtPmAGIAl4c06hVQSRA+VOr4CpaoIf/EKjsfdWBbeN5ZQaKERgoP7jL2TlAErvNf6Oy3yZqOXrbeP1b+pKz1Lb1oF0MwrAAYYXn/Z7eDf3LFngFXAKQkY3x/WFxjSm+F+RlALge7rozNiQAoMnLta0bYVvyhSDcfkpk2yjV88Ix3JMPxrbPXZg6dNO9P1oT+/P9pDUh6CBHU7CPBQIl+FkcEhVErqMXHkedfApjFNvejkEoyBdehojDTdP8t1Xk7T9IEoXvF1DB5H2TpiJlhe3IDSZ0k+S76OweJmxq3mjpfpo+W8ECML/Pl3eZPZ9qkwJlbEkC5EGfaJg00N6eqJQcBSkCwURhY3b2ynN0Jcs1ede/Xx+Jf6DTbop9S5bW9/E0epbNBWLFPIr3YAZ2t45sJ1tjj/FzdBYxDA4Dznu6k5fuxg/ps3PcoyJ7kmMDjc0WSDFrTVdY4DqJhy0fyJIlpaibXZU76z8H6rg0y7EPnOLw26aIPSw8qdKcUvIKVtls/pJXNNjsXmlZp5xVRDPSdxCLY80sRAoICAQCHgsVDetkbdKKMg3DKYpa/Ks/4AumvHB4zv/T+DH4Kal4ED6ZCXVHnmn/bLLFpasnMYOa8hcPDQfR7t15mcKJ+X1kmy7gCvM8LvfvnEIZUVtmlwSObbGDSR8lLr2RUg1RUi90n32KB2DLT5gBiAJeHNOoVUEkQPlTq+AqWqCH/xCo7H3VgW3jeWUGihEYKD+4y9k5QBK7zX+jst8majl623j9W/qSs9S29aBdDMKwAGGF5/2e3g39yxZ4BVwCkJGN8f1hcY0pvhfkZQC4Hu66MzYkAKDJy7WtG2Fb8oUg3H5KZNso1fPCMdyTD8a2z12YOnTTvT9aE/vz/aQ1IeggR1OwjwUCJfhZHBIVRK6jFx5HnXwKYxTb3o5BKMgXXoaIw03T/LdV5O0/SBKF7xdQweR9k6YiZYXtyA0mdJPku+jsHiZsat5o6X6aPlvBAjC/z5d3mT2fapMCZWxJAuRBn2iYNNDenqiUHAUpAsFEYWN29spzdCXLNXnXv18fiX+g026KfUuW1vfxNHqWzQVixTyK92AGdreObCdbY4/xc3QWMQwOA857upOX7sYP6bNz3KMie5JjA43NFkgxa01XWOA6iYctH8iSJaWom12VO+s/B+q4NMuxD5zi8NumiD0sPKnSnFLyClbZbP6SVzTY7F5pWaecVUQz0ncQi2PNLEQKCAQEA/G9PEH0qrMia7UvI4CRgW0KLMHFPKulzmD8DLJFuLbFDg/d+9v/jBVA13ZrFW1rAOEh8Oz7T2ujQwFw0RGkewYqaWsTI5jYB9ddEKfN3TPb2BHA1wxXSvlrR6T1Fp88aT9KFXDPlHur/q9qBCvkvP7n/1rd6Okx1wsy/zloJKrkyjTPNXjFC8zpGQurRo1t1tkcoOl7fLE2E8qC7I7XtNtZv3Wk0AISlWXDDw3UvRy+jV7ieclKKwaCxMHyAZJ0Op1xS1q/wG24ymafb8d0+h+4vd2iE7F0Kj7EQchwWBHhNnsnG1EtTVB8n0bP8U/T4Hc5D4q16jT71Zg23U5xEyQKCAQEAjM1Q8SBBCpu7N3f1jhoQav+Fty+tiLu1W+JGUbaUWBhDg5zsz47B6gJKCw/llDKqJ+pAgyUKJYU+3Z59UkaxkN+Se1fXxRJx4p3EOTfuI17RQFxADg9x6kkLovwXC9oG/r216kqoMT4j+YM56Vt4T2FgY1vRiifPhKHByol8bj8jnXRhZrg4Iz0ANIhMYZVEJABnjkjplLtPCpzayl6Y/J98y/herty0e0hhkouDDLO2IhlSZ3L56tFkSAQTdgNrquv7fmo8DJbe9emCYdIGIbHUEobSzO7rXu6F2yrGhn1xZfjnR58eJGwkL4aKdUFZEmHy1cvm3Ou+FkqlqkfQWwKCAQEA7clUKu4c0uGsvrbSpADgG1cVki5KKtv5nYJN1R+xL615MchjevwTt5+U/giau7FCvEHbdFt8aQtCCNFSEtcKt7l+KN6Rd/mL4y5B8Vp8GK3RlOC2Y+wctl8KuLCU+rvlxydBpFbmDzfCWvna8KFF1ru4uWPf6Sa5DySb0R+S3wHREp2naIDy1fcg1Ewp6b1vpqJkzIctpqfnAj5RyhPHPg7FFUXSTGKm9xd38JhkTqQbM7ie2IXUWwypnEjLEPu5IAGhrsXQYaZuV7t9Pdnw206Mu+hivdvu5Ogf272FJ/TC+T6M4tGJzwYCFlF68QMi7cCsxcwwUvjpZJarCEF9sQKCAQBex8b4ydF+pp48FJBDe+AZZrBIQ9v48wJ+O69CSjlJo+uuqO/wOBToxWm6UJUmUYShIdsTbNeLskpDPPD3dYcKErW0OcmRa30mIzV3nuK7BJSvUmn8DQGNyGYA7NlGrRmQWXwfnunhXAczataM83nlVZNgzuoaqfnTOmANSsdsHyyGTVVTpCaF8gY1Vpq0BZq88VjEOuihqgTnC/dryooJZALJ+wMhiogjhPHJiAhLgJ3WDl2eLZN2MkXjBHtlMaBEil3dFv4dK2Ii/3E5D/v4qpAreH5mXV4rpTyN8Bl7Zu3yyr5FRCMyOWmSZGrHy5l9+llQ+dUKWda3gsBKA9WJAoIBAQC+3ybL8bHxhM4FqXVxEBMxsavxsdEBHvUSqi6D8wSLpwnKhP+487ZaRw0Ez0hcbQjXzitbKGHpFKZccN+e1vVBsGra0uZHo8GV0AERTTOZs9pHNjYva8dJ4Kg+G1qT5xqM3c4kWa9w44ExgS8lVcKlXrestO30HLXRZOgf8wmaCy5ybvVqRlARgUcUPMltPdevOMfJnFXO6YmcqRV1ugCauCqPydp6TC1cy1uanVs3K6H/D2jB2JLTdLQUtrwugHVR1GMhyeiPpstdxmrWVfZNPfUF7WRq5KXloAiC7XHIl0frO+KM7D6lXxQYxSwS1lTnXLwuvQ9yQLZOeCcwkjD0", + "publicTokenKey": + "MIICCgKCAgEAitdUL+H0M+hIddD+2cXJ58sB77TkygmmiNft1o0gIx/1jlYk9tpy05TRuKzZT7A5qQLfwIb8qYoibvrn2qG3Vx1xM9X45NkyqnktReA8d/E7W9+WdN8x7hCLGROTI9y8cEUqJV/l4O4JcAM4CN9sn2KLBVxDkzfUyBTNNgU6v2rWonCJGSYuiWdVGwrcM6bxwHSMgIvZLdMSl7nPl0VHan+CtFarikuIN4J6YES00musVO4Ss6/nb4Z28umyGyGGXGv/nV5hABPipyDmLGxTChLn0lDYvTDhKgiQYwf+6bwnXmGUPPlUGHnlRQ0xnPt1BSHBtDnEVKkLXy8rJ1nMoBc7/tHZl7gMIPET4/WFi4/9unopLkObWZs6p+8mssC9HlbFNteMkkegc5uha5chxA/jxjRgbdEQNi9kr3Y46qJMZ+tSbioOXmCXYm9DdJGfjyAU78LJUGle1xEfiQDX6Egr6PvCyvhU2QW9aBJFKzDbEitp4cVLQ3Jz0X/yLrigLe+dVis3U3DcCe3l0dFcI6yAsVx1RG6DtgO9Wr2vzO/u8JUARYoVWroNGBg+Xr3Il0nloVsnJD7l+7vh/5JZmObWz0bTOHwZhqgagB+h/Y5ill9zqqMWIGSuuJx4tGN4iBITNM/wy8bzZc2LLzSNDsuWSBlcchF/2Ko5qz4uw3MCAwEAAQ==" + }); + + final request = PushRequest.fromJson({ + "title": "privacyIDEA", + "question": "Do you want to confirm the login?", + "id": 134382661, + "uri": "https://192.168.56.103/ttype/push", + "nonce": "DIHEUYEDNJ6AC5FSGM7T3OTHTD6T5NK4", + "sslVerify": false, + "expirationDate": "2024-04-11T15:52:00.136352", + "serial": "FALSCHER SERIAL", + "signature": + "LNIZZSTEFVECXOFBHT4ANPLJXXUJA2S7CQ6S52KMWGE22LUBMWMZSF6BSQHV3NAI2RTHVUVFAPYALQ3A4W3Z4H7S26QZPRSEVHT4EMX2JPJOWNH5A6SSXLLPQJAZ3MDMKKXYSJOU27KHHHL56YQNKHOJQZPH5TPLFC6NPMQ4IGZB6TX4MLA2PIHZGGHIJM2TOXE4NFWNLDR5YKQ6JH7WO4G24VCACK7KKQRTZYXZFMSYAMO4ERBAYYQDS7SL6Y7CDKA4MBKSR2BKGYUVTR5AZUNVNHFWO7KPF3Y2THIXOSSMQ7VHHDCUQN6NGV63A27V7IX4EP6JRIDMHNOVEAVEIPFHKK55QCBFX2Y6HO4EZBP2X3ZXI5NEI7FO3CJ2VIC4ZFXOT4HKYTZRGTENAMLTAP56XCTDSKPNEUSZZMO6UQCCWGTQ5QTST47OIML4BLZJQOESXJ3OVUUWCZHCS6V46OMIRAQDIGRHGS7KQY7ZY4MKRRW4RDW7J4IYQ3EZWP777IKZCFNMQ6WV2KDA6W7T6O7VJB5NQ7VFQ3JGYPR6STX52H2RIEFKMNLMNW7UFDPZWDVCRLHI7FHOROHSKOECEC3T3LP7GLBZHHTGX46DGCOETLLEF67HU62DZHAUCOUPWHF6TY7KKKTQ3XNMDF5H4TWO7C5JTL46QC4PYFFOUDEUULTY2DVJBCMIXXF63PZ4YGAYFU4BPW3LTMBTM3PT6YBNJ6EQFUBR6N3KYAAUENZQBK3J5VZS6UWEBFL33AW3AFVV6TMVZQU4UJOJMSPL7T46F2VRRS27TA4FFE4JA5O6AVNETRYA====", + "accepted": null + }); + + final success = await request.verifySignature(token); + // Assert + expect(success, false); }); }); } diff --git a/test/unit_test/model/serializable_RSA_private_key_test.dart b/test/unit_test/model/serializable_RSA_private_key_test.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/unit_test/model/serializable_RSA_public_key_test.dart b/test/unit_test/model/serializable_RSA_public_key_test.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/unit_test/model/version_test.dart b/test/unit_test/model/version_test.dart index 8b1378917..4d699f853 100644 --- a/test/unit_test/model/version_test.dart +++ b/test/unit_test/model/version_test.dart @@ -1 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/version.dart'; +void main() { + _testTokenVersion(); +} + +void _testTokenVersion() { + group('Token Version Test', () { + group('serialzation', () { + test('toJson', () { + // Arrange + const version = Version(1, 2, 3); + // Act + final result = version.toJson(); + // Assert + expect(result, {'major': 1, 'minor': 2, 'patch': 3}); + }); + test('fromJson', () { + // Arrange + const json = {'major': 1, 'minor': 2, 'patch': 3}; + // Act + final result = Version.fromJson(json); + // Assert + expect(result, const Version(1, 2, 3)); + }); + }); + }); +} diff --git a/test/unit_test/processors/mixins/token_import_processor_test.dart b/test/unit_test/processors/mixins/token_import_processor_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/processors/mixins/token_import_processor_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor_test.dart b/test/unit_test/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface_test.dart b/test/unit_test/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart index 8b1378917..714a7045a 100644 --- a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart @@ -1 +1,52 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart'; +void main() { + _testFreeOtpPlusQrProcessor(); +} + +void _testFreeOtpPlusQrProcessor() { + group('Free Otp Plus Qr Processor', () { + FreeOtpPlusQrProcessor; + test('processUri', () async { + // Arrange + final normalOtpAuthUri = Uri.parse('otpauth://totp/FreeOTP+:alice?secret=secret&issuer=FreeOTP&algorithm=SHA1&digits=6&period=30'); + // Act + final results = await const FreeOtpPlusQrProcessor().processUri(normalOtpAuthUri); + // Assert + expect(results.length, equals(1)); + expect(results.first, isA>()); + final firstResult = results.first as ProcessorResultSuccess; + expect(firstResult.resultData, isA()); + expect(firstResult.resultData.issuer, equals('FreeOTP+')); + expect(firstResult.resultData.label, equals('alice')); + expect(firstResult.resultData, isA()); + expect(firstResult.resultData.origin!.appName, equals('FreeOTP+')); + }); + test('processUri without secret', () async { + // Arrange + final normalOtpAuthUri = Uri.parse('otpauth://totp/FreeOTP+:alice?issuer=FreeOTP&algorithm=SHA1&digits=6&period=30'); + // Act + final results = await const FreeOtpPlusQrProcessor().processUri(normalOtpAuthUri); + // Assert + expect(results.length, equals(1)); + expect(results.first, isA>()); + final firstResult = results.first as ProcessorResultFailed; + expect(firstResult.message.isNotEmpty, equals(true)); + }); + test('processUri without counter', () async { + // Arrange + final normalOtpAuthUri = Uri.parse('otpauth://hotp/FreeOTP+:alice?secret=secret&issuer=FreeOTP&algorithm=SHA1&digits=6'); + // Act + final results = await const FreeOtpPlusQrProcessor().processUri(normalOtpAuthUri); + // Assert + expect(results.length, equals(1)); + expect(results.first, isA>()); + final firstResult = results.first as ProcessorResultFailed; + expect(firstResult.message.isNotEmpty, equals(true)); + }); + }); +} From 00486d00818cca40402f64f635b758ef04c217fc Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:52:21 +0200 Subject: [PATCH 10/11] added some tests --- lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_cs.dart | 5 + lib/l10n/app_localizations_de.dart | 5 + lib/l10n/app_localizations_en.dart | 5 + lib/l10n/app_localizations_es.dart | 5 + lib/l10n/app_localizations_fr.dart | 5 + lib/l10n/app_localizations_nl.dart | 5 + lib/l10n/app_localizations_pl.dart | 5 + lib/model/processor_result.dart | 6 +- lib/model/token_import/token_origin_data.dart | 2 +- lib/model/tokens/day_password_token.dart | 35 +- .../otp_auth_processor.dart | 35 +- .../aegis_import_file_processor.dart | 7 + lib/utils/logger.dart | 33 +- lib/utils/view_utils.dart | 6 +- .../folder_widgets/token_folder_widget.dart | 15 +- .../token_widgets/token_widget_base.dart | 12 +- .../dialog_widgets/two_step_dialog.dart | 12 +- pubspec.lock | 120 ++--- .../home_widget_processor_test.dart | 1 - .../scheme_processor_interface_test.dart | 1 - ...oogle_authenticator_qr_processor_test.dart | 53 +++ .../otp_auth_processor_test.dart | 418 ++++++++++++++++++ ...yidea_authenticator_qr_processor_test.dart | 12 + ...mport_scheme_processor_interface_test.dart | 1 - .../aegis_import_file_processor_test.dart | 61 +++ 26 files changed, 747 insertions(+), 124 deletions(-) delete mode 100644 test/unit_test/processors/scheme_processors/home_widget_processor_test.dart delete mode 100644 test/unit_test/processors/scheme_processors/scheme_processor_interface_test.dart delete mode 100644 test/unit_test/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface_test.dart diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b4ef15da1..f608e8c83 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1662,6 +1662,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'[{value}] is not a valid value for uri parameter [parameter].'** String invalidValueForParameter(Object value, Object parameter); + + /// No description provided for @unsupported. + /// + /// In en, this message translates to: + /// **'The {name} [{value}] is not supported by this version of the app.'** + String unsupported(Object name, Object value); } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_cs.dart b/lib/l10n/app_localizations_cs.dart index e8e93e82d..f5ec56d65 100644 --- a/lib/l10n/app_localizations_cs.dart +++ b/lib/l10n/app_localizations_cs.dart @@ -870,4 +870,9 @@ class AppLocalizationsCs extends AppLocalizations { String invalidValueForParameter(Object value, Object parameter) { return '[$value] is not a valid value for uri parameter [parameter].'; } + + @override + String unsupported(Object name, Object value) { + return 'The $name [$value] is not supported by this version of the app.'; + } } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f2c4133e1..5109fb4d8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -870,4 +870,9 @@ class AppLocalizationsDe extends AppLocalizations { String invalidValueForParameter(Object value, Object parameter) { return '[$value] is not a valid value for uri parameter [parameter].'; } + + @override + String unsupported(Object name, Object value) { + return 'The $name [$value] is not supported by this version of the app.'; + } } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0aad88a5f..ca4f6163b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -870,4 +870,9 @@ class AppLocalizationsEn extends AppLocalizations { String invalidValueForParameter(Object value, Object parameter) { return '[$value] is not a valid value for uri parameter [parameter].'; } + + @override + String unsupported(Object name, Object value) { + return 'The $name [$value] is not supported by this version of the app.'; + } } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 194192b9a..046d5d766 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -870,4 +870,9 @@ class AppLocalizationsEs extends AppLocalizations { String invalidValueForParameter(Object value, Object parameter) { return '[$value] is not a valid value for uri parameter [parameter].'; } + + @override + String unsupported(Object name, Object value) { + return 'The $name [$value] is not supported by this version of the app.'; + } } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 9ab2c39ff..0b7c478da 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -870,4 +870,9 @@ class AppLocalizationsFr extends AppLocalizations { String invalidValueForParameter(Object value, Object parameter) { return '[$value] is not a valid value for uri parameter [parameter].'; } + + @override + String unsupported(Object name, Object value) { + return 'The $name [$value] is not supported by this version of the app.'; + } } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index b3db085ce..372e68eb5 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -870,4 +870,9 @@ class AppLocalizationsNl extends AppLocalizations { String invalidValueForParameter(Object value, Object parameter) { return '[$value] is not a valid value for uri parameter [parameter].'; } + + @override + String unsupported(Object name, Object value) { + return 'The $name [$value] is not supported by this version of the app.'; + } } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 34df8103e..e9d999ede 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -870,4 +870,9 @@ class AppLocalizationsPl extends AppLocalizations { String invalidValueForParameter(Object value, Object parameter) { return '[$value] is not a valid value for uri parameter [parameter].'; } + + @override + String unsupported(Object name, Object value) { + return 'The $name [$value] is not supported by this version of the app.'; + } } diff --git a/lib/model/processor_result.dart b/lib/model/processor_result.dart index 67a5495cd..ffe5ca295 100644 --- a/lib/model/processor_result.dart +++ b/lib/model/processor_result.dart @@ -2,9 +2,11 @@ abstract class ProcessorResult { const ProcessorResult(); factory ProcessorResult.success(T data) => ProcessorResultSuccess(data); factory ProcessorResult.failed(String errorMessage) => ProcessorResultFailed(errorMessage); + ProcessorResultSuccess? get asSuccess => this is ProcessorResultSuccess ? this as ProcessorResultSuccess : null; + ProcessorResultFailed? get asFailed => this is ProcessorResultFailed ? this as ProcessorResultFailed : null; } -class ProcessorResultSuccess implements ProcessorResult { +class ProcessorResultSuccess extends ProcessorResult { final T resultData; const ProcessorResultSuccess(this.resultData); @@ -14,7 +16,7 @@ class ProcessorResultSuccess implements ProcessorResult { } } -class ProcessorResultFailed implements ProcessorResult { +class ProcessorResultFailed extends ProcessorResult { final String message; const ProcessorResultFailed(this.message); diff --git a/lib/model/token_import/token_origin_data.dart b/lib/model/token_import/token_origin_data.dart index eeefa7d3c..280614fff 100644 --- a/lib/model/token_import/token_origin_data.dart +++ b/lib/model/token_import/token_origin_data.dart @@ -67,7 +67,7 @@ class TokenOriginData { // toString prints not data because it contains the secret @override - String toString() => 'TokenOrigin{source: $source, app: $appName, isPrivacyIdeaToken: $isPrivacyIdeaToken, createdAt: $createdAt}'; + String toString() => 'TokenOrigin{source: $source, app: $appName, isPrivacyIdeaToken: $isPrivacyIdeaToken, createdAt: $createdAt, data: $data'; factory TokenOriginData.fromJson(Map json) => _$TokenOriginDataFromJson(json); diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart index d39a573a8..8f0d36a9e 100644 --- a/lib/model/tokens/day_password_token.dart +++ b/lib/model/tokens/day_password_token.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:privacyidea_authenticator/model/extensions/enums/algorithms_extension.dart'; import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; +import 'package:privacyidea_authenticator/utils/errors.dart'; import 'package:uuid/uuid.dart'; import '../../utils/identifiers.dart'; @@ -120,10 +121,36 @@ class DayPasswordToken extends OTPToken { return DateTime.now().add(durationUntilNextOTP + const Duration(milliseconds: 1)); } + /// Throws an Error if the uriMap is invalid + static void validateUriMap(Map uriMap) { + if (uriMap[URI_SECRET] == null) { + throw LocalizedArgumentError( + localizedMessage: ((localizations, value, name) => localizations.secretIsRequired), + unlocalizedMessage: 'Secret is required', + invalidValue: uriMap[URI_SECRET], + name: URI_SECRET, + ); + } + if (uriMap[URI_PERIOD] < 1) { + throw LocalizedArgumentError( + localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter), + unlocalizedMessage: 'Period must be greater than 0', + invalidValue: uriMap[URI_PERIOD], + name: URI_PERIOD, + ); + } + if (uriMap[URI_DIGITS] < 1) { + throw LocalizedArgumentError( + localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter), + unlocalizedMessage: 'Digits must be greater than 0', + invalidValue: uriMap[URI_DIGITS], + name: URI_DIGITS, + ); + } + } + factory DayPasswordToken.fromUriMap(Map uriMap) { - if (uriMap[URI_SECRET] == null) throw ArgumentError('Secret is required'); - if (uriMap[URI_PERIOD] < 1) throw ArgumentError('Period must be greater than 0'); - if (uriMap[URI_DIGITS] < 1) throw ArgumentError('Digits must be greater than 0'); + validateUriMap(uriMap); return DayPasswordToken( label: uriMap[URI_LABEL] ?? '', @@ -132,7 +159,7 @@ class DayPasswordToken extends OTPToken { algorithm: Algorithms.values.byName(uriMap[URI_ALGORITHM] ?? 'SHA1'), digits: uriMap[URI_DIGITS] ?? 6, secret: Encodings.base32.encode(uriMap[URI_SECRET]), - period: Duration(seconds: uriMap[URI_PERIOD]), + period: Duration(seconds: uriMap[URI_PERIOD] ?? 86400), // default 24 hours tokenImage: uriMap[URI_IMAGE], pin: uriMap[URI_PIN], isLocked: uriMap[URI_PIN], diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart index e9c94bacb..0a53482af 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart @@ -39,7 +39,7 @@ class OtpAuthProcessor extends TokenImportSchemeProcessor { String? message; if (e is ArgumentError) { Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e.message, stackTrace: s); - message = e.message; + message = '${e.message} - ${e.name}: ${e.invalidValue}'; } message ??= 'An error occurred while parsing the QR code.'; return [ProcessorResult.failed(globalContextSync != null ? AppLocalizations.of(globalContextSync!)?.tokenDataParseError ?? message : message)]; @@ -48,18 +48,18 @@ class OtpAuthProcessor extends TokenImportSchemeProcessor { validateMap(uriMap, [URI_SECRET, URI_ITERATIONS, URI_OUTPUT_LENGTH_IN_BYTES, URI_SALT_LENGTH]); final secret = uriMap[URI_SECRET] as Uint8List; // Calculate the whole secret. - Uint8List? twoStepSecret; - while (twoStepSecret == null) { - twoStepSecret = (await showAsyncDialog( - barrierDismissible: false, - builder: (context) => GenerateTwoStepDialog( - iterations: uriMap[URI_ITERATIONS], - keyLength: uriMap[URI_OUTPUT_LENGTH_IN_BYTES], - saltLength: uriMap[URI_SALT_LENGTH], - password: secret, - ), - )); - await Future.delayed(const Duration(milliseconds: 500)); + + final twoStepSecret = (await showAsyncDialog( + barrierDismissible: false, + builder: (context) => GenerateTwoStepDialog( + iterations: uriMap[URI_ITERATIONS], + keyLength: uriMap[URI_OUTPUT_LENGTH_IN_BYTES], + saltLength: uriMap[URI_SALT_LENGTH], + password: secret, + ), + )); + if (twoStepSecret == null) { + return [const ProcessorResultFailed('The two step secret could not be generated, or was canceled.')]; } uriMap[URI_SECRET] = twoStepSecret; } @@ -142,10 +142,11 @@ Map _parseOtpAuth(Uri uri) { String digitsAsString = uri.queryParameters['digits'] ?? '6'; // Optional parameter if (digitsAsString != '6' && digitsAsString != '8') { - throw ArgumentError.value( - uri, - 'uri', - '[$digitsAsString] is not a valid number of digits', + throw LocalizedArgumentError( + localizedMessage: (localizations, value, name) => localizations.invalidValueForParameter(value, name), + unlocalizedMessage: '[$digitsAsString] is not a valid number of digits.', + invalidValue: digitsAsString, + name: 'digits', ); } diff --git a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart index 003a9b0fd..07c2eedf1 100644 --- a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart @@ -1,7 +1,9 @@ // ignore_for_file: constant_identifier_names import 'dart:convert'; +import 'dart:developer' as dev; import 'dart:isolate'; +import 'dart:math'; import 'dart:typed_data'; import 'package:cryptography/cryptography.dart' as crypto; @@ -75,6 +77,11 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { @override Future fileIsValid({required XFile file}) async { final Map json; + final bytes = await file.readAsBytes(); + for (var i = 0; i < bytes.length; i += 100) { + final sublist = bytes.sublist(i, min(i + 100, bytes.length)).toString(); + dev.log('bytes: $sublist'); + } try { final String fileContent = await file.readAsString(); json = jsonDecode(fileContent) as Map; diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index 0749d1f69..ea728348b 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -110,17 +110,20 @@ class Logger { /*----------- LOGGING METHODS -----------*/ - static void info(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) { - String infoString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.INFO); + void logInfo(String message, {String? name, bool verbose = false}) { + String infoString = _convertLogToSingleString(message, name: name, logLevel: LogLevel.INFO); infoString = _textFilter(infoString); - if (instance._verbose || verbose) { - instance._logToFile(infoString); + if (_verbose || verbose) { + _logToFile(infoString); } _print(infoString); } - static void warning(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) { - String warningString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.WARNING); + static void info(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) => + instance.logInfo(message, name: name, verbose: verbose); + + void logWarning(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) { + String warningString = _convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.WARNING); warningString = _textFilter(warningString); if (instance._verbose || verbose) { instance._logToFile(warningString); @@ -128,16 +131,19 @@ class Logger { _printWarning(warningString); } - static void error(String? message, {dynamic error, dynamic stackTrace, String? name}) { - String errorString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.ERROR); + static void warning(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) => + instance.logWarning(message, error: error, stackTrace: stackTrace, name: name, verbose: verbose); + + void logError(String? message, {dynamic error, dynamic stackTrace, String? name}) { + String errorString = _convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.ERROR); errorString = _textFilter(errorString); if (message != null) { - instance._lastError = message.substring(0, min(message.length, 100)); + _lastError = message.substring(0, min(message.length, 100)); } else if (error != null) { - instance._lastError = error.toString().substring(0, min(error.toString().length, 100)); + _lastError = error.toString().substring(0, min(error.toString().length, 100)); } - instance._logToFile(errorString); - instance._showSnackbar(); + _logToFile(errorString); + _showSnackbar(); StackTrace? stackTraceObject; if (stackTrace is StackTrace) { stackTraceObject = stackTrace; @@ -147,6 +153,9 @@ class Logger { _printError(message, error: error, stackTrace: stackTraceObject, name: name); } + static void error(String? message, {dynamic error, dynamic stackTrace, String? name}) => + instance.logError(message, error: error, stackTrace: stackTrace, name: name); + Future _logToFile(String fileMessage) async { if (_enableLoggingToFile == false) return; await _mutexWriteFile.acquire(); diff --git a/lib/utils/view_utils.dart b/lib/utils/view_utils.dart index ebf8bf78f..a3c5328f5 100644 --- a/lib/utils/view_utils.dart +++ b/lib/utils/view_utils.dart @@ -21,12 +21,12 @@ Future showAsyncDialog({ required WidgetBuilder builder, bool barrierDismissible = true, }) { - if (globalNavigatorKey.currentContext == null) { - Logger.warning('globalNavigatorKey.currentContext is null'); + if (globalContextSync == null) { + Logger.warning('globalContextSync is null'); return Future.value(null); } return showDialog( - context: globalNavigatorKey.currentContext!, + context: globalContextSync!, builder: builder, useRootNavigator: false, barrierDismissible: barrierDismissible, diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart index 4514fc15e..f86dafdf2 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart @@ -16,6 +16,7 @@ class TokenFolderWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final draggingSortable = ref.watch(draggingSortableProvider); + final draggingSortableNotifier = ref.read(draggingSortableProvider.notifier); final TokenFolder? draggingFolder = draggingSortable is TokenFolder ? draggingSortable : null; return draggingSortable == null ? LongPressDraggable( @@ -24,15 +25,13 @@ class TokenFolderWidget extends ConsumerWidget { final textSize = textSizeOf(folder.label, Theme.of(context).textTheme.titleLarge!); return Offset(max(textSize.width / 2, 30), textSize.height / 2 + 30); }, - onDragStarted: () { - ref.read(draggingSortableProvider.notifier).state = folder; - }, - onDragCompleted: () { - globalRef?.read(draggingSortableProvider.notifier).state = null; - }, - onDraggableCanceled: (velocity, offset) { - globalRef?.read(draggingSortableProvider.notifier).state = null; + onDragStarted: () => draggingSortableNotifier.state = folder, + onDragCompleted: () async { + await Future.delayed(const Duration(milliseconds: 50)); + // FIXME: The folder may appear before reordering the list. (race condition) This results in a flickering effect. Waiting here is a workaround so the list is updated before the folder visible again. We should find a better solution. + draggingSortableNotifier.state = null; }, + onDraggableCanceled: (velocity, offset) => draggingSortableNotifier.state = null, data: folder, childWhenDragging: const SizedBox(), feedback: Column( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart index ef5e706ac..0ab1ce383 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart @@ -48,15 +48,13 @@ class TokenWidgetBase extends ConsumerWidget { return draggingSortable == null ? LongPressDraggable( maxSimultaneousDrags: 1, - onDragStarted: () { - ref.read(draggingSortableProvider.notifier).state = token; - }, - onDragCompleted: () { - globalRef?.read(draggingSortableProvider.notifier).state = null; - }, - onDraggableCanceled: (velocity, offset) { + onDragStarted: () => ref.read(draggingSortableProvider.notifier).state = token, + onDragCompleted: () async { + await Future.delayed(const Duration(milliseconds: 50)); + // FIXME: The token may appear before reordering the list. (race condition) This results in a flickering effect. Waiting here is a workaround so the list is updated before the token visible again. We should find a better solution. globalRef?.read(draggingSortableProvider.notifier).state = null; }, + onDraggableCanceled: (velocity, offset) => globalRef?.read(draggingSortableProvider.notifier).state = null, dragAnchorStrategy: (Draggable d, BuildContext context, Offset point) { final textSize = textSizeOf(token.label, Theme.of(context).textTheme.titleLarge!); return Offset(max(textSize.width / 2, 30), textSize.height / 2 + 30); diff --git a/lib/widgets/dialog_widgets/two_step_dialog.dart b/lib/widgets/dialog_widgets/two_step_dialog.dart index 387fee3f8..d87b43d5e 100644 --- a/lib/widgets/dialog_widgets/two_step_dialog.dart +++ b/lib/widgets/dialog_widgets/two_step_dialog.dart @@ -62,12 +62,8 @@ class GenerateTwoStepDialog extends StatelessWidget { } // 3. Show phone part if this widget is still mounted. - Navigator.of(context).pop(generatedSecret); - showAsyncDialog( - barrierDismissible: false, - builder: (context) => TwoStepDialog( - phoneChecksum: phoneChecksum, - )); + if (context.mounted) Navigator.of(context).pop(generatedSecret); + showAsyncDialog(barrierDismissible: false, builder: (context) => TwoStepDialog(phoneChecksum: phoneChecksum)); } @override @@ -119,7 +115,9 @@ class _TwoStepDialogState extends State { ), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + if (context.mounted) Navigator.of(context).pop(); + }, child: Text( AppLocalizations.of(context)!.dismiss, overflow: TextOverflow.fade, diff --git a/pubspec.lock b/pubspec.lock index 73f9ac4b1..cfd6bd7a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "79b6452b4066fcbdd74c2aac354e80c591a727e0364bedccecdb5a5321784fa2" + sha256: "0cb43f83f36ba8cb20502dee0c205e3f3aafb751732d724aeac3f2e044212cc2" url: "https://pub.dev" source: hosted - version: "1.3.28" + version: "1.3.29" analyzer: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" asn1lib: dependency: "direct main" description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.2" camera: dependency: "direct main" description: @@ -157,18 +157,18 @@ packages: dependency: transitive description: name: camera_android - sha256: "1100e527b44a96906987a91ef78c8dacb539e34612a8058de89023380acf67f1" + sha256: "7b0aba6398afa8475e2bc9115d976efb49cf8db781e922572d443795c04a4f4f" url: "https://pub.dev" source: hosted - version: "0.10.8+18" + version: "0.10.9+1" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "8b113e43ee4434c9244c03c905432a0d5956cedaded3cd7381abaab89ce50297" + sha256: "5d009ae48de1c8ab621b1c4496dadb6e2a83f3223b76c6e6a4a252414105f561" url: "https://pub.dev" source: hosted - version: "0.9.14+1" + version: "0.9.15" camera_platform_interface: dependency: transitive description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: camera_web - sha256: f18ccfb33b2a7c49a52ad5aa3f07330b7422faaecbdfd9b9fe8e51182f6ad67d + sha256: "9e9aba2fbab77ce2472924196ff8ac4dd8f9126c4f9a3096171cd1d870d6b26c" url: "https://pub.dev" source: hosted - version: "0.3.2+4" + version: "0.3.3" characters: dependency: transitive description: @@ -237,10 +237,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: e9feae83b1849f61bad9f6f33ee00646e3410d54ce0821e02f262f9901dad3c9 + sha256: ebe15d94de9dd7c31dc2ac54e42780acdf3384b1497c69290c9f3c5b0279fc57 url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -293,10 +293,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: @@ -317,10 +317,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "50fb435ed30c6d2525cbfaaa0f46851ea6131315f213c0d921b0e407b34e3b84" + sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 url: "https://pub.dev" source: hosted - version: "10.0.1" + version: "10.1.0" device_info_plus_platform_interface: dependency: transitive description: @@ -397,10 +397,10 @@ packages: dependency: transitive description: name: file_selector_ios - sha256: b015154e6d9fddbc4d08916794df170b44531798c8dd709a026df162d07ad81d + sha256: "0a1196a9c5795858aa315332da2fb5c4bcfdcb312d8a4e27651f765b87904431" url: "https://pub.dev" source: hosted - version: "0.5.1+8" + version: "0.5.1+9" file_selector_linux: dependency: transitive description: @@ -445,10 +445,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "4b45655ec1b21a1783681f72f840a2e74d298046c2b7c286ab0e4f0efbf93d0a" + sha256: a864d1b6afd25497a3b57b016886d1763df52baaa69758a46723164de8d187fe url: "https://pub.dev" source: hosted - version: "2.28.0" + version: "2.29.0" firebase_core_platform_interface: dependency: transitive description: @@ -461,34 +461,34 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "28e30e00748497b9a70db2025942a42c5d752534eb678e9b9b98db056cf404ba" + sha256: c8b02226e548f35aace298e2bb2e6c24e34e8a203d614e742bb1146e5a4ad3c8 url: "https://pub.dev" source: hosted - version: "2.14.0" + version: "2.15.0" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "502233442839406198c34458a4ff71ca3350cc7be88ce06a8b729cbd2162ef57" + sha256: "87e3eda0ecdfeadb5fd1cf0dc5153aea5307a0cfca751c4b1ac97bfdd805660e" url: "https://pub.dev" source: hosted - version: "14.8.0" + version: "14.8.1" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "9438353a857c8000b0680d7ee246acb14fb854c4a14df4ebc7e1efde166903ac" + sha256: "80b4ccf20066b0579ebc88d4678230a5f53ab282fe040e31671af745db1588f9" url: "https://pub.dev" source: hosted - version: "4.5.30" + version: "4.5.31" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "6672c1c41e79d607b1ce0bbf1c6dcf97f7894b98bf65fe806e40d62a700bae3a" + sha256: "9224aa4db1ce6f08d96a82978453d37e9980204a20e410a11d9b774b24c6841c" url: "https://pub.dev" source: hosted - version: "3.8.0" + version: "3.8.1" fixnum: dependency: transitive description: @@ -501,10 +501,10 @@ packages: dependency: "direct main" description: name: fluentui_system_icons - sha256: cb71bafe3d760b0a6c49aabeaa8ae9534968b8a8e2e6d8e32f67090cd9bb53be + sha256: "1c860f10a0e74c5788ff8a650ae6074d9a544463ae269714f1044b32df52b978" url: "https://pub.dev" source: hosted - version: "1.1.232" + version: "1.1.234" flutter: dependency: "direct main" description: flutter @@ -527,10 +527,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1 + sha256: a701df4866f9a38bb8e4450a54c143bbeeb0ce2381e7df5a36e1006f3b43bb28 url: "https://pub.dev" source: hosted - version: "17.0.0" + version: "17.0.1" flutter_local_notifications_linux: dependency: transitive description: @@ -564,10 +564,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.19" flutter_riverpod: dependency: "direct main" description: @@ -816,10 +816,10 @@ packages: dependency: "direct main" description: name: local_auth_android - sha256: "3bcd732dda7c75fcb7ddaef12e131230f53dcc8c00790d0d6efb3aa0fbbeda57" + sha256: e0e5b1ea247c5a0951c13a7ee13dc1beae69750e6a2e1910d1ed6a3cd4d56943 url: "https://pub.dev" source: hosted - version: "1.0.37" + version: "1.0.38" local_auth_darwin: dependency: "direct main" description: @@ -984,18 +984,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" path_provider_foundation: dependency: transitive description: @@ -1111,10 +1111,10 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "3.8.0" pool: dependency: transitive description: @@ -1183,18 +1183,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_foundation: dependency: transitive description: @@ -1460,18 +1460,18 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.2.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_ios: dependency: transitive description: @@ -1508,10 +1508,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" url_launcher_windows: dependency: transitive description: @@ -1524,10 +1524,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vector_math: dependency: transitive description: @@ -1564,10 +1564,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" webdriver: dependency: transitive description: @@ -1596,10 +1596,10 @@ packages: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" xdg_directories: dependency: transitive description: diff --git a/test/unit_test/processors/scheme_processors/home_widget_processor_test.dart b/test/unit_test/processors/scheme_processors/home_widget_processor_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/processors/scheme_processors/home_widget_processor_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/processors/scheme_processors/scheme_processor_interface_test.dart b/test/unit_test/processors/scheme_processors/scheme_processor_interface_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/processors/scheme_processors/scheme_processor_interface_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart index 8b1378917..29eb52fdb 100644 --- a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart @@ -1 +1,54 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/token_import/token_origin_data.dart'; +import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart'; +import 'package:privacyidea_authenticator/utils/token_import_origins.dart'; +void main() { + _testGooleAuthenticatorQrProcessor(); +} + +void _testGooleAuthenticatorQrProcessor() { + group('Google Authenticator Qr Processor', () { + test('processUri', () async { + // Arrange + const processor = GoogleAuthenticatorQrProcessor(); + const uriString = + 'otpauth-migration://offline?data=ChkKCpklNznImSU3OcgSBVRlc3QxIAEoATACChsKCpklNznamSU3OdoSBVRlc3QyIAEoATABOAAQARgBIAAo8enF1vr%2F%2F%2F%2F%2FAQ%3D%3D'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(2)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.label, equals('Test1')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNotNull); + final tokenOriginData0Matcher = TokenOriginData( + source: TokenOriginSourceType.qrScanImport, + data: 'ChkKCpklNznImSU3OcgSBVRlc3QxIAEoATACChsKCpklNznamSU3OdoSBVRlc3QyIAEoATABOAAQARgBIAAo8enF1vr/////AQ==', + appName: TokenImportOrigins.googleAuthenticator.appName, + isPrivacyIdeaToken: false, + createdAt: token0.origin!.createdAt, + ); + expect(token0.origin, tokenOriginData0Matcher); + final result1 = results[1]; + expect(result1, isA()); + final token1 = result1.asSuccess!.resultData; + expect(token1.label, equals('Test2')); + expect(token1.type, equals('HOTP')); + expect(token1.origin, isNotNull); + final tokenOriginData1Matcher = TokenOriginData( + source: TokenOriginSourceType.qrScanImport, + data: 'ChkKCpklNznImSU3OcgSBVRlc3QxIAEoATACChsKCpklNznamSU3OdoSBVRlc3QyIAEoATABOAAQARgBIAAo8enF1vr/////AQ==', + appName: TokenImportOrigins.googleAuthenticator.appName, + isPrivacyIdeaToken: false, + createdAt: token1.origin!.createdAt, + ); + expect(token1.origin, tokenOriginData1Matcher); + }); + }); +} diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart index 8b1378917..128dc1d4f 100644 --- a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart @@ -1 +1,419 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/tokens/day_password_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart'; +void main() { + _testOtpAuthProcessor(); +} + +void _testOtpAuthProcessor() { + group('Otp Auth Processor Test', () { + group('TOTP', () { + test('processUri', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/account?secret=secret&issuer=issuer&algorithm=SHA256&digits=8&period=45'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNull); + final totpToken = token0 as TOTPToken; + expect(totpToken.origin, isNull); + expect(totpToken.period, equals(45)); + expect(totpToken.digits, equals(8)); + expect(totpToken.algorithm.name, equals('SHA256')); + }); + test('processUri missing algorithm', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/account?secret=secret&issuer=issuer&digits=6&period=30'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNull); + final totpToken = token0 as TOTPToken; + expect(totpToken.origin, isNull); + expect(totpToken.period, equals(30)); + expect(totpToken.digits, equals(6)); + expect(totpToken.algorithm.name, equals('SHA1')); + }); + test('processUri missing digits', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/account?secret=secret&issuer=issuer&algorithm=SHA1&period=30'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNull); + final totpToken = token0 as TOTPToken; + expect(totpToken.origin, isNull); + expect(totpToken.period, equals(30)); + expect(totpToken.digits, equals(6)); + expect(totpToken.algorithm.name, equals('SHA1')); + }); + test('processUri missing period', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/account?secret=secret&issuer=issuer&algorithm=SHA1&digits=6'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNull); + final totpToken = token0 as TOTPToken; + expect(totpToken.origin, isNull); + expect(totpToken.period, equals(30)); + expect(totpToken.digits, equals(6)); + expect(totpToken.algorithm.name, equals('SHA1')); + }); + test('processUri missing secret', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/account?issuer=issuer&algorithm=SHA1&digits=6&period=30'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final message = result0.asFailed!.message; + expect(message.toLowerCase().contains('secret'), isTrue); + }); + test('processUri issuer from path', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/issuer:account?secret=secret&issuer=issuer2&algorithm=SHA1&digits=6&period=30'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('TOTP')); + final totpToken = token0 as TOTPToken; + expect(totpToken.origin, isNull); + expect(totpToken.period, equals(30)); + expect(totpToken.digits, equals(6)); + expect(totpToken.algorithm.name, equals('SHA1')); + }); + group('2step', () {}); + }); + group('HOTP', () { + test('processUri', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/account?secret=secret&issuer=issuer&algorithm=SHA256&digits=8&counter=5'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('HOTP')); + expect(token0.origin, isNull); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.origin, isNull); + expect(hotpToken.counter, equals(5)); + expect(hotpToken.digits, equals(8)); + expect(hotpToken.algorithm.name, equals('SHA256')); + }); + test('processUri missing algorithm', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/account?secret=secret&issuer=issuer&digits=8&counter=5'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('HOTP')); + expect(token0.origin, isNull); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.origin, isNull); + expect(hotpToken.counter, equals(5)); + expect(hotpToken.digits, equals(8)); + expect(hotpToken.algorithm.name, equals('SHA1')); + }); + test('processUri missing digits', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/account?secret=secret&issuer=issuer&algorithm=SHA256&counter=5'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('HOTP')); + expect(token0.origin, isNull); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.origin, isNull); + expect(hotpToken.counter, equals(5)); + expect(hotpToken.digits, equals(6)); + expect(hotpToken.algorithm.name, equals('SHA256')); + }); + test('processUri missing counter', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/account?secret=secret&issuer=issuer&algorithm=SHA256&digits=8'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('HOTP')); + expect(token0.origin, isNull); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.origin, isNull); + expect(hotpToken.counter, equals(0)); + expect(hotpToken.digits, equals(8)); + expect(hotpToken.algorithm.name, equals('SHA256')); + }); + test('processUri missing secret', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/account?issuer=issuer&algorithm=SHA256&digits=8&counter=5'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final message = result0.asFailed!.message; + expect(message.toLowerCase().contains('secret'), isTrue); + }); + test('processUri issuer from path', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/issuer:account?secret=secret&algorithm=SHA256&digits=8&counter=5'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('HOTP')); + expect(token0.origin, isNull); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.origin, isNull); + expect(hotpToken.counter, equals(5)); + expect(hotpToken.digits, equals(8)); + expect(hotpToken.algorithm.name, equals('SHA256')); + }); + + test('2step', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = + 'otpauth://hotp/issuer:account?secret=secret&algorithm=SHA256&digits=8&counter=5&2step_salt=10&2step_output=20&2step_difficulty=10000'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); // FIXME: 2step secret is currently generated by the ui so it will fail in tests + }); + }); + group('DayPassword', () { + test('processUri', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://daypassword/account?secret=secret&issuer=issuer&algorithm=SHA256&period=86400&digits=8'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type.toLowerCase(), equals('daypassword')); + expect(token0.origin, isNull); + final dayPasswordToken = token0 as DayPasswordToken; + expect(dayPasswordToken.origin, isNull); + expect(dayPasswordToken.period, equals(const Duration(days: 1))); + expect(dayPasswordToken.digits, equals(8)); + expect(dayPasswordToken.algorithm.name, equals('SHA256')); + }); + + test('processUri missing algorithm', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://daypassword/account?secret=secret&issuer=issuer&period=86400&digits=8'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type.toLowerCase(), equals('daypassword')); + expect(token0.origin, isNull); + final dayPasswordToken = token0 as DayPasswordToken; + expect(dayPasswordToken.origin, isNull); + expect(dayPasswordToken.period, equals(const Duration(days: 1))); + expect(dayPasswordToken.digits, equals(8)); + expect(dayPasswordToken.algorithm.name, equals('SHA1')); + }); + + test('processUri missing digits', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://daypassword/account?secret=secret&issuer=issuer&algorithm=SHA256&period=172800'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type.toLowerCase(), equals('daypassword')); + expect(token0.origin, isNull); + final dayPasswordToken = token0 as DayPasswordToken; + expect(dayPasswordToken.origin, isNull); + expect(dayPasswordToken.period, equals(const Duration(days: 2))); + expect(dayPasswordToken.digits, equals(6)); + expect(dayPasswordToken.algorithm.name, equals('SHA256')); + }); + + test('processUri missing period', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://daypassword/account?secret=secret&issuer=issuer&algorithm=SHA256&digits=8'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type.toLowerCase(), equals('daypassword')); + expect(token0.origin, isNull); + final dayPasswordToken = token0 as DayPasswordToken; + expect(dayPasswordToken.origin, isNull); + expect(dayPasswordToken.period, equals(const Duration(days: 1))); + expect(dayPasswordToken.digits, equals(8)); + expect(dayPasswordToken.algorithm.name, equals('SHA256')); + }); + + test('processUri missing secret', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://daypassword/account?issuer=issuer&algorithm=SHA256&period=86400&digits=8'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final message = result0.asFailed!.message; + expect(message.toLowerCase().contains('secret'), isTrue); + }); + }); + group('Push Token', () { + test('processUri', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = + 'otpauth://pipush/PIPU0000D79E?url=https%3A//123.456.78.9/ttype/push&ttl=10&issuer=privacyIDEA&enrollment_credential=3342826741eb64e8f94e01920a88745bccdecd9e&v=1&serial=PIPU0000D79E&sslverify=0'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('privacyIDEA')); + expect(token0.label, equals('PIPU0000D79E')); + expect(token0.type.toLowerCase(), equals('pipush')); + expect(token0.origin, isNull); + final pushToken = token0 as PushToken; + expect(pushToken.origin, isNull); + expect(pushToken.url, equals(Uri.parse('https://123.456.78.9/ttype/push'))); + expect(pushToken.expirationDate, isNotNull); + // DateTimes.now() are never the same + // So we check the difference in minutes and allow a 5 second difference (9:59 => 9 minutes, 10:04 => 10 minutes) + expect(pushToken.expirationDate!.difference(DateTime.now().subtract(const Duration(seconds: 5))).inMinutes, equals(10)); + expect(pushToken.serial, equals('PIPU0000D79E')); + expect(pushToken.sslVerify, isFalse); + }); + }); + }); +} diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart index 8b1378917..2ed307def 100644 --- a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart @@ -1 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; +void main() { + _testPrivacyideaAuthenticatorQrProcessor(); +} + +void _testPrivacyideaAuthenticatorQrProcessor() { + group('Privacyidea Authenticator Qr Processor test', () { + test('', () { + // TODO: implement test when its sure that the functionallity will not change anymore + }); + }); +} diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface_test.dart deleted file mode 100644 index 8b1378917..000000000 --- a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart index 8b1378917..10d7760a6 100644 --- a/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart +++ b/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart @@ -1 +1,62 @@ +import 'dart:convert'; +import 'package:camera/camera.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/processors/token_import_file_processor/aegis_import_file_processor.dart'; + +void main() { + _testAegisImportFileProcessor(); +} + +void _testAegisImportFileProcessor() { + group('Aegis Import File Processor test', () { + group('version 3', () { + group('import json', () { + test('plain', () async { + // Arrange + const byteDataString = + '[123, 10, 32, 32, 32, 32, 34, 118, 101, 114, 115, 105, 111, 110, 34, 58, 32, 49, 44, 10, 32, 32, 32, 32, 34, 104, 101, 97, 100, 101, 114, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 108, 111, 116, 115, 34, 58, 32, 110, 117, 108, 108, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 112, 97, 114, 97, 109, 115, 34, 58, 32, 110, 117, 108, 108, 10, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 34, 100, 98, 34, 58, 32, 123,' + '10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 118, 101, 114, 115, 105, 111, 110, 34, 58, 32, 51, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 101, 110, 116, 114, 105, 101, 115, 34, 58, 32, 91, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 121, 112, 101, 34, 58, 32, 34, 116, 111, 116, 112, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 34, 117, 117, 105, 100, 34, 58, 32, 34, 99, 52, 57, 51, 102, 50, 52, 97, 45, 48, 54, 102, 55, 45, 52, 54, 57, 51, 45, 57, 100, 98, 102, 45, 53, 50, 53, 102, 56, 49, 54, 54, 102, 57, 100, 97, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 97, 109, 101, 34, 58, 32, 34, 84, 101, 115, 116, 49, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 34, 105, 115, 115, 117, 101, 114, 34, 58, 32, 34, 84, 101, 115, 116, 105, 110, 103, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 111, 116, 101, 34, 58, 32, 34, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 102, 97, 118, 111, 114, 105, 116, 101, 34, 58, 32, 102, 97, 108, 115, 101, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 34, 105, 99, 111, 110, 34, 58, 32, 110, 117, 108, 108, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 110, 102, 111, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 101, 99, 114, 101, 116, 34, 58, 32, 34, 65, 65, 65, 65, 65, 65, 65, 65, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 34, 97, 108, 103, 111, 34, 58, 32, 34, 83, 72, 65, 49, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 100, 105, 103, 105, 116, 115, 34, 58, 32, 54, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 112, 101, 114, 105, 111, 100, 34, 58, 32, 51, 48, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 103, 114, 111, 117, 112, 115, 34, 58, 32, 91, 93, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 121, 112, 101, 34, 58, 32, 34, 104, 111, 116, 112, 34, 44, 10, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 117, 117, 105, 100, 34, 58, 32, 34, 50, 48, 55, 102, 49, 49, 102, 48, 45, 54, 100, 101, 52, 45, 52, 97, 52, 99, 45, 57, 97, 98, 97, 45, 57, 55, 98, 50, 55, 55, 102, 101, 54, 48, 56, 97, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 97, 109, 101, 34, 58, 32, 34, 84, 101, 115, 116, 50, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 115, 115, 117, 101, 114, 34, 58, 32, 34, 84, 101, 115, 116, 105, 110, 103, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 111, 116, 101, 34, 58, 32, 34, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 102, 97, 118, 111, 114, 105, 116, 101, 34, 58, 32, 102, 97, 108, 115, 101, 44, 10, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 99, 111, 110, 34, 58, 32, 110, 117, 108, 108, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 110, 102, 111, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 101, 99, 114, 101, 116, 34, 58, 32, 34, 65, 65, 65, 65, 65, 65, 65, 65, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 97, 108, 103, 111, 34, 58, 32, 34, 83, 72, 65, 49, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 100, 105, 103, 105, 116, 115, 34, 58, 32, 54, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 99, 111, 117, 110, 116, 101, 114, 34, 58, 32, 48, 10, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 103, 114, 111, 117, 112, 115, 34, 58, 32, 91, 93, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 93, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 103, 114, 111, 117, 112, 115, 34, 58, 32, 91, 93, 10, 32, 32, 32, 32, 125, 10, 125]'; + final byteData = ByteData.view(Uint8List.fromList((jsonDecode(byteDataString) as List).cast()).buffer); + + const aegisImportFileProcessor = AegisImportFileProcessor(); + final XFile file = XFile.fromData(byteData.buffer.asUint8List(), name: 'aegis_plain.json'); + // Act + final isValid = await aegisImportFileProcessor.fileIsValid(file: file); + final results = await aegisImportFileProcessor.processFile(file: file); + // Assert + expect(isValid, isTrue); + expect(results.length, equals(2)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.label, equals('Test1')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNotNull); + final result1 = results[1]; + expect(result1, isA()); + final token1 = result1.asSuccess!.resultData; + expect(token1.label, equals('Test2')); + expect(token1.type, equals('HOTP')); + expect(token1.origin, isNotNull); + }); + }); + + group('import HTML', () {}); + + group('import TXT', () {}); + }); + }); +} From fa9d3848830c5969213fb52a970bf18afce5bbf8 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:02:56 +0200 Subject: [PATCH 11/11] added support for aegis import version 3 --- .../aegis_import_file_processor.dart | 82 ++++++++++++++++--- lib/utils/utils.dart | 11 ++- 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart index 07c2eedf1..f1cfc9977 100644 --- a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart @@ -1,9 +1,7 @@ // ignore_for_file: constant_identifier_names import 'dart:convert'; -import 'dart:developer' as dev; import 'dart:isolate'; -import 'dart:math'; import 'dart:typed_data'; import 'package:cryptography/cryptography.dart' as crypto; @@ -12,6 +10,7 @@ import 'package:file_selector/file_selector.dart'; import 'package:pointycastle/export.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/enums/token_types.dart'; import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; @@ -23,6 +22,7 @@ import '../../l10n/app_localizations.dart'; import '../../model/processor_result.dart'; import '../../utils/errors.dart'; import '../../utils/globals.dart'; +import '../../utils/utils.dart'; import 'token_import_file_processor_interface.dart'; import 'two_fas_import_file_processor.dart'; @@ -53,6 +53,7 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { static const String AEGIS_PERIOD = 'period'; static const String AEGIS_COUNTER = 'counter'; static const String AEGIS_PIN = 'pin'; + static const String AEGIS_ID = 'uuid'; bool _isValidPlain(Map json) { try { @@ -77,11 +78,6 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { @override Future fileIsValid({required XFile file}) async { final Map json; - final bytes = await file.readAsBytes(); - for (var i = 0; i < bytes.length; i += 100) { - final sublist = bytes.sublist(i, min(i + 100, bytes.length)).toString(); - dev.log('bytes: $sublist'); - } try { final String fileContent = await file.readAsString(); json = jsonDecode(fileContent) as Map; @@ -121,16 +117,34 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { } } - Future>> _processPlain(Map json) async { - final results = >[]; - if (json['db']['version'] != 2) { - throw Exception('Unsupported backup version: ${json['db']['version']}.'); + Future>> _processPlain(Map json) async => switch (json['db']['version'] as int) { + 2 => _processPlainV2(json), + 3 => _processPlainV3(json), + _ => _processPlainTryLatest(json), + }; + + Future>> _processPlainTryLatest(Map json) async { + try { + return await _processPlainV3(json); + } catch (_) { + throw LocalizedArgumentError( + localizedMessage: (localizations, value, name) => localizations.unsupported(name, value), + unlocalizedMessage: 'Unsupported backup version: ${json['db']['version']}.', + invalidValue: json['db']['version'], + name: 'aegis backup version', + ); } + } + + Future>> _processPlainV2(Map json) async { + final results = >[]; + final localization = AppLocalizations.of(await globalContext)!; for (Map entry in json['db']['entries']) { try { if (entry['type'] != 'totp' && entry['type'] != 'hotp') { // TODO: support other token types Logger.warning('Unsupported token type: ${entry['type']}', name: '_processPlain#OtpAuthImportFileProcessor'); + results.add(ProcessorResult.failed(localization.unsupported('token type', entry['type']))); continue; } Map info = entry['info']; @@ -150,14 +164,56 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { data: jsonEncode(entry), ), }; - results.add(ProcessorResultSuccess(Token.fromUriMap(entryUriMap))); + final token = Token.fromUriMap(entryUriMap); + results.add(ProcessorResult.success(token.copyWith(id: entry[AEGIS_ID]))); } on LocalizedException catch (e) { - results.add(ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!))); + results.add(ProcessorResult.failed(e.localizedMessage(localization))); + } catch (e) { + Logger.error('Failed to parse token.', name: 'AegisImportFileProcessor#_processPlain', error: e, stackTrace: StackTrace.current); + results.add(ProcessorResult.failed(e.toString())); + } + } + return results; + } + + Future>> _processPlainV3(Map json) async { + final results = >[]; + final localization = AppLocalizations.of(await globalContext)!; + final entries = json['db']['entries'] as List; + for (Map entry in entries) { + try { + if (doesThrow(() => TokenTypes.values.byName((entry['type'] as String).toUpperCase()))) { + // TODO: support other token types + Logger.warning('Unsupported token type: ${entry['type']}', name: '_processPlain#OtpAuthImportFileProcessor'); + results.add(ProcessorResult.failed(localization.unsupported('token type', entry['type']))); + continue; + } + Map info = entry['info']; + final entryUriMap = { + URI_TYPE: entry[AEGIS_TYPE], + URI_LABEL: entry[AEGIS_LABEL], + URI_ISSUER: entry[AEGIS_ISSUER], + URI_SECRET: Encodings.base32.decode(info[AEGIS_SECRET]), + URI_ALGORITHM: info[AEGIS_ALGORITHM], + URI_DIGITS: info[AEGIS_DIGITS], + URI_PERIOD: info[AEGIS_PERIOD], + URI_COUNTER: info[AEGIS_COUNTER], + URI_PIN: info[AEGIS_PIN], + URI_ORIGIN: TokenOriginSourceType.backupFile.toTokenOrigin( + appName: TokenImportOrigins.aegisAuthenticator.appName, + isPrivacyIdeaToken: false, + data: jsonEncode(entry), + ), + }; + results.add(ProcessorResult.success(Token.fromUriMap(entryUriMap))); + } on LocalizedException catch (e) { + results.add(ProcessorResultFailed(e.localizedMessage(localization))); } catch (e) { Logger.error('Failed to parse token.', name: 'AegisImportFileProcessor#_processPlain', error: e, stackTrace: StackTrace.current); results.add(ProcessorResultFailed(e.toString())); } } + return results; } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index dc8c8f4af..45cccf80b 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -108,6 +108,13 @@ Size textSizeOf(String text, TextStyle style, {int? maxLines = 1, double minWidt Future getPackageName() async => (await PackageInfo.fromPlatform()).packageName.replaceAll('.debug', ''); -String removeIllegalFilenameChars(String filename) { - return filename.replaceAll(RegExp(r'[<>:"/\\|?*]'), ''); +String removeIllegalFilenameChars(String filename) => filename.replaceAll(RegExp(r'[<>:"/\\|?*]'), ''); + +bool doesThrow(Function() f) { + try { + f(); + return false; + } catch (_) { + return true; + } }