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 @@
-
-
[]);
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 51d052658..88eb2bd81 100644
--- a/integration_test/copy_to_clipboard_test.dart
+++ b/integration_test/copy_to_clipboard_test.dart
@@ -14,7 +14,7 @@ import 'package:privacyidea_authenticator/state_notifiers/token_folder_notifier.
import 'package:privacyidea_authenticator/state_notifiers/token_notifier.dart';
import 'package:privacyidea_authenticator/utils/app_customizer.dart';
import 'package:privacyidea_authenticator/utils/riverpod_providers.dart';
-import 'package:privacyidea_authenticator/utils/version.dart';
+import 'package:privacyidea_authenticator/model/version.dart';
import '../test/tests_app_wrapper.dart';
import '../test/tests_app_wrapper.mocks.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 501ec69d7..17a438071 100644
--- a/integration_test/rename_and_delete_test.dart
+++ b/integration_test/rename_and_delete_test.dart
@@ -15,7 +15,7 @@ import 'package:privacyidea_authenticator/state_notifiers/token_folder_notifier.
import 'package:privacyidea_authenticator/state_notifiers/token_notifier.dart';
import 'package:privacyidea_authenticator/utils/app_customizer.dart';
import 'package:privacyidea_authenticator/utils/riverpod_providers.dart';
-import 'package:privacyidea_authenticator/utils/version.dart';
+import 'package:privacyidea_authenticator/model/version.dart';
import 'package:privacyidea_authenticator/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart';
import 'package:privacyidea_authenticator/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart';
import 'package:privacyidea_authenticator/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget.dart';
@@ -42,7 +42,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 fe04617a6..2b84ba31a 100644
--- a/integration_test/two_step_rollout_test.dart
+++ b/integration_test/two_step_rollout_test.dart
@@ -13,7 +13,7 @@ import 'package:privacyidea_authenticator/state_notifiers/token_notifier.dart';
import 'package:privacyidea_authenticator/utils/app_customizer.dart';
import 'package:privacyidea_authenticator/utils/logger.dart';
import 'package:privacyidea_authenticator/utils/riverpod_providers.dart';
-import 'package:privacyidea_authenticator/utils/version.dart';
+import 'package:privacyidea_authenticator/model/version.dart';
import 'package:privacyidea_authenticator/views/main_view/main_view.dart';
import 'package:privacyidea_authenticator/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart';
import 'package:privacyidea_authenticator/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.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 526b4572d..b3752ff03 100644
--- a/integration_test/views_test.dart
+++ b/integration_test/views_test.dart
@@ -15,7 +15,7 @@ import 'package:privacyidea_authenticator/state_notifiers/token_notifier.dart';
import 'package:privacyidea_authenticator/utils/app_customizer.dart';
import 'package:privacyidea_authenticator/utils/riverpod_providers.dart';
import 'package:privacyidea_authenticator/utils/rsa_utils.dart';
-import 'package:privacyidea_authenticator/utils/version.dart';
+import 'package:privacyidea_authenticator/model/version.dart';
import 'package:privacyidea_authenticator/views/settings_view/settings_view_widgets/settings_groups.dart';
import '../test/tests_app_wrapper.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/l10n/app_en.arb b/lib/l10n/app_en.arb
index b159cbea2..2937cd9e0 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -608,17 +608,6 @@
}
},
"thisAppIsOpenSource": "This Application is Open Source\nVisit us on GitHub",
- "invalidArgument": "{argument} is not a valid value for {type}",
- "@invalidArgument": {
- "placeholders": {
- "argument": {
- "example": "abc"
- },
- "type": {
- "example": "number"
- }
- }
- },
"importExportTokens": "Import/Export tokens",
"exportNonPrivacyIDEATokens": "Export non-privacyIDEA tokens",
"selectTokensToExport": "{count, plural, zero{} one{Select token to export} other{Select tokens to export}}",
@@ -636,5 +625,37 @@
"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",
+ "tokenDataParseError": "Token data could not be parsed",
+ "missingRequiredParameter": "Value for parameter [{counter}] is required and is missing",
+ "@missingRequiredParameter": {
+ "placeholders": {
+ "counter": {
+ "example": "counter"
+ }
+ }
+ },
+ "invalidValueForParameter": "[{value}] is not a valid value for uri parameter [parameter].",
+ "@invalidValueForParameter": {
+ "placeholders": {
+ "value": {
+ "example": "abc"
+ },
+ "parameter": {
+ "example": "number"
+ }
+ }
+ },
+ "unsupported": "The {name} [{value}] is not supported by this version of the app.",
+ "@unsupported": {
+ "placeholders": {
+ "name": {
+ "example": "piauth version"
+ },
+ "value": {
+ "example": "5"
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index add4d4838..f608e8c83 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -1531,12 +1531,6 @@ abstract class AppLocalizations {
/// **'This Application is Open Source\nVisit us on GitHub'**
String get thisAppIsOpenSource;
- /// No description provided for @invalidArgument.
- ///
- /// In en, this message translates to:
- /// **'{argument} is not a valid value for {type}'**
- String invalidArgument(Object argument, Object type);
-
/// No description provided for @importExportTokens.
///
/// In en, this message translates to:
@@ -1644,6 +1638,36 @@ 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;
+
+ /// No description provided for @tokenDataParseError.
+ ///
+ /// In en, this message translates to:
+ /// **'Token data could not be parsed'**
+ String get tokenDataParseError;
+
+ /// No description provided for @missingRequiredParameter.
+ ///
+ /// In en, this message translates to:
+ /// **'Value for parameter [{counter}] is required and is missing'**
+ String missingRequiredParameter(Object counter);
+
+ /// No description provided for @invalidValueForParameter.
+ ///
+ /// In en, this message translates to:
+ /// **'[{value}] is not a valid value for uri parameter [parameter].'**
+ String invalidValueForParameter(Object value, Object parameter);
+
+ /// No description provided for @unsupported.
+ ///
+ /// In en, this message translates to:
+ /// **'The {name} [{value}] is not supported by this version of the app.'**
+ String unsupported(Object name, Object value);
}
class _AppLocalizationsDelegate extends LocalizationsDelegate {
diff --git a/lib/l10n/app_localizations_cs.dart b/lib/l10n/app_localizations_cs.dart
index 98a0b2b47..f5ec56d65 100644
--- a/lib/l10n/app_localizations_cs.dart
+++ b/lib/l10n/app_localizations_cs.dart
@@ -792,11 +792,6 @@ class AppLocalizationsCs extends AppLocalizations {
@override
String get thisAppIsOpenSource => 'Tato aplikace má otevřený zdrojový kód\nNavštivte nás na GitHub';
- @override
- String invalidArgument(Object argument, Object type) {
- return '$argument není platná hodnota pro $type';
- }
-
@override
String get importExportTokens => 'Import/Exportovat žetony';
@@ -859,4 +854,25 @@ class AppLocalizationsCs extends AppLocalizations {
@override
String get confirmPassword => 'Potvrďte heslo';
+
+ @override
+ String get secretIsRequired => 'Secret is required';
+
+ @override
+ String get tokenDataParseError => 'Token data could not be parsed';
+
+ @override
+ String missingRequiredParameter(Object counter) {
+ return 'Value for parameter [$counter] is required and is missing';
+ }
+
+ @override
+ String invalidValueForParameter(Object value, Object parameter) {
+ return '[$value] is not a valid value for uri parameter [parameter].';
+ }
+
+ @override
+ String unsupported(Object name, Object value) {
+ return 'The $name [$value] is not supported by this version of the app.';
+ }
}
diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart
index 107a728e1..5109fb4d8 100644
--- a/lib/l10n/app_localizations_de.dart
+++ b/lib/l10n/app_localizations_de.dart
@@ -792,11 +792,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get thisAppIsOpenSource => 'Diese App ist Open Source\nBesuchen Sie uns auf GitHub';
- @override
- String invalidArgument(Object argument, Object type) {
- return '$argument ist kein gültiger Wert für $type';
- }
-
@override
String get importExportTokens => 'Token importieren/exportieren';
@@ -859,4 +854,25 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get confirmPassword => 'Passwort bestätigen';
+
+ @override
+ String get secretIsRequired => 'Secret is required';
+
+ @override
+ String get tokenDataParseError => 'Token data could not be parsed';
+
+ @override
+ String missingRequiredParameter(Object counter) {
+ return 'Value for parameter [$counter] is required and is missing';
+ }
+
+ @override
+ String invalidValueForParameter(Object value, Object parameter) {
+ return '[$value] is not a valid value for uri parameter [parameter].';
+ }
+
+ @override
+ String unsupported(Object name, Object value) {
+ return 'The $name [$value] is not supported by this version of the app.';
+ }
}
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index c821e1946..ca4f6163b 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -792,11 +792,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get thisAppIsOpenSource => 'This Application is Open Source\nVisit us on GitHub';
- @override
- String invalidArgument(Object argument, Object type) {
- return '$argument is not a valid value for $type';
- }
-
@override
String get importExportTokens => 'Import/Export tokens';
@@ -859,4 +854,25 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get confirmPassword => 'Confirm password';
+
+ @override
+ String get secretIsRequired => 'Secret is required';
+
+ @override
+ String get tokenDataParseError => 'Token data could not be parsed';
+
+ @override
+ String missingRequiredParameter(Object counter) {
+ return 'Value for parameter [$counter] is required and is missing';
+ }
+
+ @override
+ String invalidValueForParameter(Object value, Object parameter) {
+ return '[$value] is not a valid value for uri parameter [parameter].';
+ }
+
+ @override
+ String unsupported(Object name, Object value) {
+ return 'The $name [$value] is not supported by this version of the app.';
+ }
}
diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart
index fccb3e29d..046d5d766 100644
--- a/lib/l10n/app_localizations_es.dart
+++ b/lib/l10n/app_localizations_es.dart
@@ -792,11 +792,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get thisAppIsOpenSource => 'Esta aplicación es de código abierto\nVisítanos en GitHub';
- @override
- String invalidArgument(Object argument, Object type) {
- return '$argument no es un valor válido para $type';
- }
-
@override
String get importExportTokens => 'Importar/Exportar tokens';
@@ -859,4 +854,25 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get confirmPassword => 'Confirmar contraseña';
+
+ @override
+ String get secretIsRequired => 'Secret is required';
+
+ @override
+ String get tokenDataParseError => 'Token data could not be parsed';
+
+ @override
+ String missingRequiredParameter(Object counter) {
+ return 'Value for parameter [$counter] is required and is missing';
+ }
+
+ @override
+ String invalidValueForParameter(Object value, Object parameter) {
+ return '[$value] is not a valid value for uri parameter [parameter].';
+ }
+
+ @override
+ String unsupported(Object name, Object value) {
+ return 'The $name [$value] is not supported by this version of the app.';
+ }
}
diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart
index a84c16359..0b7c478da 100644
--- a/lib/l10n/app_localizations_fr.dart
+++ b/lib/l10n/app_localizations_fr.dart
@@ -792,11 +792,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get thisAppIsOpenSource => 'Cette application est open source\nRendez-nous visite sur GitHub';
- @override
- String invalidArgument(Object argument, Object type) {
- return '$argument n\'est pas une valeur valide pour $type';
- }
-
@override
String get importExportTokens => 'Importer/Exporter les jetons';
@@ -859,4 +854,25 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get confirmPassword => 'Confirmer le mot de passe';
+
+ @override
+ String get secretIsRequired => 'Secret is required';
+
+ @override
+ String get tokenDataParseError => 'Token data could not be parsed';
+
+ @override
+ String missingRequiredParameter(Object counter) {
+ return 'Value for parameter [$counter] is required and is missing';
+ }
+
+ @override
+ String invalidValueForParameter(Object value, Object parameter) {
+ return '[$value] is not a valid value for uri parameter [parameter].';
+ }
+
+ @override
+ String unsupported(Object name, Object value) {
+ return 'The $name [$value] is not supported by this version of the app.';
+ }
}
diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart
index 03d93b77c..372e68eb5 100644
--- a/lib/l10n/app_localizations_nl.dart
+++ b/lib/l10n/app_localizations_nl.dart
@@ -792,11 +792,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get thisAppIsOpenSource => 'Deze app is open source\nBezoek ons op GitHub';
- @override
- String invalidArgument(Object argument, Object type) {
- return '$argument is geen geldige waarde voor $type';
- }
-
@override
String get importExportTokens => 'Tokens importeren/exporteren';
@@ -859,4 +854,25 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get confirmPassword => 'Wachtwoord bevestigen';
+
+ @override
+ String get secretIsRequired => 'Secret is required';
+
+ @override
+ String get tokenDataParseError => 'Token data could not be parsed';
+
+ @override
+ String missingRequiredParameter(Object counter) {
+ return 'Value for parameter [$counter] is required and is missing';
+ }
+
+ @override
+ String invalidValueForParameter(Object value, Object parameter) {
+ return '[$value] is not a valid value for uri parameter [parameter].';
+ }
+
+ @override
+ String unsupported(Object name, Object value) {
+ return 'The $name [$value] is not supported by this version of the app.';
+ }
}
diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart
index 4ebe1bfea..e9d999ede 100644
--- a/lib/l10n/app_localizations_pl.dart
+++ b/lib/l10n/app_localizations_pl.dart
@@ -792,11 +792,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get thisAppIsOpenSource => 'Ta aplikacja jest open source\nOdwiedź nas na GitHub';
- @override
- String invalidArgument(Object argument, Object type) {
- return '$argument nie jest prawidłową wartością dla $type';
- }
-
@override
String get importExportTokens => 'Importuj/Eksportuj tokeny';
@@ -859,4 +854,25 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get confirmPassword => 'Potwierdź hasło';
+
+ @override
+ String get secretIsRequired => 'Secret is required';
+
+ @override
+ String get tokenDataParseError => 'Token data could not be parsed';
+
+ @override
+ String missingRequiredParameter(Object counter) {
+ return 'Value for parameter [$counter] is required and is missing';
+ }
+
+ @override
+ String invalidValueForParameter(Object value, Object parameter) {
+ return '[$value] is not a valid value for uri parameter [parameter].';
+ }
+
+ @override
+ String unsupported(Object name, Object value) {
+ return 'The $name [$value] is not supported by this version of the app.';
+ }
}
diff --git a/lib/model/encryption/aes_encrypted.dart b/lib/model/encryption/aes_encrypted.dart
index b610b2cfc..4e8ca823c 100644
--- a/lib/model/encryption/aes_encrypted.dart
+++ b/lib/model/encryption/aes_encrypted.dart
@@ -1,10 +1,19 @@
import 'dart:convert';
+import 'dart:math';
import 'dart:typed_data';
import 'package:cryptography/cryptography.dart';
+import 'package:cryptography/dart.dart';
import 'package:flutter/foundation.dart';
class AesEncrypted {
+ // [KdfAlgorithm/MacAlgorithm/Iterations/Bits]
+ static const Map 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..898d6b442 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 = jsonDecode(tokenJsonString) as List;
+ return tokenJsonsList.map((e) => Token.fromJson(e)).toList();
}
static Uri generateQrCodeUri({required Token token}) {
@@ -28,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..fa09abb40 100644
--- a/lib/model/enums/algorithms.dart
+++ b/lib/model/enums/algorithms.dart
@@ -1,111 +1,7 @@
// 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';
-
+// Do not rename or remove values, they are used for serialization. Only add new values.
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..11c4e6f17 100644
--- a/lib/model/enums/app_feature.dart
+++ b/lib/model/enums/app_feature.dart
@@ -1,20 +1,4 @@
-import 'algorithms.dart';
-
+// Do not rename or remove values, they are used for serialization. Only add new values.
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/day_passoword_token_view_mode.dart b/lib/model/enums/day_password_token_view_mode.dart
similarity index 54%
rename from lib/model/enums/day_passoword_token_view_mode.dart
rename to lib/model/enums/day_password_token_view_mode.dart
index 5196afd95..e06569c57 100644
--- a/lib/model/enums/day_passoword_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/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..61a3cd8ae 100644
--- a/lib/model/enums/introduction.dart
+++ b/lib/model/enums/introduction.dart
@@ -1,69 +1,13 @@
-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';
-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 {
- @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 {
- 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.tokenSwipe =>
- ref.watch(tokenProvider).tokens.isNotEmpty && state.isCompleted(Introduction.addTokenManually) && 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.addTokenManually => 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 b3dc8cbda..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 getName(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 47ea211aa..0f92f2e59 100644
--- a/lib/model/enums/token_import_type.dart
+++ b/lib/model/enums/token_import_type.dart
@@ -1,27 +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 {
- String get name => toString().split('.').last;
- 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 getButtonText(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..c24d1a34a 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 or remove values, they are used for serialization. Only add new values.
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/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 2dba81809..301f32513 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, {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
new file mode 100644
index 000000000..5402adc3e
--- /dev/null
+++ b/lib/model/extensions/enums/algorithms_extension.dart
@@ -0,0 +1,38 @@
+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,
+ bool isGoogle = true,
+ }) =>
+ 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),
+ };
+
+ /// 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,
+ bool isGoogle = true,
+ }) =>
+ 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/extensions/enums/introduction_extension.dart b/lib/model/extensions/enums/introduction_extension.dart
new file mode 100644
index 000000000..cc319b1fb
--- /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 => 'Not implemented',
+ 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..925ecbd64
--- /dev/null
+++ b/lib/model/extensions/enums/push_token_rollout_state_extension.dart
@@ -0,0 +1,37 @@
+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,
+ };
+
+ 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,
+ 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/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/sortable_list.dart b/lib/model/extensions/sortable_list.dart
new file mode 100644
index 000000000..1c32c896b
--- /dev/null
+++ b/lib/model/extensions/sortable_list.dart
@@ -0,0 +1,85 @@
+import '../mixins/sortable_mixin.dart';
+
+extension SortableList on List {
+ List get sorted {
+ var list = List.from(this);
+ list.sort((a, b) => a.compareTo(b));
+ print('-----------------------------------');
+ list.forEach((element) {
+ print('sorted: ${element.runtimeType} Sortindex: ${element.sortIndex}');
+ });
+ print('-----------------------------------');
+ return list;
+ }
+
+ List fillNullIndices() {
+ var list = List.from(this);
+ var highestIndex = fold(0, (previousValue, element) {
+ if (element.sortIndex == null) return previousValue;
+ if (previousValue > element.sortIndex!) return previousValue;
+ return element.sortIndex!;
+ });
+ for (var i = 0; i < list.length; i++) {
+ if (list[i].sortIndex == null) {
+ highestIndex++;
+ list[i] = list[i].copyWith(sortIndex: highestIndex) as T;
+ }
+ }
+ print('-----------------------------------');
+ list.forEach((element) {
+ print('fillNullIndices: ${element.runtimeType} Sortindex: ${element.sortIndex}');
+ });
+ print('-----------------------------------');
+ return list;
+ }
+
+ /// Moves the [movedItem] to the position after [moveAfter] or before [moveBefore].
+ /// If both [moveAfter] and [moveBefore] are null, the [movedItem] will be moved to the end of the list.
+ /// If both is set, [moveBefore] will be prioritized.
+ List moveBetween({T? moveAfter, required T movedItem, T? moveBefore}) {
+ var list = List.from(this).sorted.withCurrentSortIndexSet();
+ final success = list.remove(movedItem);
+ if (!success) return list;
+ final newIndex = moveBefore != null
+ ? list.indexOf(moveBefore)
+ : moveAfter != null && list.contains(moveAfter)
+ ? list.indexOf(moveAfter) + 1
+ : list.length;
+ list.insert(newIndex, movedItem);
+ list = list.withCurrentSortIndexSet();
+ print('-----------------------------------');
+ list.forEach((element) {
+ print('moveBetween: ${element.runtimeType} Sortindex: ${element.sortIndex}');
+ });
+ print('-----------------------------------');
+ return list;
+ }
+
+ List moveAllBetween({T? moveAfter, required List movedItems, T? moveBefore}) {
+ var list = List.from(this).sorted.withCurrentSortIndexSet();
+ List removedItems = [];
+ for (final movedItem in movedItems) {
+ final success = list.remove(movedItem);
+ if (success) removedItems.add(movedItem);
+ }
+ if (removedItems.isEmpty) return list;
+ final newIndex = moveBefore != null
+ ? list.indexOf(moveBefore)
+ : moveAfter != null && list.contains(moveAfter)
+ ? list.indexOf(moveAfter) + 1
+ : list.length;
+ list.insertAll(newIndex, removedItems);
+ list = list.withCurrentSortIndexSet();
+ return list;
+ }
+
+ List withCurrentSortIndexSet() {
+ final list = List.from(this);
+ for (var i = 0; i < list.length; i++) {
+ if (list[i].sortIndex != i) {
+ list[i] = list[i].copyWith(sortIndex: i) as T;
+ }
+ }
+ return list;
+ }
+}
diff --git a/lib/model/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/mixins/sortable_mixin.dart b/lib/model/mixins/sortable_mixin.dart
index 9081babf4..5274c67f0 100644
--- a/lib/model/mixins/sortable_mixin.dart
+++ b/lib/model/mixins/sortable_mixin.dart
@@ -1,8 +1,9 @@
mixin SortableMixin {
int? get sortIndex;
-
SortableMixin copyWith({int? sortIndex});
+ /// 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;
diff --git a/lib/model/processor_result.dart b/lib/model/processor_result.dart
index 62363792f..ffe5ca295 100644
--- a/lib/model/processor_result.dart
+++ b/lib/model/processor_result.dart
@@ -1,10 +1,12 @@
abstract class ProcessorResult {
const ProcessorResult();
factory ProcessorResult.success(T data) => ProcessorResultSuccess(data);
- factory ProcessorResult.error(String errorMessage) => ProcessorResultError(errorMessage);
+ factory ProcessorResult.failed(String errorMessage) => ProcessorResultFailed(errorMessage);
+ ProcessorResultSuccess? get asSuccess => this is ProcessorResultSuccess ? this as ProcessorResultSuccess : null;
+ ProcessorResultFailed? get asFailed => this is ProcessorResultFailed ? this as ProcessorResultFailed : null;
}
-class ProcessorResultSuccess implements ProcessorResult {
+class ProcessorResultSuccess extends ProcessorResult {
final T resultData;
const ProcessorResultSuccess(this.resultData);
@@ -14,12 +16,10 @@ class ProcessorResultSuccess implements ProcessorResult {
}
}
-class ProcessorResultError implements ProcessorResult {
- final String errorMessage;
- const ProcessorResultError(this.errorMessage);
+class ProcessorResultFailed extends ProcessorResult {
+ final String message;
+ const ProcessorResultFailed(this.message);
@override
- String toString() {
- return 'ProcessorResultError(errorMessage: $errorMessage)';
- }
+ String toString() => '$runtimeType(message: $message)';
}
diff --git a/lib/model/push_request.dart b/lib/model/push_request.dart
index 677040d29..6c7fa0dee 100644
--- a/lib/model/push_request.dart
+++ b/lib/model/push_request.dart
@@ -65,7 +65,7 @@ class PushRequest {
}
@override
- bool operator ==(Object other) => identical(this, other) || other is PushRequest && runtimeType == other.runtimeType && id == other.id;
+ bool operator ==(Object other) => other is PushRequest && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => Object.hash(runtimeType, id);
diff --git a/lib/model/push_request_queue.dart b/lib/model/push_request_queue.dart
deleted file mode 100644
index a725b6fba..000000000
--- a/lib/model/push_request_queue.dart
+++ /dev/null
@@ -1,119 +0,0 @@
-// import 'dart:collection';
-// import 'package:json_annotation/json_annotation.dart';
-
-// import 'push_request.dart';
-
-// part 'push_request_queue.g.dart';
-
-// @JsonSerializable()
-// class PushRequestQueue implements Iterable {
-// const PushRequestQueue({List? list}) : _list = list ?? const [];
-// final List _list;
-// List get list => _list;
-
-// @override
-// bool operator ==(Object other) {
-// if (identical(this, other)) return true;
-// return other is PushRequestQueue && _listsAreEqual(list, other.list);
-// }
-
-// @override
-// int get hashCode => list.hashCode;
-
-// bool _listsAreEqual(List l1, List l2) {
-// if (l1.length != l2.length) return false;
-// for (int i = 0; i < l1.length - 1; i++) {
-// if (l1[i] != l2[i]) return false;
-// }
-// return true;
-// }
-
-// factory PushRequestQueue.fromJson(Map json) => _$PushRequestQueueFromJson(json);
-// Map toJson() => _$PushRequestQueueToJson(this);
-
-// @override
-// PushRequest get first => list.first;
-// @override
-// PushRequest get last => list.last;
-// @override
-// int get length => list.length;
-// void add(PushRequest value) => list.add(value);
-// bool replace(PushRequest value) {
-// final index = list.indexWhere((element) => element.id == value.id);
-// if (index == -1) return false;
-// list[index] = value;
-// return true;
-// }
-
-// void addOrReplace(PushRequest value) {
-// final index = list.indexWhere((element) => element.id == value.id);
-// if (index != -1) {
-// list[index] = value;
-// } else {
-// list.add(value);
-// }
-// }
-
-// void addAll(Iterable iterable) => list.addAll(iterable);
-// void addFirst(PushRequest value) => list.insert(0, value);
-// @override
-// bool any(bool Function(PushRequest element) test) => list.any(test);
-// @override
-// Queue cast() => list.cast() as Queue;
-// void clear() => list.clear();
-// @override
-// bool contains(Object? element) => list.contains(element);
-// @override
-// PushRequest elementAt(int index) => list.elementAt(index);
-// @override
-// bool every(bool Function(PushRequest element) test) => list.every(test);
-// @override
-// Iterable expand(Iterable Function(PushRequest element) toElements) => list.expand(toElements);
-// @override
-// PushRequest firstWhere(bool Function(PushRequest element) test, {PushRequest Function()? orElse}) => list.firstWhere(test, orElse: orElse);
-// @override
-// T fold(T initialValue, T Function(T previousValue, PushRequest element) combine) => list.fold(initialValue, combine);
-// @override
-// Iterable followedBy(Iterable other) => list.followedBy(other);
-// @override
-// void forEach(void Function(PushRequest element) action) => list.forEach(action);
-// @override
-// bool get isEmpty => list.isEmpty;
-// @override
-// bool get isNotEmpty => list.isNotEmpty;
-// @override
-// Iterator get iterator => list.iterator;
-// @override
-// String join([String separator = ""]) => list.join(separator);
-// @override
-// PushRequest lastWhere(bool Function(PushRequest element) test, {PushRequest Function()? orElse}) => list.lastWhere(test, orElse: orElse);
-// @override
-// Iterable map(T Function(PushRequest e) toElement) => list.map(toElement);
-// @override
-// PushRequest reduce(PushRequest Function(PushRequest value, PushRequest element) combine) => list.reduce(combine);
-// bool remove(Object? value) => list.remove(value);
-// PushRequest? peek() => list.isNotEmpty ? list.first : null;
-// PushRequest pop() => list.removeAt(0);
-// PushRequest? tryPop() => list.isNotEmpty ? pop() : null;
-// @override
-// PushRequest get single => list.single;
-// @override
-// PushRequest singleWhere(bool Function(PushRequest element) test, {PushRequest Function()? orElse}) => list.singleWhere(test, orElse: orElse);
-// @override
-// Iterable skip(int count) => list.skip(count);
-// @override
-// Iterable skipWhile(bool Function(PushRequest value) test) => list.skipWhile(test);
-// @override
-// Iterable take(int count) => list.take(count);
-// @override
-// Iterable takeWhile(bool Function(PushRequest value) test) => list.takeWhile(test);
-// @override
-// List toList({bool growable = true}) => list.toList(growable: growable);
-// PushRequestQueue copy() => PushRequestQueue(list: list.toList());
-// @override
-// Set toSet() => list.toSet();
-// @override
-// Iterable where(bool Function(PushRequest element) test) => list.where(test);
-// @override
-// Iterable whereType() => list.whereType();
-// }
diff --git a/lib/model/serializable_RSA_private_key.dart b/lib/model/serializable_RSA_private_key.dart
deleted file mode 100644
index a83f338d1..000000000
--- a/lib/model/serializable_RSA_private_key.dart
+++ /dev/null
@@ -1,15 +0,0 @@
-// ignore_for_file: file_names
-
-import 'package:json_annotation/json_annotation.dart';
-import 'package:pointycastle/asymmetric/api.dart';
-
-part 'serializable_RSA_private_key.g.dart';
-
-@JsonSerializable()
-class SerializableRSAPrivateKey extends RSAPrivateKey {
- SerializableRSAPrivateKey(super.modulus, super.exponent, BigInt super.p, BigInt super.q);
-
- factory SerializableRSAPrivateKey.fromJson(Map json) => _$SerializableRSAPrivateKeyFromJson(json);
-
- Map toJson() => _$SerializableRSAPrivateKeyToJson(this);
-}
diff --git a/lib/model/serializable_RSA_private_key.g.dart b/lib/model/serializable_RSA_private_key.g.dart
deleted file mode 100644
index 31ac2fb60..000000000
--- a/lib/model/serializable_RSA_private_key.g.dart
+++ /dev/null
@@ -1,25 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'serializable_RSA_private_key.dart';
-
-// **************************************************************************
-// JsonSerializableGenerator
-// **************************************************************************
-
-SerializableRSAPrivateKey _$SerializableRSAPrivateKeyFromJson(
- Map json) =>
- SerializableRSAPrivateKey(
- BigInt.parse(json['modulus'] as String),
- BigInt.parse(json['exponent'] as String),
- BigInt.parse(json['p'] as String),
- BigInt.parse(json['q'] as String),
- );
-
-Map _$SerializableRSAPrivateKeyToJson(
- SerializableRSAPrivateKey instance) =>
- {
- 'modulus': instance.modulus?.toString(),
- 'exponent': instance.exponent?.toString(),
- 'p': instance.p?.toString(),
- 'q': instance.q?.toString(),
- };
diff --git a/lib/model/serializable_RSA_public_key.dart b/lib/model/serializable_RSA_public_key.dart
deleted file mode 100644
index 3096d6a6a..000000000
--- a/lib/model/serializable_RSA_public_key.dart
+++ /dev/null
@@ -1,15 +0,0 @@
-// ignore_for_file: file_names
-
-import 'package:json_annotation/json_annotation.dart';
-import 'package:pointycastle/asymmetric/api.dart';
-
-part 'serializable_RSA_public_key.g.dart';
-
-@JsonSerializable()
-class SerializableRSAPublicKey extends RSAPublicKey {
- SerializableRSAPublicKey(super.modulus, super.exponent);
-
- factory SerializableRSAPublicKey.fromJson(Map json) => _$SerializableRSAPublicKeyFromJson(json);
-
- Map toJson() => _$SerializableRSAPublicKeyToJson(this);
-}
diff --git a/lib/model/serializable_RSA_public_key.g.dart b/lib/model/serializable_RSA_public_key.g.dart
deleted file mode 100644
index 6350252c0..000000000
--- a/lib/model/serializable_RSA_public_key.g.dart
+++ /dev/null
@@ -1,21 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'serializable_RSA_public_key.dart';
-
-// **************************************************************************
-// JsonSerializableGenerator
-// **************************************************************************
-
-SerializableRSAPublicKey _$SerializableRSAPublicKeyFromJson(
- Map json) =>
- SerializableRSAPublicKey(
- BigInt.parse(json['modulus'] as String),
- BigInt.parse(json['exponent'] as String),
- );
-
-Map _$SerializableRSAPublicKeyToJson(
- SerializableRSAPublicKey instance) =>
- {
- 'modulus': instance.modulus?.toString(),
- 'exponent': instance.exponent?.toString(),
- };
diff --git a/lib/model/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/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/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/states/token_folder_state.dart b/lib/model/states/token_folder_state.dart
index c5010e532..f87626322 100644
--- a/lib/model/states/token_folder_state.dart
+++ b/lib/model/states/token_folder_state.dart
@@ -11,15 +11,19 @@ class TokenFolderState {
const TokenFolderState({required this.folders});
- TokenFolderState withFolder(String name) {
+ /// Add a new folder with the given name
+ /// Returns a new TokenFolderState with the new folder
+ /// The original List is not modified
+ TokenFolderState addNewFolder(String name) {
final newFolders = List.from(folders);
newFolders.add(TokenFolder(label: name, folderId: newFolderId));
return TokenFolderState(folders: newFolders);
}
- // replace all folders where the folderid is the same
- // if the folderid is none, add it to the list
- TokenFolderState withUpdated(List folders) {
+ /// Add or replace the folders with the same folderId
+ /// Returns a new TokenFolderState with the new folders
+ /// The original List is not modified
+ TokenFolderState addOrReplaceFolders(List folders) {
final newFolders = List.from(this.folders);
for (var newFolder in folders) {
final index = newFolders.indexWhere((oldFolder) => oldFolder.folderId == newFolder.folderId);
@@ -30,13 +34,28 @@ class TokenFolderState {
return TokenFolderState(folders: newFolders);
}
- TokenFolderState withoutFolder(TokenFolder folder) {
+ TokenFolderState addOrReplaceFolder(TokenFolder newFolder) {
+ final newFolders = List.from(folders);
+ final index = newFolders.indexWhere((element) => element.folderId == newFolder.folderId);
+ if (index != -1) {
+ newFolders[index] = newFolder;
+ }
+ return TokenFolderState(folders: newFolders);
+ }
+
+ /// Remove the folder with the same folderId
+ /// Returns a new TokenFolderState without the folder
+ /// The original List is not modified
+ TokenFolderState removeFolder(TokenFolder folder) {
final newFolders = List.from(folders);
newFolders.removeWhere((element) => element.folderId == folder.folderId);
return TokenFolderState(folders: newFolders);
}
- TokenFolderState withoutFolders(List folders) {
+ /// Remove the folders with the same folderId
+ /// Returns a new TokenFolderState without the folders
+ /// The original List is not modified
+ TokenFolderState removeFolders(List folders) {
final newFolders = List.from(this.folders);
newFolders.removeWhere((element) => folders.any((folder) => folder.folderId == element.folderId));
return TokenFolderState(folders: newFolders);
@@ -54,5 +73,9 @@ class TokenFolderState {
get newFolderId => folders.fold(0, (previousValue, element) => max(previousValue, element.folderId)) + 1;
- TokenFolder? getFolderById(int? id) => id == null ? null : folders.firstWhereOrNull((element) => element.folderId == id);
+ /// Get the folder by the given id, or null if the folder does not exist
+ TokenFolder? currentById(int? id) => id == null ? null : folders.firstWhereOrNull((element) => element.folderId == id);
+
+ /// Returns the current folder of the given folder, or null if the folder does not exist
+ TokenFolder? currentOf(TokenFolder folder) => folders.firstWhereOrNull((element) => element.folderId == folder.folderId);
}
diff --git a/lib/model/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..12c48674f 100644
--- a/lib/model/token_folder.dart
+++ b/lib/model/token_folder.dart
@@ -14,7 +14,7 @@ class TokenFolder with SortableMixin {
final bool isLocked;
@override
final int? sortIndex;
-
+ @override
const TokenFolder({
required this.label,
required this.folderId,
diff --git a/lib/model/token_import/token_import_origin.dart b/lib/model/token_import/token_import_origin.dart
index 21c3330f8..78f931819 100644
--- a/lib/model/token_import/token_import_origin.dart
+++ b/lib/model/token_import/token_import_origin.dart
@@ -1,7 +1,4 @@
-import 'package:flutter/material.dart';
-
-import '../../processors/mixins/token_import_processor.dart';
-import '../enums/token_import_type.dart';
+import 'token_import_source.dart';
class TokenImportOrigin {
final String appName;
@@ -14,11 +11,3 @@ class TokenImportOrigin {
this.iconPath,
});
}
-
-class TokenImportSource {
- final TokenImportType type;
- final TokenImportProcessor processor;
- final String Function(BuildContext context) importHint;
-
- const TokenImportSource({required this.processor, required this.type, required this.importHint});
-}
diff --git a/lib/model/token_import/token_import_source.dart b/lib/model/token_import/token_import_source.dart
new file mode 100644
index 000000000..10c720d6d
--- /dev/null
+++ b/lib/model/token_import/token_import_source.dart
@@ -0,0 +1,12 @@
+import 'package:privacyidea_authenticator/l10n/app_localizations.dart';
+
+import '../../processors/mixins/token_import_processor.dart';
+import '../enums/token_import_type.dart';
+
+class TokenImportSource {
+ final TokenImportType type;
+ final TokenImportProcessor processor;
+ final String Function(AppLocalizations localizations) importHint;
+
+ const TokenImportSource({required this.processor, required this.type, required this.importHint});
+}
diff --git a/lib/model/token_import/token_origin_data.dart b/lib/model/token_import/token_origin_data.dart
index 7c6d1357e..280614fff 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';
@@ -67,7 +67,7 @@ class TokenOriginData {
// toString prints not data because it contains the secret
@override
- String toString() => 'TokenOrigin{source: $source, app: $appName, isPrivacyIdeaToken: $isPrivacyIdeaToken, createdAt: $createdAt}';
+ String toString() => 'TokenOrigin{source: $source, app: $appName, isPrivacyIdeaToken: $isPrivacyIdeaToken, createdAt: $createdAt, data: $data';
factory TokenOriginData.fromJson(Map json) => _$TokenOriginDataFromJson(json);
diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart
index 44ee5574e..8f0d36a9e 100644
--- a/lib/model/tokens/day_password_token.dart
+++ b/lib/model/tokens/day_password_token.dart
@@ -1,13 +1,15 @@
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
+import 'package:privacyidea_authenticator/model/extensions/enums/algorithms_extension.dart';
+import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart';
+import 'package:privacyidea_authenticator/utils/errors.dart';
import 'package:uuid/uuid.dart';
import '../../utils/identifiers.dart';
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';
import '../token_import/token_origin_data.dart';
import 'otp_token.dart';
import 'token.dart';
@@ -17,7 +19,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 +41,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
@@ -72,10 +74,10 @@ class DayPasswordToken extends OTPToken {
int? digits,
String? secret,
String? tokenImage,
- int? sortIndex,
bool? pin,
bool? isLocked,
bool? isHidden,
+ int? sortIndex,
int? Function()? folderId,
TokenOriginData? origin,
}) =>
@@ -85,7 +87,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,
@@ -94,7 +96,7 @@ class DayPasswordToken extends OTPToken {
pin: pin ?? this.pin,
isLocked: isLocked ?? this.isLocked,
isHidden: isHidden ?? this.isHidden,
- folderId: folderId != null ? folderId.call() : this.folderId,
+ folderId: folderId != null ? folderId() : this.folderId,
origin: origin ?? this.origin,
);
@@ -119,19 +121,45 @@ class DayPasswordToken extends OTPToken {
return DateTime.now().add(durationUntilNextOTP + const Duration(milliseconds: 1));
}
+ /// Throws an Error if the uriMap is invalid
+ static void validateUriMap(Map uriMap) {
+ if (uriMap[URI_SECRET] == null) {
+ throw LocalizedArgumentError(
+ localizedMessage: ((localizations, value, name) => localizations.secretIsRequired),
+ unlocalizedMessage: 'Secret is required',
+ invalidValue: uriMap[URI_SECRET],
+ name: URI_SECRET,
+ );
+ }
+ if (uriMap[URI_PERIOD] < 1) {
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter),
+ unlocalizedMessage: 'Period must be greater than 0',
+ invalidValue: uriMap[URI_PERIOD],
+ name: URI_PERIOD,
+ );
+ }
+ if (uriMap[URI_DIGITS] < 1) {
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter),
+ unlocalizedMessage: 'Digits must be greater than 0',
+ invalidValue: uriMap[URI_DIGITS],
+ name: URI_DIGITS,
+ );
+ }
+ }
+
factory DayPasswordToken.fromUriMap(Map uriMap) {
- if (uriMap[URI_SECRET] == null) throw ArgumentError('Secret is required');
- if (uriMap[URI_PERIOD] < 1) throw ArgumentError('Period must be greater than 0');
- if (uriMap[URI_DIGITS] < 1) throw ArgumentError('Digits must be greater than 0');
+ validateUriMap(uriMap);
return DayPasswordToken(
label: uriMap[URI_LABEL] ?? '',
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]),
+ period: Duration(seconds: uriMap[URI_PERIOD] ?? 86400), // default 24 hours
tokenImage: uriMap[URI_IMAGE],
pin: uriMap[URI_PIN],
isLocked: uriMap[URI_PIN],
diff --git a/lib/model/tokens/hotp_token.dart b/lib/model/tokens/hotp_token.dart
index 7d7566211..7cb6faf57 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
@@ -28,15 +29,15 @@ 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.folderId,
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;
@@ -65,10 +66,10 @@ class HOTPToken extends OTPToken {
int? digits,
String? secret,
String? tokenImage,
- int? sortIndex,
bool? pin,
bool? isLocked,
bool? isHidden,
+ int? sortIndex,
int? Function()? folderId,
TokenOriginData? origin,
}) =>
@@ -81,10 +82,10 @@ 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,
folderId: folderId != null ? folderId() : this.folderId,
origin: origin ?? this.origin,
);
@@ -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/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 9187d0ff3..36711210e 100644
--- a/lib/model/tokens/otp_token.dart
+++ b/lib/model/tokens/otp_token.dart
@@ -22,9 +22,9 @@ abstract class OTPToken extends Token {
required super.type,
super.pin,
super.tokenImage,
- super.sortIndex,
super.isLocked,
super.isHidden,
+ super.sortIndex,
super.folderId,
super.origin,
super.label = '',
diff --git a/lib/model/tokens/push_token.dart b/lib/model/tokens/push_token.dart
index 550b77e9c..3239f2191 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';
@@ -59,8 +58,8 @@ 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.folderId,
super.pin,
super.isLocked,
@@ -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) {
@@ -107,7 +106,6 @@ class PushToken extends Token {
bool? sslVerify,
String? enrollmentCredentials,
Uri? url,
- int? sortIndex,
String? publicServerKey,
String? publicTokenKey,
String? privateTokenKey,
@@ -115,6 +113,7 @@ class PushToken extends Token {
bool? isRolledOut,
PushTokenRollOutState? rolloutState,
CustomIntBuffer? knownPushRequests,
+ int? sortIndex,
int? Function()? folderId,
TokenOriginData? origin,
}) {
@@ -131,13 +130,13 @@ 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,
folderId: folderId != null ? folderId() : this.folderId,
origin: origin ?? this.origin,
);
@@ -160,6 +159,7 @@ class PushToken extends Token {
'isRolledOut: $isRolledOut, '
'rolloutState: $rolloutState, '
'publicServerKey: $publicServerKey, '
+ 'privateTokenKey: $privateTokenKey, '
'publicTokenKey: $publicTokenKey}';
}
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 b29ef84fe..6e1d2eb24 100644
--- a/lib/model/tokens/steam_token.dart
+++ b/lib/model/tokens/steam_token.dart
@@ -1,17 +1,18 @@
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';
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';
-import 'totp_token.dart' show TOTPToken;
+import 'totp_token.dart';
part 'steam_token.g.dart';
@@ -19,26 +20,27 @@ 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({
- required super.period,
required super.id,
- required super.algorithm,
required super.secret,
+ String? type,
super.tokenImage,
- super.sortIndex,
super.pin,
super.isLocked,
super.isHidden,
+ super.sortIndex,
super.folderId,
super.origin,
super.label = '',
super.issuer = '',
}) : super(
- type: tokenType,
+ type: type ?? tokenType,
+ period: 30,
digits: 5,
+ algorithm: Algorithms.SHA1,
);
@override
@@ -53,23 +55,21 @@ class SteamToken extends TOTPToken {
int? sortIndex,
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,
- sortIndex: sortIndex ?? this.sortIndex,
pin: pin ?? this.pin,
isLocked: isLocked ?? this.isLocked,
isHidden: isHidden ?? this.isHidden,
+ sortIndex: sortIndex ?? this.sortIndex,
folderId: folderId != null ? folderId() : this.folderId,
origin: origin ?? this.origin,
);
@@ -82,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;
@@ -100,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: AlgorithmsX.fromString(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 971f72250..68a5d14ab 100644
--- a/lib/model/tokens/steam_token.g.dart
+++ b/lib/model/tokens/steam_token.g.dart
@@ -7,15 +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
@@ -36,13 +35,6 @@ Map _$SteamTokenToJson(SteamToken instance) =>
'folderId': instance.folderId,
'sortIndex': instance.sortIndex,
'origin': instance.origin,
- 'algorithm': _$AlgorithmsEnumMap[instance.algorithm]!,
+ 'type': instance.type,
'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 e0985e18b..6d55e8cea 100644
--- a/lib/model/tokens/token.dart
+++ b/lib/model/tokens/token.dart
@@ -25,7 +25,6 @@ abstract class Token with SortableMixin {
final int? folderId;
@override
final int? sortIndex;
-
final TokenOriginData? origin;
// Must be string representation of TokenType enum.
@@ -33,22 +32,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, 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.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, 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/model/tokens/totp_token.dart b/lib/model/tokens/totp_token.dart
index 9132b6df5..c09665871 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
@@ -46,10 +47,10 @@ class TOTPToken extends OTPToken {
required super.secret,
String? type,
super.tokenImage,
- super.sortIndex,
super.pin,
super.isLocked,
super.isHidden,
+ super.sortIndex,
super.folderId,
super.origin,
super.label = '',
@@ -74,10 +75,10 @@ class TOTPToken extends OTPToken {
String? secret,
int? period,
String? tokenImage,
- int? sortIndex,
bool? pin,
bool? isLocked,
bool? isHidden,
+ int? sortIndex,
int? Function()? folderId,
TokenOriginData? origin,
}) {
@@ -90,10 +91,10 @@ 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,
folderId: folderId != null ? folderId() : this.folderId,
origin: origin ?? this.origin,
);
@@ -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/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/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/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..c2ea1ff5c 100644
--- a/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart
+++ b/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart
@@ -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';
@@ -5,8 +7,6 @@ import '../../../utils/token_import_origins.dart';
import 'otp_auth_processor.dart';
class FreeOtpPlusQrProcessor extends OtpAuthProcessor {
- static const String _steamTokenIssuer = "Steam";
- static const String _steamTokenHost = "steam";
const FreeOtpPlusQrProcessor();
@override
@@ -15,10 +15,6 @@ class FreeOtpPlusQrProcessor extends OtpAuthProcessor {
Future>> _processOtpAuth(Uri uri) async {
final results = >[];
- final issuer = _parseIssuer(uri);
- if (issuer == _steamTokenIssuer) {
- uri = uri.replace(host: _steamTokenHost);
- }
final result = await super.processUri(uri);
results.addAll(result);
@@ -29,28 +25,12 @@ class FreeOtpPlusQrProcessor extends OtpAuthProcessor {
appName: TokenImportOrigins.freeOtpPlus.appName,
token: t.resultData,
isPrivacyIdeaToken: false,
- data: t.resultData.origin!.data,
+ data: uri.toString(),
),
);
}).toList();
}
- /// Parse the label and the issuer (if it exists) from the url.
- String _parseIssuer(Uri uri) {
- String param = uri.path.substring(1);
- param = Uri.decodeFull(param);
- try {
- if (param.contains(':')) {
- List split = param.split(':');
- return split[0];
- } else {
- return _parseIssuer(uri);
- }
- } catch (_) {
- return '';
- }
- }
-
@override
Set get supportedSchemes => const OtpAuthProcessor().supportedSchemes;
}
diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart
index 49dc80eb9..b0ee10ba6 100644
--- a/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart
+++ b/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart
@@ -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';
@@ -103,7 +104,7 @@ class GoogleAuthenticatorQrProcessor extends TokenImportSchemeProcessor {
error: e,
stackTrace: StackTrace.current,
);
- results.add(ProcessorResultError(e.toString()));
+ results.add(ProcessorResultFailed(e.toString()));
continue;
}
}
diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart
index ce32642a5..0a53482af 100644
--- a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart
+++ b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart
@@ -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';
@@ -23,32 +25,41 @@ class OtpAuthProcessor extends TokenImportSchemeProcessor {
@override
Future>> processUri(Uri uri, {bool fromInit = false}) async {
- if (!supportedSchemes.contains(uri.scheme)) return [ProcessorResultError('The scheme [${uri.scheme}] not supported')];
+ if (!supportedSchemes.contains(uri.scheme)) return [ProcessorResultFailed('The scheme [${uri.scheme}] not supported')];
Logger.info('Try to handle otpAuth:', name: 'token_notifier.dart#addTokenFromOtpAuth');
Map uriMap;
try {
uriMap = _parseOtpToken(uri);
- } on LocalizedException catch (e) {
- Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e);
- final message = e.localizedMessage(AppLocalizations.of(await globalContext)!);
- return [ProcessorResultError(message)];
+ } catch (e, s) {
+ if (e is LocalizedException) {
+ Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e.unlocalizedMessage, stackTrace: s);
+ final message = e.localizedMessage(AppLocalizations.of(await globalContext)!);
+ return [ProcessorResult.failed(message)];
+ }
+ String? message;
+ if (e is ArgumentError) {
+ Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e.message, stackTrace: s);
+ message = '${e.message} - ${e.name}: ${e.invalidValue}';
+ }
+ message ??= 'An error occurred while parsing the QR code.';
+ return [ProcessorResult.failed(globalContextSync != null ? AppLocalizations.of(globalContextSync!)?.tokenDataParseError ?? message : message)];
}
if (_is2StepURI(uri)) {
validateMap(uriMap, [URI_SECRET, URI_ITERATIONS, URI_OUTPUT_LENGTH_IN_BYTES, URI_SALT_LENGTH]);
final secret = uriMap[URI_SECRET] as Uint8List;
// Calculate the whole secret.
- Uint8List? twoStepSecret;
- while (twoStepSecret == null) {
- twoStepSecret = (await showAsyncDialog(
- barrierDismissible: false,
- builder: (context) => GenerateTwoStepDialog(
- iterations: uriMap[URI_ITERATIONS],
- keyLength: uriMap[URI_OUTPUT_LENGTH_IN_BYTES],
- saltLength: uriMap[URI_SALT_LENGTH],
- password: secret,
- ),
- ));
- await Future.delayed(const Duration(milliseconds: 500));
+
+ final twoStepSecret = (await showAsyncDialog(
+ barrierDismissible: false,
+ builder: (context) => GenerateTwoStepDialog(
+ iterations: uriMap[URI_ITERATIONS],
+ keyLength: uriMap[URI_OUTPUT_LENGTH_IN_BYTES],
+ saltLength: uriMap[URI_SALT_LENGTH],
+ password: secret,
+ ),
+ ));
+ if (twoStepSecret == null) {
+ return [const ProcessorResultFailed('The two step secret could not be generated, or was canceled.')];
}
uriMap[URI_SECRET] = twoStepSecret;
}
@@ -57,11 +68,11 @@ class OtpAuthProcessor extends TokenImportSchemeProcessor {
newToken = Token.fromUriMap(uriMap);
} on FormatException catch (e) {
Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e);
- return [ProcessorResultError(e.message)];
+ return [ProcessorResultFailed(e.message)];
} catch (e, s) {
Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e, stackTrace: s);
showMessage(message: 'An error occurred while parsing the QR code.', duration: const Duration(seconds: 3));
- return [const ProcessorResultError('An error occurred while parsing the QR code.')];
+ return [const ProcessorResultFailed('An error occurred while parsing the QR code.')];
}
return [ProcessorResultSuccess(newToken)];
}
@@ -71,11 +82,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, caseSensitive: false)) {
// otpauth://pipush/LABEL?PARAMETERS
return _parsePiPushToken(uri);
}
- if (TokenTypes.values.firstWhereOrNull((element) => element.isString(type)) != null) {
+ if (TokenTypes.values.firstWhereOrNull((element) => element.isName(type, caseSensitive: false)) != null) {
return _parseOtpAuth(uri);
}
throw ArgumentError.value(
@@ -85,6 +96,7 @@ Map _parseOtpToken(Uri uri) {
);
}
+const String _steamTokenIssuer = "Steam";
Map _parseOtpAuth(Uri uri) {
// otpauth://TYPE/LABEL?PARAMETERS
Map uriMap = {};
@@ -108,6 +120,9 @@ Map _parseOtpAuth(Uri uri) {
final (label, issuer) = _parseLabelAndIssuer(uri);
uriMap[URI_LABEL] = label;
uriMap[URI_ISSUER] = issuer;
+ if (issuer == _steamTokenIssuer) {
+ uriMap[URI_TYPE] = TokenTypes.STEAM.name;
+ }
// parse pin from response 'True'
if (uri.queryParameters['pin'] == 'True') {
@@ -118,8 +133,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;
@@ -127,10 +142,11 @@ Map _parseOtpAuth(Uri uri) {
String digitsAsString = uri.queryParameters['digits'] ?? '6'; // Optional parameter
if (digitsAsString != '6' && digitsAsString != '8') {
- throw ArgumentError.value(
- uri,
- 'uri',
- '[$digitsAsString] is not a valid number of digits',
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, name) => localizations.invalidValueForParameter(value, name),
+ unlocalizedMessage: '[$digitsAsString] is not a valid number of digits.',
+ invalidValue: digitsAsString,
+ name: 'digits',
);
}
@@ -140,7 +156,7 @@ Map _parseOtpAuth(Uri uri) {
// Parse secret.
String? secretAsString = uri.queryParameters['secret'];
- ArgumentError.checkNotNull(secretAsString);
+ ArgumentError.checkNotNull(secretAsString, 'secret');
// This is a fix for omitted padding in base32 encoded secrets.
//
@@ -155,7 +171,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].',
);
}
@@ -164,20 +180,22 @@ Map _parseOtpAuth(Uri uri) {
if (uriMap[URI_TYPE] == 'hotp') {
// Parse counter.
String? counterAsString = uri.queryParameters['counter'];
+ if (counterAsString == null) {
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, name) => localizations.missingRequiredParameter(name),
+ unlocalizedMessage: 'Value for parameter [counter] is required and is missing.',
+ invalidValue: counterAsString,
+ name: 'counter',
+ );
+ }
try {
- if (counterAsString == null) {
- throw ArgumentError.value(
- uri,
- 'uri',
- 'Value for parameter [counter] is not optional and is missing.',
- );
- }
uriMap[URI_COUNTER] = int.parse(counterAsString);
} on FormatException {
- throw ArgumentError.value(
- uri,
- 'uri',
- '[$counterAsString] is not a valid value for uri parameter [counter].',
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter),
+ unlocalizedMessage: '[$counterAsString] is not a valid value for uri parameter [counter].',
+ invalidValue: counterAsString,
+ name: 'counter',
);
}
}
@@ -203,28 +221,31 @@ Map _parseOtpAuth(Uri uri) {
try {
uriMap[URI_SALT_LENGTH] = int.parse(saltLengthAsString);
} on FormatException {
- throw ArgumentError.value(
- uri,
- 'uri',
- '[$saltLengthAsString] is not a valid value for parameter [2step_salt].',
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter),
+ unlocalizedMessage: '[$saltLengthAsString] is not a valid value for parameter [2step_salt].',
+ invalidValue: saltLengthAsString,
+ name: '2step_salt',
);
}
try {
uriMap[URI_OUTPUT_LENGTH_IN_BYTES] = int.parse(outputLengthInByteAsString);
} on FormatException {
- throw ArgumentError.value(
- uri,
- 'uri',
- '[$outputLengthInByteAsString] is not a valid value for parameter [2step_output].',
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter),
+ unlocalizedMessage: '[$outputLengthInByteAsString] is not a valid value for parameter [2step_output].',
+ invalidValue: outputLengthInByteAsString,
+ name: '2step_output',
);
}
try {
uriMap[URI_ITERATIONS] = int.parse(iterationsAsString);
} on FormatException {
- throw ArgumentError.value(
- uri,
- 'uri',
- '[$iterationsAsString] is not a valid value for parameter [2step_difficulty].',
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter),
+ unlocalizedMessage: '[$iterationsAsString] is not a valid value for parameter [2step_difficulty].',
+ invalidValue: iterationsAsString,
+ name: '2step_difficulty',
);
}
}
@@ -250,7 +271,12 @@ Map _parsePiPushToken(Uri uri) {
String? pushVersionAsString = uri.queryParameters['v'];
if (pushVersionAsString == null) {
- throw ArgumentError.value(uri, 'uri', 'Parameter [v] is not an optional parameter and is missing.');
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, name) => localizations.missingRequiredParameter(name),
+ unlocalizedMessage: 'Parameter [v] is not an optional parameter and is missing.',
+ invalidValue: pushVersionAsString,
+ name: 'v',
+ );
}
try {
@@ -259,17 +285,19 @@ Map _parsePiPushToken(Uri uri) {
Logger.info('Parsing push token with version: $pushVersion', name: 'parsing_utils.dart#parsePiAuth');
if (pushVersion > maxPushTokenVersion) {
- throw ArgumentError.value(
- 'Unsupported version: $pushVersion',
- 'QrParser#_parsePiAuth',
- 'The piauth version [$pushVersionAsString] is not supported by this version of the app.',
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, name) => localizations.unsupported(value, name),
+ unlocalizedMessage: 'The piauth version [$pushVersionAsString] is not supported by this version of the app.',
+ invalidValue: pushVersionAsString,
+ name: 'piauth version',
);
}
} on FormatException {
- throw ArgumentError.value(
- 'Invalid version: $pushVersionAsString',
- 'QrParser#_parsePiAuth',
- '[$pushVersionAsString] is not a valid value for parameter [v].',
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter),
+ unlocalizedMessage: '[$pushVersionAsString] is not a valid value for parameter [v].',
+ invalidValue: pushVersionAsString,
+ name: 'v',
);
}
@@ -283,19 +311,29 @@ Map _parsePiPushToken(Uri uri) {
uriMap[URI_SERIAL] = uri.queryParameters['serial'];
ArgumentError.checkNotNull(uriMap[URI_SERIAL], 'serial');
- String? url = uri.queryParameters['url'];
+ final String? url = uri.queryParameters['url'];
ArgumentError.checkNotNull(url, 'url');
try {
uriMap[URI_ROLLOUT_URL] = Uri.parse(url!);
} on FormatException {
- throw ArgumentError.value(uri, 'uri', '[$url] is not a valid Uri.');
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, name) => localizations.invalidValueForParameter(value, name),
+ unlocalizedMessage: '[$url] is not a valid Uri.',
+ invalidValue: url!,
+ name: 'url',
+ );
}
String ttlAsString = uri.queryParameters['ttl'] ?? '10';
try {
uriMap[URI_TTL] = int.parse(ttlAsString);
} on FormatException {
- throw ArgumentError.value('Invalid ttl: $ttlAsString', 'QrParser#_parsePiAuth', '[$ttlAsString] is not a valid value for parameter [ttl].');
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter),
+ unlocalizedMessage: '[$ttlAsString] is not a valid value for parameter [ttl].',
+ invalidValue: ttlAsString,
+ name: 'ttl',
+ );
}
uriMap[URI_ENROLLMENT_CREDENTIAL] = uri.queryParameters['enrollment_credential'];
diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart
index 17689df30..e2c13b161 100644
--- a/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart
+++ b/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart
@@ -23,11 +23,11 @@ class PrivacyIDEAAuthenticatorQrProcessor extends TokenImportSchemeProcessor {
}
try {
- final token = await TokenEncryption.fromQrCodeUri(uri);
+ final token = TokenEncryption.fromQrCodeUri(uri);
return [ProcessorResult.success(token)];
} catch (e) {
- return [ProcessorResult.error('Invalid URI')];
+ return [ProcessorResult.failed('Invalid URI')];
}
}
}
diff --git a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart
index 28996176c..f1cfc9977 100644
--- a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart
+++ b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart
@@ -10,15 +10,19 @@ import 'package:file_selector/file_selector.dart';
import 'package:pointycastle/export.dart';
import 'package:privacyidea_authenticator/model/enums/encodings.dart';
import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart';
+import 'package:privacyidea_authenticator/model/enums/token_types.dart';
+import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart';
+import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart';
import 'package:privacyidea_authenticator/model/tokens/token.dart';
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 '../../utils/utils.dart';
import 'token_import_file_processor_interface.dart';
import 'two_fas_import_file_processor.dart';
@@ -49,6 +53,7 @@ class AegisImportFileProcessor extends TokenImportFileProcessor {
static const String AEGIS_PERIOD = 'period';
static const String AEGIS_COUNTER = 'counter';
static const String AEGIS_PIN = 'pin';
+ static const String AEGIS_ID = 'uuid';
bool _isValidPlain(Map json) {
try {
@@ -112,16 +117,34 @@ class AegisImportFileProcessor extends TokenImportFileProcessor {
}
}
- Future>> _processPlain(Map json) async {
- final results = >[];
- if (json['db']['version'] != 2) {
- throw Exception('Unsupported backup version: ${json['db']['version']}.');
+ Future>> _processPlain(Map json) async => switch (json['db']['version'] as int) {
+ 2 => _processPlainV2(json),
+ 3 => _processPlainV3(json),
+ _ => _processPlainTryLatest(json),
+ };
+
+ Future>> _processPlainTryLatest(Map json) async {
+ try {
+ return await _processPlainV3(json);
+ } catch (_) {
+ throw LocalizedArgumentError(
+ localizedMessage: (localizations, value, name) => localizations.unsupported(name, value),
+ unlocalizedMessage: 'Unsupported backup version: ${json['db']['version']}.',
+ invalidValue: json['db']['version'],
+ name: 'aegis backup version',
+ );
}
+ }
+
+ Future>> _processPlainV2(Map json) async {
+ final results = >[];
+ final localization = AppLocalizations.of(await globalContext)!;
for (Map entry in json['db']['entries']) {
try {
if (entry['type'] != 'totp' && entry['type'] != 'hotp') {
// TODO: support other token types
Logger.warning('Unsupported token type: ${entry['type']}', name: '_processPlain#OtpAuthImportFileProcessor');
+ results.add(ProcessorResult.failed(localization.unsupported('token type', entry['type'])));
continue;
}
Map info = entry['info'];
@@ -141,14 +164,56 @@ class AegisImportFileProcessor extends TokenImportFileProcessor {
data: jsonEncode(entry),
),
};
- results.add(ProcessorResultSuccess(Token.fromUriMap(entryUriMap)));
+ final token = Token.fromUriMap(entryUriMap);
+ results.add(ProcessorResult.success(token.copyWith(id: entry[AEGIS_ID])));
+ } on LocalizedException catch (e) {
+ results.add(ProcessorResult.failed(e.localizedMessage(localization)));
+ } catch (e) {
+ Logger.error('Failed to parse token.', name: 'AegisImportFileProcessor#_processPlain', error: e, stackTrace: StackTrace.current);
+ results.add(ProcessorResult.failed(e.toString()));
+ }
+ }
+ return results;
+ }
+
+ Future>> _processPlainV3(Map json) async {
+ final results = >[];
+ final localization = AppLocalizations.of(await globalContext)!;
+ final entries = json['db']['entries'] as List;
+ for (Map entry in entries) {
+ try {
+ if (doesThrow(() => TokenTypes.values.byName((entry['type'] as String).toUpperCase()))) {
+ // TODO: support other token types
+ Logger.warning('Unsupported token type: ${entry['type']}', name: '_processPlain#OtpAuthImportFileProcessor');
+ results.add(ProcessorResult.failed(localization.unsupported('token type', entry['type'])));
+ continue;
+ }
+ Map info = entry['info'];
+ final entryUriMap = {
+ URI_TYPE: entry[AEGIS_TYPE],
+ URI_LABEL: entry[AEGIS_LABEL],
+ URI_ISSUER: entry[AEGIS_ISSUER],
+ URI_SECRET: Encodings.base32.decode(info[AEGIS_SECRET]),
+ URI_ALGORITHM: info[AEGIS_ALGORITHM],
+ URI_DIGITS: info[AEGIS_DIGITS],
+ URI_PERIOD: info[AEGIS_PERIOD],
+ URI_COUNTER: info[AEGIS_COUNTER],
+ URI_PIN: info[AEGIS_PIN],
+ URI_ORIGIN: TokenOriginSourceType.backupFile.toTokenOrigin(
+ appName: TokenImportOrigins.aegisAuthenticator.appName,
+ isPrivacyIdeaToken: false,
+ data: jsonEncode(entry),
+ ),
+ };
+ results.add(ProcessorResult.success(Token.fromUriMap(entryUriMap)));
} on LocalizedException catch (e) {
- results.add(ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!)));
+ results.add(ProcessorResultFailed(e.localizedMessage(localization)));
} catch (e) {
Logger.error('Failed to parse token.', name: 'AegisImportFileProcessor#_processPlain', error: e, stackTrace: StackTrace.current);
- results.add(ProcessorResultError(e.toString()));
+ results.add(ProcessorResultFailed(e.toString()));
}
}
+
return results;
}
diff --git a/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart
index 3824db135..bfce3aa69 100644
--- a/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart
+++ b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart
@@ -6,7 +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/enum_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/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 +19,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 +44,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();
@@ -219,10 +220,10 @@ class AuthenticatorProImportFileProcessor extends TokenImportFileProcessor {
final newResults = await const OtpAuthProcessor().processUri(uri);
results.addAll(newResults);
} on LocalizedException catch (e) {
- results.add(ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!)));
+ results.add(ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!)));
} catch (e) {
Logger.error('Failed to parse token.', name: 'authenticator_pro_import_file_processor#_processUriList', error: e, stackTrace: StackTrace.current);
- results.add(ProcessorResultError(e.toString()));
+ results.add(ProcessorResultFailed(e.toString()));
}
}
return results;
@@ -254,10 +255,10 @@ class AuthenticatorProImportFileProcessor extends TokenImportFileProcessor {
}
}
} on LocalizedException catch (e) {
- results.add(ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!)));
+ results.add(ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!)));
} catch (e) {
Logger.error('Failed to parse token.', name: 'authenticator_pro_import_file_processor#_processHtml', error: e, stackTrace: StackTrace.current);
- results.add(ProcessorResultError(e.toString()));
+ results.add(ProcessorResultFailed(e.toString()));
}
return results;
}
@@ -292,10 +293,10 @@ class AuthenticatorProImportFileProcessor extends TokenImportFileProcessor {
final token = Token.fromUriMap(uriMap);
result.add(ProcessorResultSuccess(token));
} on LocalizedException catch (e) {
- result.add(ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!)));
+ result.add(ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!)));
} catch (e) {
Logger.error('Failed to parse token.', name: 'authenticator_pro_import_file_processor#_processAuthPro', error: e, stackTrace: StackTrace.current);
- result.add(ProcessorResultError(e.toString()));
+ result.add(ProcessorResultFailed(e.toString()));
}
}
return result;
diff --git a/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart b/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart
index d9c83ad68..8c3d277cb 100644
--- a/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart
+++ b/lib/processors/token_import_file_processor/free_otp_plus_file_processor.dart
@@ -5,13 +5,14 @@ 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/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';
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';
@@ -72,7 +73,7 @@ class FreeOtpPlusFileProcessor extends TokenImportFileProcessor {
results.addAll(await const FreeOtpPlusQrProcessor().processUri(uri));
} catch (e) {
Logger.error('Failed to process line: $line', name: 'FreeOtpPlusFileProcessor#processFile', error: e, stackTrace: StackTrace.current);
- results.add(ProcessorResultError(e.toString()));
+ results.add(ProcessorResultFailed(e.toString()));
}
}
return results.map((t) {
@@ -102,10 +103,10 @@ class FreeOtpPlusFileProcessor extends TokenImportFileProcessor {
try {
return ProcessorResultSuccess(Token.fromUriMap(_jsonToUriMap(tokenJson)));
} on LocalizedException catch (e) {
- return ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!));
+ return ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!));
} catch (e) {
Logger.error('Failed to parse token.', name: 'FreeOtpPlusFileProcessor#_processJsonToken', error: e, stackTrace: StackTrace.current);
- return ProcessorResultError(e.toString());
+ return ProcessorResultFailed(e.toString());
}
}
diff --git a/lib/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor.dart b/lib/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor.dart
index 7bec5d8b2..0e04f5b84 100644
--- a/lib/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor.dart
+++ b/lib/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor.dart
@@ -44,7 +44,7 @@ class PrivacyIDEAAuthenticatorImportFileProcessor extends TokenImportFileProcess
return results;
} catch (e) {
Logger.error('Failed to process file', name: 'PrivacyIDEAAuthenticatorImportFileProcessor#processFile', error: e, stackTrace: StackTrace.current);
- return [ProcessorResultError(e.toString())];
+ return [ProcessorResultFailed(e.toString())];
}
}
}
diff --git a/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart b/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart
index a54e84338..0f2a863b2 100644
--- a/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart
+++ b/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart
@@ -4,15 +4,17 @@ 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';
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';
@@ -124,10 +126,10 @@ class TwoFasFileImportProcessor extends TokenImportFileProcessor {
try {
results.add(ProcessorResultSuccess(Token.fromUriMap(_twoFasToUriMap(twoFasToken))));
} on LocalizedException catch (e) {
- results.add(ProcessorResultError(e.localizedMessage(AppLocalizations.of(await globalContext)!)));
+ results.add(ProcessorResultFailed(e.localizedMessage(AppLocalizations.of(await globalContext)!)));
} catch (e) {
Logger.error('Failed to parse token.', name: 'two_fas_import_file_processor.dart#_processPlainTokens', error: e, stackTrace: StackTrace.current);
- results.add(ProcessorResultError(e.toString()));
+ results.add(ProcessorResultFailed(e.toString()));
}
}
Logger.info('successfully imported ${results.length} tokens', name: 'two_fas_import_file_processor.dart#processPlainTokens');
diff --git a/lib/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/repo/preference_token_folder_repository.dart b/lib/repo/preference_token_folder_repository.dart
index 880993af4..bba126a05 100644
--- a/lib/repo/preference_token_folder_repository.dart
+++ b/lib/repo/preference_token_folder_repository.dart
@@ -26,15 +26,15 @@ class PreferenceTokenFolderRepository extends TokenFolderRepository {
}
@override
- Future> saveOrReplaceFolders(List folders) async {
+ Future saveReplaceList(List folders) async {
try {
final jsons = folders.map((e) => e.toJson()).toList();
final json = jsonEncode(jsons);
await _prefs.then((prefs) => prefs.setString(_tokenFoldersKey, json));
- return [];
+ return true;
} catch (e, s) {
Logger.error('Failed to save folders', name: 'PreferenceTokenFolderRepository#saveFolders', error: e, stackTrace: s);
- return folders;
+ return false;
}
}
}
diff --git a/lib/state_notifiers/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/state_notifiers/token_folder_notifier.dart b/lib/state_notifiers/token_folder_notifier.dart
index d814a3310..03be7a086 100644
--- a/lib/state_notifiers/token_folder_notifier.dart
+++ b/lib/state_notifiers/token_folder_notifier.dart
@@ -1,4 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:mutex/mutex.dart';
import '../interfaces/repo/token_folder_repository.dart';
import '../model/states/token_folder_state.dart';
@@ -6,63 +7,120 @@ import '../model/token_folder.dart';
import '../utils/logger.dart';
class TokenFolderNotifier extends StateNotifier {
- Future? isLoading;
+ late final Future initState;
+ final Mutex _loadingRepoMutex = Mutex();
+ final Mutex _updateFolderMutex = Mutex();
final TokenFolderRepository _repo;
TokenFolderNotifier({required TokenFolderRepository repository, TokenFolderState? initialState})
: _repo = repository,
super(initialState ?? const TokenFolderState(folders: [])) {
- _loadFromRepo();
+ _init();
}
- void _loadFromRepo() => isLoading = Future(() async => state = TokenFolderState(folders: await _repo.loadFolders()));
-
- void _saveOrReplaceFolders(List folders) {
- isLoading = Future(() async {
- final failedFolders = await _repo.saveOrReplaceFolders(folders);
- if (failedFolders.isNotEmpty) {
- Logger.error('Failed to save or replace folders: $failedFolders', name: 'TokenFolderNotifier#_saveOrReplaceFolders');
- state = state.withoutFolders(failedFolders);
- }
- });
+ void _init() {
+ initState = Future(() async => state = TokenFolderState(folders: await _repo.loadFolders()));
}
- void addFolder(String name) {
- final newState = state.withFolder(name);
- state = newState;
- _saveOrReplaceFolders(newState.folders);
+ Future _addOrReplaceFolders(List folders) async {
+ await _loadingRepoMutex.acquire();
+ final success = await _repo.saveReplaceList(folders);
+ if (!success) {
+ Logger.warning(
+ 'Failed to save folders',
+ name: 'TokenFolderNotifier#_addOrReplaceFolders',
+ );
+ return false;
+ }
+ state = state.addOrReplaceFolders(folders);
+ _loadingRepoMutex.release();
+ return true;
}
- void removeFolder(TokenFolder folder) {
- final newState = state.withoutFolder(folder);
+ Future _addOrReplaceFolder(TokenFolder folder) async {
+ await _loadingRepoMutex.acquire();
+ final newState = state.addOrReplaceFolder(folder);
+ final success = await _repo.saveReplaceList(newState.folders);
+ if (!success) {
+ Logger.warning(
+ 'Failed to add or replace folder',
+ name: 'TokenFolderNotifier#_addOrReplaceFolder',
+ );
+ return false;
+ }
state = newState;
- _saveOrReplaceFolders(newState.folders);
+ _loadingRepoMutex.release();
+ return true;
}
- void updateFolder(TokenFolder folder) {
- final newState = state.withUpdated([folder]);
+ Future _addNewFolder(String name) async {
+ await _loadingRepoMutex.acquire();
+ final newState = state.addNewFolder(name);
+ final success = await _repo.saveReplaceList(newState.folders);
+ if (!success) {
+ Logger.warning(
+ 'Failed to add new folder',
+ name: 'TokenFolderNotifier#_addNewFolder',
+ );
+ return false;
+ }
state = newState;
- _saveOrReplaceFolders(newState.folders);
+ _loadingRepoMutex.release();
+ return true;
}
- void updateFolders(List folders) {
- final newState = state.withUpdated(folders);
+ Future _removeFolder(TokenFolder folder) async {
+ await _loadingRepoMutex.acquire();
+ final newState = state.removeFolder(folder);
+ final success = await _repo.saveReplaceList(newState.folders);
+ if (!success) {
+ Logger.warning(
+ 'Failed to remove folder',
+ name: 'TokenFolderNotifier#_removeFolder',
+ );
+ return false;
+ }
state = newState;
- _saveOrReplaceFolders(newState.folders);
+ _loadingRepoMutex.release();
+ return true;
}
- void expandFolderById(int id) {
- final folder = state.folders.firstWhere((element) => element.folderId == id).copyWith(isExpanded: true);
- updateFolder(folder);
+ Future addNewFolder(String name) => _addNewFolder(name);
+
+ Future removeFolder(TokenFolder folder) => _removeFolder(folder);
+
+ /// Search for the current version of the given folder and update it with the updater function.
+ /// If the folder is not found, nothing will happen.
+ /// Returns true if the operation is successful, false otherwise.
+ Future updateFolder(TokenFolder folder, Function(TokenFolder) updater) async {
+ await _updateFolderMutex.acquire();
+ final curent = state.currentOf(folder);
+ if (curent == null) return false;
+ final newFolder = updater(curent);
+ final success = await _addOrReplaceFolder(newFolder);
+ _updateFolderMutex.release();
+ return success;
}
- void collapseLockedFolders() {
+ Future addOrReplaceFolders(List folders) => _addOrReplaceFolders(folders);
+
+ Future expandFolder(TokenFolder folder) => updateFolder(folder, (p0) => p0.copyWith(isExpanded: true));
+ Future expandFolderById(int folderId) => updateFolder(state.currentById(folderId)!, (p0) => p0.copyWith(isExpanded: true));
+ Future collapseFolder(TokenFolder folder) => updateFolder(folder, (p0) => p0.copyWith(isExpanded: false));
+ Future lockFolder(TokenFolder folder) => updateFolder(folder, (p0) => p0.copyWith(isLocked: true));
+ Future unlockFolder(TokenFolder folder) => updateFolder(folder, (p0) => p0.copyWith(isLocked: false));
+ Future toggleFolderLock(TokenFolder folder) => updateFolder(folder, (p0) => p0.copyWith(isLocked: !folder.isLocked));
+ Future updateLabel(TokenFolder folder, String label) => updateFolder(folder, (p0) => p0.copyWith(label: label));
+
+ Future collapseLockedFolders() async {
+ await _updateFolderMutex.acquire();
final lockedFolders = state.folders.where((element) => element.isLocked).toList();
for (var i = 0; i < lockedFolders.length; i++) {
lockedFolders[i] = lockedFolders[i].copyWith(isExpanded: false);
}
- final newState = state.withUpdated(lockedFolders);
- state = newState;
- _saveOrReplaceFolders(newState.folders);
+ final newState = state.addOrReplaceFolders(lockedFolders);
+ final success = _addOrReplaceFolders(newState.folders);
+ _updateFolderMutex.release();
+ return success;
}
}
diff --git a/lib/state_notifiers/token_notifier.dart b/lib/state_notifiers/token_notifier.dart
index 1db512e85..8ec4c0690 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';
@@ -39,8 +41,8 @@ import '../utils/view_utils.dart';
class TokenNotifier extends StateNotifier {
static final Map _hidingTimers = {};
late final Future initState;
- final loadingRepoMutex = Mutex();
- final updatingTokensMutex = Mutex();
+ final _loadingRepoMutex = Mutex();
+ final _updatingTokensMutex = Mutex();
final TokenRepository _repo;
final RsaUtils _rsaUtils;
final PrivacyIdeaIOClient _ioClient;
@@ -80,24 +82,24 @@ class TokenNotifier extends StateNotifier {
/// Adds a token and returns true if successful, false if not.
Future