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