diff --git a/.github/workflows/flutter_build.yml b/.github/workflows/flutter_build.yml index dd19244aa..d1d1078a7 100644 --- a/.github/workflows/flutter_build.yml +++ b/.github/workflows/flutter_build.yml @@ -27,8 +27,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.16.5' - - run: "flutter upgrade" + flutter-version: '3.19.6' - run: "flutter --version" - run: "flutter pub get" - run: "flutter build ios -t 'lib/mains/main_netknights.dart' --debug --flavor netknights --no-codesign" @@ -48,14 +47,13 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: - distribution: 'zulu' - java-version: '17.0.7' + distribution: 'oracle' + java-version: '17' - uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.19.0' - - run: "flutter upgrade" + flutter-version: '3.19.6' + - run: 'flutter clean' - run: "flutter --version" - run: "flutter pub get" - - run: 'flutter clean' - - run: "flutter build apk -t 'lib/mains/main_netknights.dart' --debug --flavor netknights" \ No newline at end of file + - run: "flutter build apk -t 'lib/mains/main_netknights.dart' --debug --flavor netknights" diff --git a/.gitignore b/.gitignore index c7c2122a5..2fae39ee8 100644 --- a/.gitignore +++ b/.gitignore @@ -890,3 +890,4 @@ DerivedData/ .flutter-plugins-dependencies lib/l10n/untranslated.txt *.jks +ios/Flutter/flutter_export_environment.sh 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 @@ - - diff --git a/android/app/src/netknights/res/drawable-hdpi/hw_background.png b/android/app/src/netknights/res/drawable-hdpi/hw_background.png new file mode 100644 index 000000000..ac9ba78be Binary files /dev/null and b/android/app/src/netknights/res/drawable-hdpi/hw_background.png differ diff --git a/android/app/src/netknights/res/drawable-mdpi/hw_background.png b/android/app/src/netknights/res/drawable-mdpi/hw_background.png new file mode 100644 index 000000000..ac9ba78be Binary files /dev/null and b/android/app/src/netknights/res/drawable-mdpi/hw_background.png differ diff --git a/android/app/src/netknights/res/drawable-xhdpi/hw_background.png b/android/app/src/netknights/res/drawable-xhdpi/hw_background.png new file mode 100644 index 000000000..ac9ba78be Binary files /dev/null and b/android/app/src/netknights/res/drawable-xhdpi/hw_background.png differ diff --git a/android/app/src/netknights/res/drawable-xxhdpi/hw_background.png b/android/app/src/netknights/res/drawable-xxhdpi/hw_background.png new file mode 100644 index 000000000..ac9ba78be Binary files /dev/null and b/android/app/src/netknights/res/drawable-xxhdpi/hw_background.png differ diff --git a/android/app/src/netknights/res/drawable-xxxhdpi/hw_background.png b/android/app/src/netknights/res/drawable-xxxhdpi/hw_background.png new file mode 100644 index 000000000..ac9ba78be Binary files /dev/null and b/android/app/src/netknights/res/drawable-xxxhdpi/hw_background.png differ diff --git a/android/app/src/netknights/res/drawable/app_icon.png b/android/app/src/netknights/res/drawable/app_icon.png deleted file mode 100644 index f1142776d..000000000 Binary files a/android/app/src/netknights/res/drawable/app_icon.png and /dev/null differ diff --git a/android/app/src/netknights/res/drawable/hw_background.png b/android/app/src/netknights/res/drawable/hw_background.png new file mode 100644 index 000000000..ac9ba78be Binary files /dev/null and b/android/app/src/netknights/res/drawable/hw_background.png differ diff --git a/coverage/lcov.info b/coverage/lcov.info index 29fa84cc5..f7f21aaf6 100644 --- a/coverage/lcov.info +++ b/coverage/lcov.info @@ -1,600 +1,509 @@ -SF:lib\model\states\token_folder_state.dart -DA:11,19 -DA:13,2 -DA:14,4 -DA:15,6 -DA:16,2 -DA:21,2 -DA:22,4 -DA:23,4 -DA:24,10 -DA:25,4 -DA:26,2 -DA:29,2 -DA:32,2 -DA:33,4 -DA:34,10 -DA:35,2 +SF:lib\model\enums\introduction.dart +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 DA:38,0 DA:39,0 DA:40,0 DA:41,0 +DA:42,0 +DA:43,0 DA:44,0 +DA:45,0 DA:46,0 +DA:47,0 DA:48,0 DA:49,0 +DA:50,0 DA:51,0 DA:52,0 -DA:54,10 -LF:27 -LH:17 -end_of_record -SF:lib\model\token_folder.dart -DA:18,6 -DA:26,1 -DA:34,1 -DA:35,1 -DA:36,1 -DA:37,1 -DA:38,1 -DA:39,0 -DA:43,1 -DA:44,4 -DA:46,0 -DA:47,0 -DA:49,0 -DA:50,0 -DA:52,1 -DA:53,1 -DA:54,2 +DA:53,0 +DA:54,0 DA:57,0 -LF:18 -LH:12 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +LF:32 +LH:0 end_of_record -SF:lib\model\mixins\sortable_mixin.dart -DA:6,0 -DA:7,0 -DA:8,0 +SF:lib\model\states\introduction_state.dart DA:11,0 DA:13,0 -LF:5 -LH:0 -end_of_record -SF:lib\model\token_folder.g.dart -DA:9,2 -DA:10,1 -DA:11,1 -DA:12,1 -DA:13,1 -DA:14,1 -DA:17,0 +DA:14,0 +DA:16,36 DA:18,0 DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -LF:13 -LH:6 -end_of_record -SF:lib\model\states\token_state.dart -DA:11,4 -DA:12,4 -DA:14,2 -DA:16,2 -DA:17,10 -DA:20,1 -DA:21,2 -DA:22,1 +DA:21,1 +DA:22,2 DA:23,1 -DA:26,1 -DA:27,2 -DA:28,1 +DA:24,1 +DA:27,1 +DA:28,2 DA:29,1 -DA:32,2 -DA:33,4 -DA:34,10 -DA:35,2 -DA:38,1 -DA:39,2 -DA:40,7 -DA:41,1 -DA:45,2 -DA:46,4 -DA:47,10 -DA:48,4 -DA:49,2 -DA:51,2 -DA:53,2 -DA:58,2 -DA:59,4 -DA:60,4 -DA:61,10 -DA:62,4 -DA:63,2 -DA:66,2 -DA:68,2 -DA:71,0 -DA:72,0 -DA:73,0 -LF:39 -LH:36 -end_of_record -SF:lib\model\tokens\token.dart -DA:27,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 +DA:30,1 DA:33,0 -DA:35,1 -DA:36,1 -DA:37,5 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:44,6 -DA:70,1 -DA:72,4 -DA:75,0 -DA:76,0 -DA:78,1 -DA:80,10 -LF:21 -LH:8 +DA:35,0 +DA:36,0 +LF:17 +LH:9 end_of_record -SF:lib\model\states\settings_state.dart +SF:lib\model\states\introduction_state.g.dart +DA:9,0 +DA:10,0 DA:11,0 DA:12,0 DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 DA:17,0 +DA:18,0 DA:19,0 DA:20,0 -DA:28,0 -DA:30,0 -DA:31,0 -DA:35,1 -DA:44,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:51,0 +DA:21,0 +LF:10 +LH:0 +end_of_record +SF:lib\model\push_request.dart +DA:28,1 +DA:41,1 DA:53,1 +DA:54,1 +DA:55,1 +DA:56,1 +DA:57,1 +DA:58,1 +DA:59,1 +DA:60,1 +DA:61,1 +DA:62,1 DA:63,1 -DA:64,1 -DA:65,1 -DA:66,1 DA:67,1 -DA:68,1 -DA:69,1 -DA:70,1 -DA:71,1 -DA:75,1 -DA:77,9 -DA:79,0 -DA:80,0 -DA:83,1 -DA:88,1 -DA:89,3 -DA:90,3 -DA:91,3 -DA:92,3 -DA:93,5 -DA:94,5 -DA:95,3 -DA:96,3 -DA:99,0 -DA:100,0 -DA:101,0 -LF:48 -LH:23 -end_of_record -SF:lib\l10n\app_localizations.dart -DA:68,0 -DA:72,0 -DA:73,0 -DA:852,16 -DA:854,0 -DA:856,0 -DA:859,0 -DA:860,0 -DA:862,0 -DA:866,0 -DA:870,0 -DA:871,0 -DA:872,0 -DA:873,0 -DA:874,0 -DA:875,0 -DA:876,0 -DA:877,0 -DA:880,0 -LF:19 -LH:1 -end_of_record -SF:lib\l10n\app_localizations_cs.dart -DA:5,0 -DA:7,0 -DA:10,0 -DA:13,0 -DA:16,0 -DA:19,0 -DA:22,0 -DA:25,0 -DA:28,0 -DA:31,0 -DA:34,0 -DA:37,0 -DA:40,0 -DA:43,0 -DA:46,0 -DA:49,0 -DA:52,0 -DA:55,0 -DA:58,0 -DA:61,0 -DA:64,0 -DA:67,0 +DA:68,7 DA:70,0 -DA:72,0 +DA:71,0 +DA:73,0 DA:75,0 +DA:76,0 +DA:77,0 DA:78,0 DA:81,0 DA:83,0 -DA:86,0 +DA:85,0 +DA:87,0 DA:89,0 +DA:91,0 DA:92,0 +DA:93,0 +DA:94,0 DA:95,0 +DA:96,0 +DA:97,0 DA:98,0 -DA:101,0 -DA:104,0 +DA:99,0 +DA:100,0 +DA:106,0 DA:107,0 +DA:108,0 DA:110,0 +DA:111,0 DA:113,0 +DA:114,0 +DA:115,0 DA:116,0 +DA:118,0 DA:119,0 +DA:121,0 DA:122,0 +DA:124,0 DA:125,0 +DA:127,0 DA:128,0 -DA:131,0 +DA:132,0 DA:133,0 +DA:134,0 +DA:135,0 DA:136,0 +DA:137,0 +DA:138,0 DA:139,0 -DA:141,0 -DA:144,0 +DA:142,0 +DA:143,0 DA:146,0 -DA:149,0 -DA:152,0 -DA:155,0 +DA:147,0 +DA:148,0 +DA:151,0 DA:158,0 -DA:161,0 -DA:164,0 -DA:167,0 -DA:170,0 -DA:173,0 -DA:176,0 -DA:179,0 -DA:182,0 -DA:185,0 -DA:188,0 -DA:191,0 -DA:194,0 -DA:197,0 -DA:200,0 -DA:203,0 -DA:206,0 -DA:209,0 -DA:212,0 -DA:215,0 -DA:218,0 -DA:221,0 -DA:224,0 -DA:227,0 -DA:229,0 -DA:232,0 -DA:235,0 -DA:238,0 -DA:241,0 -DA:244,0 -DA:247,0 -DA:250,0 -DA:253,0 -DA:256,0 -DA:259,0 -DA:262,0 -DA:265,0 -DA:268,0 -DA:271,0 -DA:274,0 -DA:277,0 -DA:280,0 -DA:283,0 -DA:286,0 -DA:289,0 -DA:292,0 -DA:295,0 -DA:298,0 -DA:301,0 -DA:304,0 -DA:307,0 -DA:310,0 -DA:313,0 -DA:316,0 -DA:319,0 -DA:322,0 -DA:325,0 -DA:328,0 -DA:331,0 -DA:334,0 -DA:337,0 -DA:340,0 -DA:343,0 -DA:346,0 -DA:349,0 -DA:352,0 -DA:355,0 -DA:358,0 -DA:361,0 -DA:364,0 -DA:367,0 -DA:370,0 -DA:372,0 -DA:375,0 -DA:377,0 -DA:380,0 -DA:383,0 -DA:386,0 -DA:389,0 -DA:392,0 -LF:133 -LH:0 +LF:69 +LH:15 end_of_record -SF:lib\l10n\app_localizations_de.dart -DA:5,0 -DA:7,0 +SF:lib\model\push_request.g.dart +DA:9,0 DA:10,0 +DA:11,0 +DA:12,0 DA:13,0 +DA:14,0 +DA:15,0 DA:16,0 +DA:17,0 +DA:18,0 DA:19,0 DA:22,0 +DA:23,0 +DA:24,0 DA:25,0 +DA:26,0 +DA:27,0 DA:28,0 +DA:29,0 +DA:30,0 DA:31,0 -DA:34,0 +DA:32,0 +DA:33,0 +LF:23 +LH:0 +end_of_record +SF:lib\model\states\push_request_state.dart +DA:13,35 +DA:15,3 +DA:16,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:24,2 +DA:25,3 +DA:26,2 +DA:29,1 +DA:30,2 +DA:31,2 +DA:32,1 +DA:33,0 +DA:35,0 +DA:36,0 DA:37,0 -DA:40,0 -DA:43,0 -DA:46,0 -DA:49,0 -DA:52,0 -DA:55,0 -DA:58,0 -DA:61,0 -DA:64,0 +DA:40,2 +DA:41,1 +DA:42,1 +DA:43,1 +DA:47,1 +DA:48,2 +DA:49,5 +DA:50,2 +DA:53,1 +DA:54,2 +DA:57,7 +DA:59,1 +DA:63,7 +DA:66,0 DA:67,0 +DA:69,0 DA:70,0 -DA:72,0 -DA:75,0 +DA:77,0 DA:78,0 -DA:81,0 +DA:79,0 +DA:82,0 DA:83,0 -DA:86,0 -DA:89,0 -DA:92,0 -DA:95,0 -DA:98,0 -DA:101,0 -DA:104,0 -DA:107,0 -DA:110,0 -DA:113,0 -DA:116,0 -DA:119,0 -DA:122,0 -DA:125,0 -DA:128,0 -DA:131,0 -DA:133,0 -DA:136,0 -DA:139,0 -DA:141,0 -DA:144,0 -DA:146,0 -DA:149,0 -DA:152,0 -DA:155,0 -DA:158,0 -DA:161,0 -DA:164,0 -DA:167,0 -DA:170,0 -DA:173,0 -DA:176,0 -DA:179,0 -DA:182,0 -DA:185,0 -DA:188,0 -DA:191,0 -DA:194,0 -DA:197,0 -DA:200,0 -DA:203,0 -DA:206,0 -DA:209,0 -DA:212,0 -DA:215,0 -DA:218,0 -DA:221,0 -DA:224,0 -DA:227,0 -DA:229,0 -DA:232,0 -DA:235,0 -DA:238,0 -DA:241,0 -DA:244,0 -DA:247,0 -DA:250,0 -DA:253,0 -DA:256,0 -DA:259,0 -DA:262,0 -DA:265,0 -DA:268,0 -DA:271,0 -DA:274,0 -DA:277,0 -DA:280,0 -DA:283,0 -DA:286,0 -DA:289,0 -DA:292,0 -DA:295,0 -DA:298,0 -DA:301,0 -DA:304,0 -DA:307,0 -DA:310,0 -DA:313,0 -DA:316,0 -DA:319,0 -DA:322,0 -DA:325,0 -DA:328,0 -DA:331,0 -DA:334,0 -DA:337,0 -DA:340,0 -DA:343,0 -DA:346,0 -DA:349,0 -DA:352,0 -DA:355,0 -DA:358,0 -DA:361,0 -DA:364,0 -DA:367,0 -DA:370,0 -DA:372,0 -DA:375,0 -DA:377,0 -DA:380,0 -DA:383,0 -DA:386,0 -DA:389,0 -DA:392,0 -LF:133 -LH:0 +DA:84,0 +LF:40 +LH:22 end_of_record -SF:lib\l10n\app_localizations_en.dart -DA:5,0 -DA:7,0 +SF:lib\model\states\push_request_state.g.dart +DA:9,0 DA:10,0 +DA:11,0 +DA:12,0 DA:13,0 -DA:16,0 -DA:19,0 -DA:22,0 -DA:25,0 -DA:28,0 +DA:14,0 +DA:15,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +LF:11 +LH:0 +end_of_record +SF:lib\model\states\settings_state.dart +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,2 +DA:23,4 DA:31,0 +DA:33,0 DA:34,0 -DA:37,0 -DA:40,0 -DA:43,0 -DA:46,0 -DA:49,0 +DA:40,2 +DA:51,0 DA:52,0 +DA:53,0 +DA:54,0 DA:55,0 +DA:56,0 +DA:57,0 DA:58,0 -DA:61,0 -DA:64,0 -DA:67,0 -DA:70,0 +DA:59,2 +DA:60,2 +DA:62,2 +DA:74,2 +DA:75,1 +DA:76,1 +DA:77,1 +DA:78,1 +DA:79,1 +DA:80,1 +DA:81,1 +DA:82,1 +DA:83,2 +DA:84,2 +DA:88,1 +DA:89,5 +DA:90,4 +DA:91,1 +DA:93,1 +DA:94,3 +DA:97,1 +DA:102,1 +DA:103,3 +DA:104,3 +DA:105,3 +DA:106,3 +DA:107,5 +DA:108,5 +DA:109,3 +DA:110,3 +DA:111,3 +DA:114,1 +DA:115,1 +DA:116,4 +LF:57 +LH:37 +end_of_record +SF:lib\model\token_folder.dart +DA:18,6 +DA:26,1 +DA:34,1 +DA:35,1 +DA:36,1 +DA:37,1 +DA:38,1 +DA:39,0 +DA:43,1 +DA:44,4 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:52,1 +DA:53,1 +DA:54,2 +DA:57,0 +LF:18 +LH:12 +end_of_record +SF:lib\model\mixins\sortable_mixin.dart +DA:6,0 +DA:7,0 +DA:8,0 +DA:11,0 +DA:13,0 +LF:5 +LH:0 +end_of_record +SF:lib\model\token_folder.g.dart +DA:9,2 +DA:10,1 +DA:11,1 +DA:12,1 +DA:13,1 +DA:14,1 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +LF:13 +LH:6 +end_of_record +SF:lib\model\tokens\token.dart +DA:16,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,1 +DA:46,1 +DA:47,1 +DA:48,2 +DA:49,2 +DA:50,0 +DA:51,0 +DA:52,0 +DA:55,6 +DA:69,6 +DA:73,0 +DA:74,0 +DA:77,0 +DA:78,0 +DA:95,1 +DA:97,4 +DA:100,0 +DA:101,0 +DA:103,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +LF:39 +LH:9 +end_of_record +SF:lib\l10n\app_localizations.dart +DA:68,0 +DA:72,0 +DA:73,0 +DA:1650,17 +DA:1652,0 +DA:1654,0 +DA:1657,0 +DA:1658,0 +DA:1660,0 +DA:1664,0 +DA:1668,0 +DA:1669,0 +DA:1670,0 +DA:1671,0 +DA:1672,0 +DA:1673,0 +DA:1674,0 +DA:1675,0 +DA:1678,0 +LF:19 +LH:1 +end_of_record +SF:lib\l10n\app_localizations_cs.dart +DA:7,0 +DA:9,0 +DA:12,0 +DA:15,0 +DA:18,0 +DA:21,0 +DA:24,0 +DA:27,0 +DA:30,0 +DA:33,0 +DA:36,0 +DA:39,0 +DA:42,0 +DA:45,0 +DA:48,0 +DA:51,0 +DA:54,0 +DA:57,0 +DA:60,0 +DA:63,0 +DA:66,0 +DA:69,0 DA:72,0 DA:75,0 DA:78,0 DA:81,0 -DA:83,0 -DA:86,0 -DA:89,0 -DA:92,0 -DA:95,0 -DA:98,0 -DA:101,0 -DA:104,0 +DA:84,0 +DA:87,0 +DA:90,0 +DA:93,0 +DA:96,0 +DA:99,0 +DA:102,0 +DA:105,0 DA:107,0 DA:110,0 DA:113,0 DA:116,0 DA:119,0 DA:122,0 -DA:125,0 -DA:128,0 -DA:131,0 +DA:124,0 +DA:127,0 +DA:130,0 DA:133,0 DA:136,0 DA:139,0 -DA:141,0 -DA:144,0 -DA:146,0 -DA:149,0 -DA:152,0 -DA:155,0 -DA:158,0 -DA:161,0 -DA:164,0 -DA:167,0 -DA:170,0 -DA:173,0 -DA:176,0 -DA:179,0 +DA:142,0 +DA:145,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:177,0 +DA:180,0 DA:182,0 DA:185,0 DA:188,0 -DA:191,0 -DA:194,0 -DA:197,0 -DA:200,0 -DA:203,0 -DA:206,0 -DA:209,0 +DA:190,0 +DA:193,0 +DA:195,0 +DA:198,0 +DA:201,0 +DA:204,0 +DA:207,0 +DA:210,0 DA:212,0 DA:215,0 DA:218,0 DA:221,0 DA:224,0 DA:227,0 -DA:229,0 -DA:232,0 -DA:235,0 -DA:238,0 -DA:241,0 -DA:244,0 -DA:247,0 -DA:250,0 -DA:253,0 -DA:256,0 -DA:259,0 -DA:262,0 -DA:265,0 -DA:268,0 -DA:271,0 -DA:274,0 -DA:277,0 -DA:280,0 -DA:283,0 -DA:286,0 -DA:289,0 -DA:292,0 -DA:295,0 +DA:230,0 +DA:233,0 +DA:236,0 +DA:239,0 +DA:242,0 +DA:245,0 +DA:248,0 +DA:251,0 +DA:254,0 +DA:257,0 +DA:260,0 +DA:263,0 +DA:266,0 +DA:269,0 +DA:272,0 +DA:275,0 +DA:278,0 +DA:281,0 +DA:284,0 +DA:287,0 +DA:290,0 +DA:293,0 +DA:296,0 DA:298,0 DA:301,0 DA:304,0 @@ -620,118 +529,280 @@ DA:361,0 DA:364,0 DA:367,0 DA:370,0 -DA:372,0 -DA:375,0 -DA:377,0 -DA:380,0 -DA:383,0 -DA:386,0 -DA:389,0 -DA:392,0 -LF:133 +DA:373,0 +DA:376,0 +DA:379,0 +DA:382,0 +DA:385,0 +DA:388,0 +DA:391,0 +DA:394,0 +DA:397,0 +DA:400,0 +DA:403,0 +DA:406,0 +DA:409,0 +DA:412,0 +DA:415,0 +DA:418,0 +DA:421,0 +DA:424,0 +DA:427,0 +DA:430,0 +DA:433,0 +DA:436,0 +DA:439,0 +DA:442,0 +DA:445,0 +DA:448,0 +DA:450,0 +DA:453,0 +DA:456,0 +DA:458,0 +DA:461,0 +DA:464,0 +DA:467,0 +DA:470,0 +DA:473,0 +DA:476,0 +DA:479,0 +DA:482,0 +DA:485,0 +DA:488,0 +DA:491,0 +DA:494,0 +DA:497,0 +DA:500,0 +DA:503,0 +DA:506,0 +DA:509,0 +DA:512,0 +DA:515,0 +DA:518,0 +DA:521,0 +DA:524,0 +DA:527,0 +DA:530,0 +DA:533,0 +DA:536,0 +DA:539,0 +DA:542,0 +DA:545,0 +DA:548,0 +DA:550,0 +DA:553,0 +DA:556,0 +DA:559,0 +DA:562,0 +DA:565,0 +DA:568,0 +DA:571,0 +DA:574,0 +DA:577,0 +DA:580,0 +DA:583,0 +DA:586,0 +DA:589,0 +DA:592,0 +DA:594,0 +DA:597,0 +DA:599,0 +DA:602,0 +DA:604,0 +DA:607,0 +DA:609,0 +DA:612,0 +DA:614,0 +DA:616,0 +DA:617,0 +DA:621,0 +DA:624,0 +DA:626,0 +DA:628,0 +DA:629,0 +DA:633,0 +DA:636,0 +DA:638,0 +DA:640,0 +DA:645,0 +DA:648,0 +DA:650,0 +DA:652,0 +DA:653,0 +DA:657,0 +DA:660,0 +DA:663,0 +DA:666,0 +DA:669,0 +DA:672,0 +DA:675,0 +DA:678,0 +DA:681,0 +DA:684,0 +DA:687,0 +DA:690,0 +DA:693,0 +DA:696,0 +DA:699,0 +DA:702,0 +DA:705,0 +DA:708,0 +DA:711,0 +DA:714,0 +DA:717,0 +DA:720,0 +DA:723,0 +DA:726,0 +DA:729,0 +DA:732,0 +DA:735,0 +DA:738,0 +DA:740,0 +DA:743,0 +DA:745,0 +DA:748,0 +DA:751,0 +DA:754,0 +DA:757,0 +DA:760,0 +DA:763,0 +DA:766,0 +DA:769,0 +DA:772,0 +DA:775,0 +DA:778,0 +DA:781,0 +DA:784,0 +DA:787,0 +DA:789,0 +DA:792,0 +DA:795,0 +DA:797,0 +DA:800,0 +DA:803,0 +DA:806,0 +DA:808,0 +DA:810,0 +DA:815,0 +DA:818,0 +DA:821,0 +DA:824,0 +DA:827,0 +DA:830,0 +DA:833,0 +DA:836,0 +DA:839,0 +DA:842,0 +DA:845,0 +DA:848,0 +DA:851,0 +DA:854,0 +DA:857,0 +DA:860,0 +LF:295 LH:0 end_of_record -SF:lib\l10n\app_localizations_es.dart -DA:5,0 +SF:lib\l10n\app_localizations_de.dart DA:7,0 -DA:10,0 -DA:13,0 -DA:16,0 -DA:19,0 -DA:22,0 -DA:25,0 -DA:28,0 -DA:31,0 -DA:34,0 -DA:37,0 -DA:40,0 -DA:43,0 -DA:46,0 -DA:49,0 -DA:52,0 -DA:55,0 -DA:58,0 -DA:61,0 -DA:64,0 -DA:67,0 -DA:70,0 +DA:9,0 +DA:12,0 +DA:15,0 +DA:18,0 +DA:21,0 +DA:24,0 +DA:27,0 +DA:30,0 +DA:33,0 +DA:36,0 +DA:39,0 +DA:42,0 +DA:45,0 +DA:48,0 +DA:51,0 +DA:54,0 +DA:57,0 +DA:60,0 +DA:63,0 +DA:66,0 +DA:69,0 DA:72,0 DA:75,0 DA:78,0 DA:81,0 -DA:83,0 -DA:86,0 -DA:89,0 -DA:92,0 -DA:95,0 -DA:98,0 -DA:101,0 -DA:104,0 +DA:84,0 +DA:87,0 +DA:90,0 +DA:93,0 +DA:96,0 +DA:99,0 +DA:102,0 +DA:105,0 DA:107,0 DA:110,0 DA:113,0 DA:116,0 DA:119,0 DA:122,0 -DA:125,0 -DA:128,0 -DA:131,0 +DA:124,0 +DA:127,0 +DA:130,0 DA:133,0 DA:136,0 DA:139,0 -DA:141,0 -DA:144,0 -DA:146,0 -DA:149,0 -DA:152,0 -DA:155,0 -DA:158,0 -DA:161,0 -DA:164,0 -DA:167,0 -DA:170,0 -DA:173,0 -DA:176,0 -DA:179,0 +DA:142,0 +DA:145,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:177,0 +DA:180,0 DA:182,0 DA:185,0 DA:188,0 -DA:191,0 -DA:194,0 -DA:197,0 -DA:200,0 -DA:203,0 -DA:206,0 -DA:209,0 +DA:190,0 +DA:193,0 +DA:195,0 +DA:198,0 +DA:201,0 +DA:204,0 +DA:207,0 +DA:210,0 DA:212,0 DA:215,0 DA:218,0 DA:221,0 DA:224,0 DA:227,0 -DA:229,0 -DA:232,0 -DA:235,0 -DA:238,0 -DA:241,0 -DA:244,0 -DA:247,0 -DA:250,0 -DA:253,0 -DA:256,0 -DA:259,0 -DA:262,0 -DA:265,0 -DA:268,0 -DA:271,0 -DA:274,0 -DA:277,0 -DA:280,0 -DA:283,0 -DA:286,0 -DA:289,0 -DA:292,0 -DA:295,0 +DA:230,0 +DA:233,0 +DA:236,0 +DA:239,0 +DA:242,0 +DA:245,0 +DA:248,0 +DA:251,0 +DA:254,0 +DA:257,0 +DA:260,0 +DA:263,0 +DA:266,0 +DA:269,0 +DA:272,0 +DA:275,0 +DA:278,0 +DA:281,0 +DA:284,0 +DA:287,0 +DA:290,0 +DA:293,0 +DA:296,0 DA:298,0 DA:301,0 DA:304,0 @@ -757,118 +828,280 @@ DA:361,0 DA:364,0 DA:367,0 DA:370,0 -DA:372,0 -DA:375,0 -DA:377,0 -DA:380,0 -DA:383,0 -DA:386,0 -DA:389,0 -DA:392,0 -LF:133 +DA:373,0 +DA:376,0 +DA:379,0 +DA:382,0 +DA:385,0 +DA:388,0 +DA:391,0 +DA:394,0 +DA:397,0 +DA:400,0 +DA:403,0 +DA:406,0 +DA:409,0 +DA:412,0 +DA:415,0 +DA:418,0 +DA:421,0 +DA:424,0 +DA:427,0 +DA:430,0 +DA:433,0 +DA:436,0 +DA:439,0 +DA:442,0 +DA:445,0 +DA:448,0 +DA:450,0 +DA:453,0 +DA:456,0 +DA:458,0 +DA:461,0 +DA:464,0 +DA:467,0 +DA:470,0 +DA:473,0 +DA:476,0 +DA:479,0 +DA:482,0 +DA:485,0 +DA:488,0 +DA:491,0 +DA:494,0 +DA:497,0 +DA:500,0 +DA:503,0 +DA:506,0 +DA:509,0 +DA:512,0 +DA:515,0 +DA:518,0 +DA:521,0 +DA:524,0 +DA:527,0 +DA:530,0 +DA:533,0 +DA:536,0 +DA:539,0 +DA:542,0 +DA:545,0 +DA:548,0 +DA:550,0 +DA:553,0 +DA:556,0 +DA:559,0 +DA:562,0 +DA:565,0 +DA:568,0 +DA:571,0 +DA:574,0 +DA:577,0 +DA:580,0 +DA:583,0 +DA:586,0 +DA:589,0 +DA:592,0 +DA:594,0 +DA:597,0 +DA:599,0 +DA:602,0 +DA:604,0 +DA:607,0 +DA:609,0 +DA:612,0 +DA:614,0 +DA:616,0 +DA:617,0 +DA:621,0 +DA:624,0 +DA:626,0 +DA:628,0 +DA:629,0 +DA:633,0 +DA:636,0 +DA:638,0 +DA:640,0 +DA:645,0 +DA:648,0 +DA:650,0 +DA:652,0 +DA:653,0 +DA:657,0 +DA:660,0 +DA:663,0 +DA:666,0 +DA:669,0 +DA:672,0 +DA:675,0 +DA:678,0 +DA:681,0 +DA:684,0 +DA:687,0 +DA:690,0 +DA:693,0 +DA:696,0 +DA:699,0 +DA:702,0 +DA:705,0 +DA:708,0 +DA:711,0 +DA:714,0 +DA:717,0 +DA:720,0 +DA:723,0 +DA:726,0 +DA:729,0 +DA:732,0 +DA:735,0 +DA:738,0 +DA:740,0 +DA:743,0 +DA:745,0 +DA:748,0 +DA:751,0 +DA:754,0 +DA:757,0 +DA:760,0 +DA:763,0 +DA:766,0 +DA:769,0 +DA:772,0 +DA:775,0 +DA:778,0 +DA:781,0 +DA:784,0 +DA:787,0 +DA:789,0 +DA:792,0 +DA:795,0 +DA:797,0 +DA:800,0 +DA:803,0 +DA:806,0 +DA:808,0 +DA:810,0 +DA:815,0 +DA:818,0 +DA:821,0 +DA:824,0 +DA:827,0 +DA:830,0 +DA:833,0 +DA:836,0 +DA:839,0 +DA:842,0 +DA:845,0 +DA:848,0 +DA:851,0 +DA:854,0 +DA:857,0 +DA:860,0 +LF:295 LH:0 end_of_record -SF:lib\l10n\app_localizations_fr.dart -DA:5,0 +SF:lib\l10n\app_localizations_en.dart DA:7,0 -DA:10,0 -DA:13,0 -DA:16,0 -DA:19,0 -DA:22,0 -DA:25,0 -DA:28,0 -DA:31,0 -DA:34,0 -DA:37,0 -DA:40,0 -DA:43,0 -DA:46,0 -DA:49,0 -DA:52,0 -DA:55,0 -DA:58,0 -DA:61,0 -DA:64,0 -DA:67,0 -DA:70,0 +DA:9,0 +DA:12,0 +DA:15,0 +DA:18,0 +DA:21,0 +DA:24,0 +DA:27,0 +DA:30,0 +DA:33,0 +DA:36,0 +DA:39,0 +DA:42,0 +DA:45,0 +DA:48,0 +DA:51,0 +DA:54,0 +DA:57,0 +DA:60,0 +DA:63,0 +DA:66,0 +DA:69,0 DA:72,0 DA:75,0 DA:78,0 DA:81,0 -DA:83,0 -DA:86,0 -DA:89,0 -DA:92,0 -DA:95,0 -DA:98,0 -DA:101,0 -DA:104,0 +DA:84,0 +DA:87,0 +DA:90,0 +DA:93,0 +DA:96,0 +DA:99,0 +DA:102,0 +DA:105,0 DA:107,0 DA:110,0 DA:113,0 DA:116,0 DA:119,0 DA:122,0 -DA:125,0 -DA:128,0 -DA:131,0 +DA:124,0 +DA:127,0 +DA:130,0 DA:133,0 DA:136,0 DA:139,0 -DA:141,0 -DA:144,0 -DA:146,0 -DA:149,0 -DA:152,0 -DA:155,0 -DA:158,0 -DA:161,0 -DA:164,0 -DA:167,0 -DA:170,0 -DA:173,0 -DA:176,0 -DA:179,0 +DA:142,0 +DA:145,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:177,0 +DA:180,0 DA:182,0 DA:185,0 DA:188,0 -DA:191,0 -DA:194,0 -DA:197,0 -DA:200,0 -DA:203,0 -DA:206,0 -DA:209,0 +DA:190,0 +DA:193,0 +DA:195,0 +DA:198,0 +DA:201,0 +DA:204,0 +DA:207,0 +DA:210,0 DA:212,0 DA:215,0 DA:218,0 DA:221,0 DA:224,0 DA:227,0 -DA:229,0 -DA:232,0 -DA:235,0 -DA:238,0 -DA:241,0 -DA:244,0 -DA:247,0 -DA:250,0 -DA:253,0 -DA:256,0 -DA:259,0 -DA:262,0 -DA:265,0 -DA:268,0 -DA:271,0 -DA:274,0 -DA:277,0 -DA:280,0 -DA:283,0 -DA:286,0 -DA:289,0 -DA:292,0 -DA:295,0 +DA:230,0 +DA:233,0 +DA:236,0 +DA:239,0 +DA:242,0 +DA:245,0 +DA:248,0 +DA:251,0 +DA:254,0 +DA:257,0 +DA:260,0 +DA:263,0 +DA:266,0 +DA:269,0 +DA:272,0 +DA:275,0 +DA:278,0 +DA:281,0 +DA:284,0 +DA:287,0 +DA:290,0 +DA:293,0 +DA:296,0 DA:298,0 DA:301,0 DA:304,0 @@ -894,117 +1127,280 @@ DA:361,0 DA:364,0 DA:367,0 DA:370,0 -DA:372,0 -DA:375,0 -DA:377,0 -DA:380,0 -DA:383,0 -DA:386,0 -DA:389,0 -DA:392,0 -LF:133 +DA:373,0 +DA:376,0 +DA:379,0 +DA:382,0 +DA:385,0 +DA:388,0 +DA:391,0 +DA:394,0 +DA:397,0 +DA:400,0 +DA:403,0 +DA:406,0 +DA:409,0 +DA:412,0 +DA:415,0 +DA:418,0 +DA:421,0 +DA:424,0 +DA:427,0 +DA:430,0 +DA:433,0 +DA:436,0 +DA:439,0 +DA:442,0 +DA:445,0 +DA:448,0 +DA:450,0 +DA:453,0 +DA:456,0 +DA:458,0 +DA:461,0 +DA:464,0 +DA:467,0 +DA:470,0 +DA:473,0 +DA:476,0 +DA:479,0 +DA:482,0 +DA:485,0 +DA:488,0 +DA:491,0 +DA:494,0 +DA:497,0 +DA:500,0 +DA:503,0 +DA:506,0 +DA:509,0 +DA:512,0 +DA:515,0 +DA:518,0 +DA:521,0 +DA:524,0 +DA:527,0 +DA:530,0 +DA:533,0 +DA:536,0 +DA:539,0 +DA:542,0 +DA:545,0 +DA:548,0 +DA:550,0 +DA:553,0 +DA:556,0 +DA:559,0 +DA:562,0 +DA:565,0 +DA:568,0 +DA:571,0 +DA:574,0 +DA:577,0 +DA:580,0 +DA:583,0 +DA:586,0 +DA:589,0 +DA:592,0 +DA:594,0 +DA:597,0 +DA:599,0 +DA:602,0 +DA:604,0 +DA:607,0 +DA:609,0 +DA:612,0 +DA:614,0 +DA:616,0 +DA:617,0 +DA:621,0 +DA:624,0 +DA:626,0 +DA:628,0 +DA:629,0 +DA:633,0 +DA:636,0 +DA:638,0 +DA:640,0 +DA:645,0 +DA:648,0 +DA:650,0 +DA:652,0 +DA:653,0 +DA:657,0 +DA:660,0 +DA:663,0 +DA:666,0 +DA:669,0 +DA:672,0 +DA:675,0 +DA:678,0 +DA:681,0 +DA:684,0 +DA:687,0 +DA:690,0 +DA:693,0 +DA:696,0 +DA:699,0 +DA:702,0 +DA:705,0 +DA:708,0 +DA:711,0 +DA:714,0 +DA:717,0 +DA:720,0 +DA:723,0 +DA:726,0 +DA:729,0 +DA:732,0 +DA:735,0 +DA:738,0 +DA:740,0 +DA:743,0 +DA:745,0 +DA:748,0 +DA:751,0 +DA:754,0 +DA:757,0 +DA:760,0 +DA:763,0 +DA:766,0 +DA:769,0 +DA:772,0 +DA:775,0 +DA:778,0 +DA:781,0 +DA:784,0 +DA:787,0 +DA:789,0 +DA:792,0 +DA:795,0 +DA:797,0 +DA:800,0 +DA:803,0 +DA:806,0 +DA:808,0 +DA:810,0 +DA:815,0 +DA:818,0 +DA:821,0 +DA:824,0 +DA:827,0 +DA:830,0 +DA:833,0 +DA:836,0 +DA:839,0 +DA:842,0 +DA:845,0 +DA:848,0 +DA:851,0 +DA:854,0 +DA:857,0 +DA:860,0 +LF:295 LH:0 end_of_record -SF:lib\l10n\app_localizations_nl.dart -DA:5,0 +SF:lib\l10n\app_localizations_es.dart DA:7,0 -DA:10,0 -DA:13,0 -DA:16,0 -DA:19,0 -DA:22,0 -DA:25,0 -DA:28,0 -DA:31,0 -DA:34,0 -DA:37,0 -DA:40,0 -DA:43,0 -DA:46,0 -DA:49,0 -DA:52,0 -DA:55,0 -DA:58,0 -DA:61,0 -DA:64,0 -DA:67,0 -DA:70,0 +DA:9,0 +DA:12,0 +DA:15,0 +DA:18,0 +DA:21,0 +DA:24,0 +DA:27,0 +DA:30,0 +DA:33,0 +DA:36,0 +DA:39,0 +DA:42,0 +DA:45,0 +DA:48,0 +DA:51,0 +DA:54,0 +DA:57,0 +DA:60,0 +DA:63,0 +DA:66,0 +DA:69,0 DA:72,0 DA:75,0 DA:78,0 DA:81,0 -DA:83,0 -DA:86,0 -DA:89,0 -DA:92,0 -DA:95,0 -DA:98,0 -DA:101,0 -DA:104,0 +DA:84,0 +DA:87,0 +DA:90,0 +DA:93,0 +DA:96,0 +DA:99,0 +DA:102,0 +DA:105,0 DA:107,0 DA:110,0 DA:113,0 DA:116,0 DA:119,0 DA:122,0 -DA:125,0 -DA:128,0 -DA:131,0 +DA:124,0 +DA:127,0 +DA:130,0 DA:133,0 DA:136,0 DA:139,0 -DA:144,0 -DA:146,0 -DA:149,0 -DA:152,0 -DA:155,0 -DA:158,0 -DA:161,0 -DA:164,0 -DA:167,0 -DA:170,0 -DA:173,0 -DA:176,0 -DA:179,0 +DA:142,0 +DA:145,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:177,0 +DA:180,0 DA:182,0 DA:185,0 DA:188,0 -DA:191,0 -DA:194,0 -DA:197,0 -DA:200,0 -DA:203,0 -DA:206,0 -DA:209,0 +DA:190,0 +DA:193,0 +DA:195,0 +DA:198,0 +DA:201,0 +DA:204,0 +DA:207,0 +DA:210,0 DA:212,0 DA:215,0 DA:218,0 DA:221,0 DA:224,0 DA:227,0 -DA:229,0 -DA:232,0 -DA:235,0 -DA:238,0 -DA:241,0 -DA:244,0 -DA:247,0 -DA:250,0 -DA:253,0 -DA:256,0 -DA:259,0 -DA:262,0 -DA:265,0 -DA:268,0 -DA:271,0 -DA:274,0 -DA:277,0 -DA:280,0 -DA:283,0 -DA:286,0 -DA:289,0 -DA:292,0 -DA:295,0 +DA:230,0 +DA:233,0 +DA:236,0 +DA:239,0 +DA:242,0 +DA:245,0 +DA:248,0 +DA:251,0 +DA:254,0 +DA:257,0 +DA:260,0 +DA:263,0 +DA:266,0 +DA:269,0 +DA:272,0 +DA:275,0 +DA:278,0 +DA:281,0 +DA:284,0 +DA:287,0 +DA:290,0 +DA:293,0 +DA:296,0 DA:298,0 DA:301,0 DA:304,0 @@ -1030,131 +1426,294 @@ DA:361,0 DA:364,0 DA:367,0 DA:370,0 -DA:372,0 -DA:375,0 -DA:377,0 -DA:380,0 -DA:383,0 -DA:386,0 -DA:389,0 -DA:392,0 -LF:132 +DA:373,0 +DA:376,0 +DA:379,0 +DA:382,0 +DA:385,0 +DA:388,0 +DA:391,0 +DA:394,0 +DA:397,0 +DA:400,0 +DA:403,0 +DA:406,0 +DA:409,0 +DA:412,0 +DA:415,0 +DA:418,0 +DA:421,0 +DA:424,0 +DA:427,0 +DA:430,0 +DA:433,0 +DA:436,0 +DA:439,0 +DA:442,0 +DA:445,0 +DA:448,0 +DA:450,0 +DA:453,0 +DA:456,0 +DA:458,0 +DA:461,0 +DA:464,0 +DA:467,0 +DA:470,0 +DA:473,0 +DA:476,0 +DA:479,0 +DA:482,0 +DA:485,0 +DA:488,0 +DA:491,0 +DA:494,0 +DA:497,0 +DA:500,0 +DA:503,0 +DA:506,0 +DA:509,0 +DA:512,0 +DA:515,0 +DA:518,0 +DA:521,0 +DA:524,0 +DA:527,0 +DA:530,0 +DA:533,0 +DA:536,0 +DA:539,0 +DA:542,0 +DA:545,0 +DA:548,0 +DA:550,0 +DA:553,0 +DA:556,0 +DA:559,0 +DA:562,0 +DA:565,0 +DA:568,0 +DA:571,0 +DA:574,0 +DA:577,0 +DA:580,0 +DA:583,0 +DA:586,0 +DA:589,0 +DA:592,0 +DA:594,0 +DA:597,0 +DA:599,0 +DA:602,0 +DA:604,0 +DA:607,0 +DA:609,0 +DA:612,0 +DA:614,0 +DA:616,0 +DA:617,0 +DA:621,0 +DA:624,0 +DA:626,0 +DA:628,0 +DA:629,0 +DA:633,0 +DA:636,0 +DA:638,0 +DA:640,0 +DA:645,0 +DA:648,0 +DA:650,0 +DA:652,0 +DA:653,0 +DA:657,0 +DA:660,0 +DA:663,0 +DA:666,0 +DA:669,0 +DA:672,0 +DA:675,0 +DA:678,0 +DA:681,0 +DA:684,0 +DA:687,0 +DA:690,0 +DA:693,0 +DA:696,0 +DA:699,0 +DA:702,0 +DA:705,0 +DA:708,0 +DA:711,0 +DA:714,0 +DA:717,0 +DA:720,0 +DA:723,0 +DA:726,0 +DA:729,0 +DA:732,0 +DA:735,0 +DA:738,0 +DA:740,0 +DA:743,0 +DA:745,0 +DA:748,0 +DA:751,0 +DA:754,0 +DA:757,0 +DA:760,0 +DA:763,0 +DA:766,0 +DA:769,0 +DA:772,0 +DA:775,0 +DA:778,0 +DA:781,0 +DA:784,0 +DA:787,0 +DA:789,0 +DA:792,0 +DA:795,0 +DA:797,0 +DA:800,0 +DA:803,0 +DA:806,0 +DA:808,0 +DA:810,0 +DA:815,0 +DA:818,0 +DA:821,0 +DA:824,0 +DA:827,0 +DA:830,0 +DA:833,0 +DA:836,0 +DA:839,0 +DA:842,0 +DA:845,0 +DA:848,0 +DA:851,0 +DA:854,0 +DA:857,0 +DA:860,0 +LF:295 LH:0 end_of_record -SF:lib\l10n\app_localizations_pl.dart -DA:5,0 +SF:lib\l10n\app_localizations_fr.dart DA:7,0 -DA:10,0 -DA:13,0 -DA:16,0 -DA:19,0 -DA:22,0 -DA:25,0 -DA:28,0 -DA:31,0 -DA:34,0 -DA:37,0 -DA:40,0 -DA:43,0 -DA:46,0 -DA:49,0 -DA:52,0 -DA:55,0 -DA:58,0 -DA:61,0 -DA:64,0 -DA:67,0 -DA:70,0 +DA:9,0 +DA:12,0 +DA:15,0 +DA:18,0 +DA:21,0 +DA:24,0 +DA:27,0 +DA:30,0 +DA:33,0 +DA:36,0 +DA:39,0 +DA:42,0 +DA:45,0 +DA:48,0 +DA:51,0 +DA:54,0 +DA:57,0 +DA:60,0 +DA:63,0 +DA:66,0 +DA:69,0 DA:72,0 DA:75,0 DA:78,0 DA:81,0 -DA:83,0 -DA:86,0 -DA:89,0 -DA:92,0 -DA:95,0 -DA:98,0 -DA:101,0 -DA:104,0 +DA:84,0 +DA:87,0 +DA:90,0 +DA:93,0 +DA:96,0 +DA:99,0 +DA:102,0 +DA:105,0 DA:107,0 DA:110,0 DA:113,0 DA:116,0 DA:119,0 DA:122,0 -DA:125,0 -DA:128,0 -DA:131,0 +DA:124,0 +DA:127,0 +DA:130,0 DA:133,0 DA:136,0 DA:139,0 -DA:144,0 -DA:146,0 -DA:149,0 -DA:152,0 -DA:155,0 -DA:158,0 -DA:161,0 -DA:164,0 -DA:167,0 -DA:170,0 -DA:173,0 -DA:176,0 -DA:179,0 +DA:142,0 +DA:145,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:177,0 +DA:180,0 DA:182,0 DA:185,0 DA:188,0 -DA:191,0 -DA:194,0 -DA:197,0 -DA:200,0 -DA:203,0 -DA:206,0 -DA:209,0 +DA:190,0 +DA:193,0 +DA:195,0 +DA:198,0 +DA:201,0 +DA:204,0 +DA:207,0 +DA:210,0 DA:212,0 DA:215,0 DA:218,0 DA:221,0 DA:224,0 DA:227,0 -DA:229,0 -DA:232,0 -DA:235,0 -DA:238,0 -DA:241,0 -DA:244,0 -DA:247,0 -DA:250,0 -DA:253,0 -DA:256,0 -DA:259,0 -DA:262,0 -DA:265,0 -DA:268,0 -DA:271,0 -DA:274,0 -DA:277,0 -DA:280,0 -DA:283,0 -DA:286,0 -DA:289,0 -DA:292,0 -DA:295,0 -DA:298,0 -DA:301,0 -DA:304,0 -DA:307,0 -DA:310,0 -DA:313,0 -DA:316,0 -DA:319,0 -DA:322,0 -DA:325,0 -DA:328,0 -DA:331,0 -DA:334,0 -DA:337,0 +DA:230,0 +DA:233,0 +DA:236,0 +DA:239,0 +DA:242,0 +DA:245,0 +DA:248,0 +DA:251,0 +DA:254,0 +DA:257,0 +DA:260,0 +DA:263,0 +DA:266,0 +DA:269,0 +DA:272,0 +DA:275,0 +DA:278,0 +DA:281,0 +DA:284,0 +DA:287,0 +DA:290,0 +DA:293,0 +DA:296,0 +DA:298,0 +DA:301,0 +DA:304,0 +DA:307,0 +DA:310,0 +DA:313,0 +DA:316,0 +DA:319,0 +DA:322,0 +DA:325,0 +DA:328,0 +DA:331,0 +DA:334,0 +DA:337,0 DA:340,0 DA:343,0 DA:346,0 @@ -1166,1995 +1725,9865 @@ DA:361,0 DA:364,0 DA:367,0 DA:370,0 -DA:372,0 -DA:375,0 -DA:377,0 -DA:380,0 -DA:383,0 -DA:386,0 -DA:389,0 -DA:392,0 -LF:132 -LH:0 -end_of_record -SF:lib\model\platform_info\platform_info_imp\dummy_platform_info.dart -DA:5,0 -DA:8,0 -DA:11,0 -DA:14,0 -DA:17,0 -DA:20,0 -LF:6 +DA:373,0 +DA:376,0 +DA:379,0 +DA:382,0 +DA:385,0 +DA:388,0 +DA:391,0 +DA:394,0 +DA:397,0 +DA:400,0 +DA:403,0 +DA:406,0 +DA:409,0 +DA:412,0 +DA:415,0 +DA:418,0 +DA:421,0 +DA:424,0 +DA:427,0 +DA:430,0 +DA:433,0 +DA:436,0 +DA:439,0 +DA:442,0 +DA:445,0 +DA:448,0 +DA:450,0 +DA:453,0 +DA:456,0 +DA:458,0 +DA:461,0 +DA:464,0 +DA:467,0 +DA:470,0 +DA:473,0 +DA:476,0 +DA:479,0 +DA:482,0 +DA:485,0 +DA:488,0 +DA:491,0 +DA:494,0 +DA:497,0 +DA:500,0 +DA:503,0 +DA:506,0 +DA:509,0 +DA:512,0 +DA:515,0 +DA:518,0 +DA:521,0 +DA:524,0 +DA:527,0 +DA:530,0 +DA:533,0 +DA:536,0 +DA:539,0 +DA:542,0 +DA:545,0 +DA:548,0 +DA:550,0 +DA:553,0 +DA:556,0 +DA:559,0 +DA:562,0 +DA:565,0 +DA:568,0 +DA:571,0 +DA:574,0 +DA:577,0 +DA:580,0 +DA:583,0 +DA:586,0 +DA:589,0 +DA:592,0 +DA:594,0 +DA:597,0 +DA:599,0 +DA:602,0 +DA:604,0 +DA:607,0 +DA:609,0 +DA:612,0 +DA:614,0 +DA:616,0 +DA:617,0 +DA:621,0 +DA:624,0 +DA:626,0 +DA:628,0 +DA:629,0 +DA:633,0 +DA:636,0 +DA:638,0 +DA:640,0 +DA:645,0 +DA:648,0 +DA:650,0 +DA:652,0 +DA:653,0 +DA:657,0 +DA:660,0 +DA:663,0 +DA:666,0 +DA:669,0 +DA:672,0 +DA:675,0 +DA:678,0 +DA:681,0 +DA:684,0 +DA:687,0 +DA:690,0 +DA:693,0 +DA:696,0 +DA:699,0 +DA:702,0 +DA:705,0 +DA:708,0 +DA:711,0 +DA:714,0 +DA:717,0 +DA:720,0 +DA:723,0 +DA:726,0 +DA:729,0 +DA:732,0 +DA:735,0 +DA:738,0 +DA:740,0 +DA:743,0 +DA:745,0 +DA:748,0 +DA:751,0 +DA:754,0 +DA:757,0 +DA:760,0 +DA:763,0 +DA:766,0 +DA:769,0 +DA:772,0 +DA:775,0 +DA:778,0 +DA:781,0 +DA:784,0 +DA:787,0 +DA:789,0 +DA:792,0 +DA:795,0 +DA:797,0 +DA:800,0 +DA:803,0 +DA:806,0 +DA:808,0 +DA:810,0 +DA:815,0 +DA:818,0 +DA:821,0 +DA:824,0 +DA:827,0 +DA:830,0 +DA:833,0 +DA:836,0 +DA:839,0 +DA:842,0 +DA:845,0 +DA:848,0 +DA:851,0 +DA:854,0 +DA:857,0 +DA:860,0 +LF:295 LH:0 end_of_record -SF:lib\model\push_request.dart -DA:20,4 -DA:33,16 -DA:34,13 -DA:37,1 -DA:49,1 -DA:50,1 -DA:51,1 -DA:52,1 -DA:53,1 -DA:54,1 -DA:55,1 -DA:56,1 -DA:57,1 -DA:58,1 -DA:59,0 -DA:63,4 -DA:64,7 -DA:66,1 -DA:67,2 -DA:69,2 -DA:71,6 -DA:72,8 -DA:73,4 -DA:74,4 -DA:77,2 -DA:79,2 -LF:26 -LH:25 -end_of_record -SF:lib\model\push_request.g.dart -DA:9,2 -DA:10,1 -DA:11,1 -DA:12,2 -DA:13,1 -DA:14,1 -DA:15,1 -DA:16,2 -DA:17,1 -DA:18,1 -DA:19,1 -DA:22,1 -DA:23,1 -DA:24,1 -DA:25,1 -DA:26,1 -DA:27,2 -DA:28,1 -DA:29,1 -DA:30,2 -DA:31,1 -DA:32,1 -DA:33,1 -LF:23 -LH:23 -end_of_record -SF:lib\utils\riverpod_providers.dart -DA:28,0 -DA:29,0 -DA:31,0 -DA:32,0 +SF:lib\l10n\app_localizations_nl.dart +DA:7,0 +DA:9,0 +DA:12,0 +DA:15,0 +DA:18,0 +DA:21,0 +DA:24,0 +DA:27,0 +DA:30,0 DA:33,0 -DA:35,0 DA:36,0 -DA:38,0 DA:39,0 -DA:43,0 -DA:44,0 +DA:42,0 DA:45,0 -DA:46,0 -DA:47,0 DA:48,0 DA:51,0 DA:54,0 -DA:58,0 -DA:59,0 +DA:57,0 DA:60,0 -DA:62,0 DA:63,0 -DA:64,0 -DA:67,0 -DA:68,0 +DA:66,0 DA:69,0 -DA:70,0 -DA:74,0 -DA:77,0 -DA:83,0 +DA:72,0 +DA:75,0 +DA:78,0 +DA:81,0 DA:84,0 -DA:85,0 -DA:86,0 +DA:87,0 DA:90,0 -DA:91,0 -DA:94,0 -DA:95,0 +DA:93,0 DA:96,0 -DA:97,0 -DA:100,0 -DA:101,0 +DA:99,0 DA:102,0 -DA:103,0 -DA:104,0 DA:105,0 -DA:108,0 -DA:109,0 +DA:107,0 +DA:110,0 +DA:113,0 DA:116,0 -DA:118,0 DA:119,0 DA:122,0 -DA:123,0 DA:124,0 -DA:128,0 +DA:127,0 DA:130,0 -LF:55 -LH:0 -end_of_record -SF:lib\model\push_request_queue.dart -DA:10,4 -DA:15,4 -DA:16,8 -DA:17,4 -DA:20,2 -DA:21,2 -DA:22,0 -DA:25,2 -DA:28,0 -DA:30,0 -DA:32,3 -DA:34,0 -DA:36,6 -DA:38,2 -DA:39,12 -DA:41,4 -DA:44,3 -DA:46,3 -DA:48,0 -DA:50,12 -DA:52,5 -DA:54,6 -DA:56,1 -DA:58,2 -DA:61,2 -DA:62,14 -DA:64,2 -DA:65,6 -DA:67,7 -DA:68,3 -DA:74,0 -DA:75,0 -DA:77,4 -DA:79,4 -LF:34 -LH:27 -end_of_record -SF:lib\model\push_request_queue.g.dart -DA:9,2 -DA:10,2 -DA:11,4 -DA:12,4 -DA:13,2 -DA:15,2 -DA:16,2 -DA:17,2 -LF:8 -LH:8 -end_of_record -SF:lib\model\tokens\push_token.dart -DA:18,2 -DA:35,5 -DA:36,4 -DA:37,5 -DA:38,4 -DA:39,0 -DA:40,4 -DA:42,2 -DA:43,4 -DA:44,6 -DA:45,6 -DA:48,2 -DA:49,14 -DA:50,4 -DA:52,4 -DA:58,2 -DA:59,7 -DA:60,2 -DA:63,3 -DA:88,3 -DA:89,2 -DA:90,6 -DA:92,2 -DA:117,2 -DA:118,2 -DA:119,2 -DA:120,2 -DA:121,2 -DA:122,2 -DA:123,2 -DA:124,2 -DA:125,2 -DA:126,2 -DA:127,2 -DA:128,2 -DA:129,2 -DA:130,2 -DA:131,2 -DA:132,2 -DA:133,2 -DA:134,2 -DA:135,2 -DA:136,2 -DA:137,3 -DA:141,2 -DA:142,7 -DA:144,0 +DA:133,0 +DA:136,0 +DA:139,0 +DA:142,0 DA:145,0 -DA:147,1 -DA:149,2 -DA:150,1 -DA:151,2 -DA:152,1 -DA:153,2 -DA:154,1 -DA:155,1 -DA:156,1 -DA:157,1 -DA:158,1 -DA:159,1 -DA:160,1 -DA:161,1 -DA:164,1 -DA:167,1 -DA:168,1 -DA:169,1 -DA:170,1 -DA:171,1 -DA:172,1 -DA:173,5 -DA:174,1 -DA:175,1 -DA:176,1 -DA:177,1 -DA:178,1 -DA:181,0 -DA:186,1 -DA:187,1 -DA:188,2 -DA:189,1 -DA:190,1 -DA:191,1 -DA:192,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:177,0 +DA:180,0 +DA:182,0 +DA:185,0 +DA:188,0 DA:193,0 -DA:194,0 -DA:196,1 -DA:199,2 -LF:87 -LH:80 -end_of_record -SF:lib\model\tokens\push_token.g.dart -DA:9,2 -DA:10,1 -DA:11,1 -DA:12,1 -DA:13,1 -DA:14,3 -DA:15,1 -DA:17,2 -DA:18,1 -DA:19,1 -DA:20,1 -DA:21,1 -DA:22,1 -DA:23,1 -DA:24,1 -DA:25,1 -DA:26,1 -DA:28,1 -DA:29,1 -DA:30,1 -DA:32,1 -DA:33,1 -DA:34,1 -DA:35,1 -DA:36,1 -DA:37,1 -DA:38,1 -DA:39,1 -DA:42,2 -DA:43,1 -DA:44,1 -DA:45,1 -DA:46,1 -DA:47,1 -DA:48,1 -DA:49,1 -DA:50,1 -DA:51,1 -DA:52,2 -DA:53,1 -DA:54,1 -DA:55,1 -DA:56,2 -DA:57,1 -DA:58,2 -DA:59,1 -DA:60,1 -DA:61,1 -DA:62,1 -DA:63,1 -LF:50 -LH:50 +DA:195,0 +DA:198,0 +DA:201,0 +DA:204,0 +DA:207,0 +DA:210,0 +DA:212,0 +DA:215,0 +DA:218,0 +DA:221,0 +DA:224,0 +DA:227,0 +DA:230,0 +DA:233,0 +DA:236,0 +DA:239,0 +DA:242,0 +DA:245,0 +DA:248,0 +DA:251,0 +DA:254,0 +DA:257,0 +DA:260,0 +DA:263,0 +DA:266,0 +DA:269,0 +DA:272,0 +DA:275,0 +DA:278,0 +DA:281,0 +DA:284,0 +DA:287,0 +DA:290,0 +DA:293,0 +DA:296,0 +DA:298,0 +DA:301,0 +DA:304,0 +DA:307,0 +DA:310,0 +DA:313,0 +DA:316,0 +DA:319,0 +DA:322,0 +DA:325,0 +DA:328,0 +DA:331,0 +DA:334,0 +DA:337,0 +DA:340,0 +DA:343,0 +DA:346,0 +DA:349,0 +DA:352,0 +DA:355,0 +DA:358,0 +DA:361,0 +DA:364,0 +DA:367,0 +DA:370,0 +DA:373,0 +DA:376,0 +DA:379,0 +DA:382,0 +DA:385,0 +DA:388,0 +DA:391,0 +DA:394,0 +DA:397,0 +DA:400,0 +DA:403,0 +DA:406,0 +DA:409,0 +DA:412,0 +DA:415,0 +DA:418,0 +DA:421,0 +DA:424,0 +DA:427,0 +DA:430,0 +DA:433,0 +DA:436,0 +DA:439,0 +DA:442,0 +DA:445,0 +DA:448,0 +DA:450,0 +DA:453,0 +DA:456,0 +DA:458,0 +DA:461,0 +DA:464,0 +DA:467,0 +DA:470,0 +DA:473,0 +DA:476,0 +DA:479,0 +DA:482,0 +DA:485,0 +DA:488,0 +DA:491,0 +DA:494,0 +DA:497,0 +DA:500,0 +DA:503,0 +DA:506,0 +DA:509,0 +DA:512,0 +DA:515,0 +DA:518,0 +DA:521,0 +DA:524,0 +DA:527,0 +DA:530,0 +DA:533,0 +DA:536,0 +DA:539,0 +DA:542,0 +DA:545,0 +DA:548,0 +DA:550,0 +DA:553,0 +DA:556,0 +DA:559,0 +DA:562,0 +DA:565,0 +DA:568,0 +DA:571,0 +DA:574,0 +DA:577,0 +DA:580,0 +DA:583,0 +DA:586,0 +DA:589,0 +DA:592,0 +DA:594,0 +DA:597,0 +DA:599,0 +DA:602,0 +DA:604,0 +DA:607,0 +DA:609,0 +DA:612,0 +DA:614,0 +DA:616,0 +DA:617,0 +DA:621,0 +DA:624,0 +DA:626,0 +DA:628,0 +DA:629,0 +DA:633,0 +DA:636,0 +DA:638,0 +DA:640,0 +DA:645,0 +DA:648,0 +DA:650,0 +DA:652,0 +DA:653,0 +DA:657,0 +DA:660,0 +DA:663,0 +DA:666,0 +DA:669,0 +DA:672,0 +DA:675,0 +DA:678,0 +DA:681,0 +DA:684,0 +DA:687,0 +DA:690,0 +DA:693,0 +DA:696,0 +DA:699,0 +DA:702,0 +DA:705,0 +DA:708,0 +DA:711,0 +DA:714,0 +DA:717,0 +DA:720,0 +DA:723,0 +DA:726,0 +DA:729,0 +DA:732,0 +DA:735,0 +DA:738,0 +DA:740,0 +DA:743,0 +DA:745,0 +DA:748,0 +DA:751,0 +DA:754,0 +DA:757,0 +DA:760,0 +DA:763,0 +DA:766,0 +DA:769,0 +DA:772,0 +DA:775,0 +DA:778,0 +DA:781,0 +DA:784,0 +DA:787,0 +DA:789,0 +DA:792,0 +DA:795,0 +DA:797,0 +DA:800,0 +DA:803,0 +DA:806,0 +DA:808,0 +DA:810,0 +DA:815,0 +DA:818,0 +DA:821,0 +DA:824,0 +DA:827,0 +DA:830,0 +DA:833,0 +DA:836,0 +DA:839,0 +DA:842,0 +DA:845,0 +DA:848,0 +DA:851,0 +DA:854,0 +DA:857,0 +DA:860,0 +LF:294 +LH:0 end_of_record -SF:lib\model\tokens\day_password_token.dart -DA:19,1 -DA:34,2 -DA:35,2 -DA:37,0 -DA:53,0 +SF:lib\l10n\app_localizations_pl.dart +DA:7,0 +DA:9,0 +DA:12,0 +DA:15,0 +DA:18,0 +DA:21,0 +DA:24,0 +DA:27,0 +DA:30,0 +DA:33,0 +DA:36,0 +DA:39,0 +DA:42,0 +DA:45,0 +DA:48,0 +DA:51,0 DA:54,0 -DA:55,0 -DA:56,0 DA:57,0 -DA:58,0 -DA:59,0 DA:60,0 -DA:61,0 -DA:62,0 DA:63,0 -DA:64,0 -DA:65,0 DA:66,0 -DA:67,0 -DA:70,1 -DA:71,1 -DA:72,1 -DA:73,2 -DA:74,1 -DA:75,2 -DA:76,2 -DA:80,0 +DA:69,0 +DA:72,0 +DA:75,0 +DA:78,0 DA:81,0 -DA:82,0 -DA:85,0 -DA:86,0 +DA:84,0 DA:87,0 -DA:89,0 -DA:92,0 +DA:90,0 DA:93,0 -DA:94,0 -DA:95,0 -DA:98,0 +DA:96,0 DA:99,0 -DA:100,0 -DA:101,0 DA:102,0 -DA:103,0 -DA:104,0 DA:105,0 -DA:106,0 DA:107,0 -DA:108,0 -DA:111,0 +DA:110,0 +DA:113,0 DA:116,0 -DA:117,0 DA:119,0 -DA:121,0 -LF:53 -LH:10 -end_of_record -SF:lib\model\tokens\day_password_token.g.dart -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:42,0 -DA:43,0 -DA:44,0 -LF:33 -LH:0 -end_of_record +DA:122,0 +DA:124,0 +DA:127,0 +DA:130,0 +DA:133,0 +DA:136,0 +DA:139,0 +DA:142,0 +DA:145,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:177,0 +DA:180,0 +DA:182,0 +DA:185,0 +DA:188,0 +DA:193,0 +DA:195,0 +DA:198,0 +DA:201,0 +DA:204,0 +DA:207,0 +DA:210,0 +DA:212,0 +DA:215,0 +DA:218,0 +DA:221,0 +DA:224,0 +DA:227,0 +DA:230,0 +DA:233,0 +DA:236,0 +DA:239,0 +DA:242,0 +DA:245,0 +DA:248,0 +DA:251,0 +DA:254,0 +DA:257,0 +DA:260,0 +DA:263,0 +DA:266,0 +DA:269,0 +DA:272,0 +DA:275,0 +DA:278,0 +DA:281,0 +DA:284,0 +DA:287,0 +DA:290,0 +DA:293,0 +DA:296,0 +DA:298,0 +DA:301,0 +DA:304,0 +DA:307,0 +DA:310,0 +DA:313,0 +DA:316,0 +DA:319,0 +DA:322,0 +DA:325,0 +DA:328,0 +DA:331,0 +DA:334,0 +DA:337,0 +DA:340,0 +DA:343,0 +DA:346,0 +DA:349,0 +DA:352,0 +DA:355,0 +DA:358,0 +DA:361,0 +DA:364,0 +DA:367,0 +DA:370,0 +DA:373,0 +DA:376,0 +DA:379,0 +DA:382,0 +DA:385,0 +DA:388,0 +DA:391,0 +DA:394,0 +DA:397,0 +DA:400,0 +DA:403,0 +DA:406,0 +DA:409,0 +DA:412,0 +DA:415,0 +DA:418,0 +DA:421,0 +DA:424,0 +DA:427,0 +DA:430,0 +DA:433,0 +DA:436,0 +DA:439,0 +DA:442,0 +DA:445,0 +DA:448,0 +DA:450,0 +DA:453,0 +DA:456,0 +DA:458,0 +DA:461,0 +DA:464,0 +DA:467,0 +DA:470,0 +DA:473,0 +DA:476,0 +DA:479,0 +DA:482,0 +DA:485,0 +DA:488,0 +DA:491,0 +DA:494,0 +DA:497,0 +DA:500,0 +DA:503,0 +DA:506,0 +DA:509,0 +DA:512,0 +DA:515,0 +DA:518,0 +DA:521,0 +DA:524,0 +DA:527,0 +DA:530,0 +DA:533,0 +DA:536,0 +DA:539,0 +DA:542,0 +DA:545,0 +DA:548,0 +DA:550,0 +DA:553,0 +DA:556,0 +DA:559,0 +DA:562,0 +DA:565,0 +DA:568,0 +DA:571,0 +DA:574,0 +DA:577,0 +DA:580,0 +DA:583,0 +DA:586,0 +DA:589,0 +DA:592,0 +DA:594,0 +DA:597,0 +DA:599,0 +DA:602,0 +DA:604,0 +DA:607,0 +DA:609,0 +DA:612,0 +DA:614,0 +DA:616,0 +DA:617,0 +DA:621,0 +DA:624,0 +DA:626,0 +DA:628,0 +DA:629,0 +DA:633,0 +DA:636,0 +DA:638,0 +DA:640,0 +DA:645,0 +DA:648,0 +DA:650,0 +DA:652,0 +DA:653,0 +DA:657,0 +DA:660,0 +DA:663,0 +DA:666,0 +DA:669,0 +DA:672,0 +DA:675,0 +DA:678,0 +DA:681,0 +DA:684,0 +DA:687,0 +DA:690,0 +DA:693,0 +DA:696,0 +DA:699,0 +DA:702,0 +DA:705,0 +DA:708,0 +DA:711,0 +DA:714,0 +DA:717,0 +DA:720,0 +DA:723,0 +DA:726,0 +DA:729,0 +DA:732,0 +DA:735,0 +DA:738,0 +DA:740,0 +DA:743,0 +DA:745,0 +DA:748,0 +DA:751,0 +DA:754,0 +DA:757,0 +DA:760,0 +DA:763,0 +DA:766,0 +DA:769,0 +DA:772,0 +DA:775,0 +DA:778,0 +DA:781,0 +DA:784,0 +DA:787,0 +DA:789,0 +DA:792,0 +DA:795,0 +DA:797,0 +DA:800,0 +DA:803,0 +DA:806,0 +DA:808,0 +DA:810,0 +DA:815,0 +DA:818,0 +DA:821,0 +DA:824,0 +DA:827,0 +DA:830,0 +DA:833,0 +DA:836,0 +DA:839,0 +DA:842,0 +DA:845,0 +DA:848,0 +DA:851,0 +DA:854,0 +DA:857,0 +DA:860,0 +LF:294 +LH:0 +end_of_record +SF:lib\mains\main_netknights.dart +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:59,0 +DA:61,0 +DA:63,0 +DA:65,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:73,0 +DA:74,0 +DA:79,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +LF:40 +LH:0 +end_of_record +SF:lib\utils\app_customizer.dart +DA:13,0 +DA:39,17 +DA:78,17 +DA:132,0 +DA:136,0 +DA:139,0 +DA:144,0 +DA:146,0 +DA:148,0 +DA:150,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:188,0 +DA:189,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:203,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:207,0 +DA:208,0 +DA:209,0 +DA:210,0 +DA:211,0 +DA:212,0 +DA:213,0 +DA:214,0 +DA:215,0 +DA:216,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:223,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:228,0 +DA:229,0 +DA:230,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:234,0 +DA:235,0 +DA:236,0 +DA:237,0 +DA:240,0 +DA:243,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:256,0 +DA:257,0 +DA:258,0 +DA:259,0 +DA:260,0 +DA:263,0 +DA:265,0 +DA:266,0 +DA:267,0 +DA:268,0 +DA:269,0 +DA:270,0 +DA:271,0 +DA:272,0 +DA:273,0 +DA:274,0 +DA:275,0 +DA:276,0 +DA:277,0 +DA:278,0 +DA:279,0 +DA:280,0 +DA:281,0 +DA:283,0 +DA:284,0 +DA:285,0 +DA:288,0 +DA:289,0 +DA:290,0 +DA:291,0 +DA:293,0 +DA:294,0 +DA:298,0 +DA:299,0 +DA:300,0 +DA:302,0 +DA:303,0 +DA:304,0 +DA:305,0 +DA:309,0 +DA:310,0 +DA:311,0 +DA:312,0 +DA:313,0 +DA:314,0 +DA:316,0 +DA:317,0 +DA:319,0 +DA:320,0 +DA:323,0 +DA:324,0 +DA:325,0 +DA:326,0 +DA:327,0 +DA:328,0 +DA:331,0 +DA:332,0 +DA:333,0 +DA:336,0 +DA:337,0 +DA:338,0 +DA:341,0 +DA:343,0 +DA:344,0 +DA:345,0 +DA:347,0 +DA:348,0 +DA:349,0 +DA:350,0 +DA:351,0 +DA:352,0 +DA:353,0 +DA:354,0 +DA:356,0 +DA:357,0 +DA:358,0 +DA:359,0 +DA:360,0 +DA:361,0 +DA:362,0 +DA:364,0 +DA:365,0 +DA:366,0 +DA:367,0 +DA:370,0 +DA:371,0 +DA:376,0 +DA:377,0 +DA:378,0 +DA:381,0 +DA:382,0 +DA:387,0 +DA:388,0 +DA:389,0 +DA:392,0 +DA:393,0 +DA:397,0 +DA:398,0 +DA:401,0 +DA:402,0 +DA:407,0 +DA:408,0 +DA:409,0 +DA:410,0 +DA:411,0 +DA:412,0 +DA:414,0 +DA:415,0 +DA:416,0 +DA:418,0 +DA:419,0 +DA:459,0 +DA:460,0 +DA:465,0 +DA:468,0 +DA:469,0 +DA:475,0 +DA:485,0 +DA:495,0 +DA:496,0 +DA:497,0 +DA:498,0 +DA:499,0 +DA:500,0 +DA:501,0 +DA:502,0 +DA:505,0 +DA:507,0 +DA:509,0 +DA:510,0 +DA:511,0 +DA:512,0 +DA:513,0 +DA:514,0 +DA:515,0 +DA:516,0 +DA:519,0 +DA:520,0 +DA:521,0 +DA:522,0 +DA:523,0 +DA:524,0 +DA:525,0 +DA:526,0 +DA:536,0 +DA:543,0 +DA:547,0 +DA:549,0 +DA:554,0 +DA:555,0 +DA:556,0 +DA:559,0 +DA:560,0 +DA:561,0 +DA:570,0 +DA:577,0 +DA:578,0 +DA:579,0 +DA:580,0 +DA:581,0 +DA:582,0 +DA:585,0 +DA:586,0 +DA:587,0 +DA:588,0 +DA:589,0 +DA:590,0 +DA:597,0 +DA:598,0 +DA:601,0 +DA:604,0 +LF:255 +LH:2 +end_of_record +SF:lib\utils\globals.dart +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:63,0 +DA:64,0 +LF:25 +LH:0 +end_of_record +SF:lib\utils\logger.dart +DA:22,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,12 +DA:31,4 +DA:39,4 +DA:41,4 +DA:42,4 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:72,0 +DA:73,0 +DA:74,4 +DA:76,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:86,4 +DA:91,0 +DA:97,12 +DA:98,4 +DA:99,12 +DA:104,0 +DA:106,0 +DA:108,0 +DA:113,4 +DA:114,8 +DA:115,4 +DA:116,8 +DA:117,0 +DA:119,4 +DA:122,1 +DA:123,2 +DA:124,1 +DA:125,2 +DA:126,0 +DA:128,1 +DA:131,0 +DA:132,0 +DA:133,0 +DA:135,0 +DA:137,0 +DA:139,0 +DA:140,0 +DA:142,0 +DA:144,0 +DA:145,0 +DA:147,0 +DA:150,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:154,0 +DA:157,0 +DA:158,0 +DA:160,0 +DA:162,0 +DA:164,0 +DA:167,0 +DA:168,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:177,0 +DA:179,0 +DA:182,0 +DA:184,0 +DA:187,0 +DA:188,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:208,4 +DA:209,4 +DA:210,4 +DA:211,4 +DA:214,4 +DA:215,8 +DA:219,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:223,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:229,0 +DA:230,0 +DA:233,0 +DA:237,4 +DA:238,8 +DA:241,4 +DA:242,8 +DA:243,0 +DA:244,0 +DA:247,4 +DA:248,0 +DA:249,0 +DA:254,8 +DA:255,4 +DA:257,0 +DA:258,0 +DA:259,0 +DA:260,0 +DA:262,4 +DA:265,4 +DA:270,4 +DA:272,8 +DA:275,4 +DA:277,8 +DA:280,0 +DA:282,0 +DA:283,0 +DA:284,0 +DA:285,0 +DA:290,0 +DA:291,0 +DA:292,0 +DA:293,0 +DA:294,0 +DA:295,0 +DA:296,0 +DA:298,0 +DA:299,0 +DA:300,0 +DA:301,0 +DA:302,0 +DA:311,0 +DA:312,0 +DA:313,0 +DA:314,0 +DA:315,0 +DA:316,0 +DA:323,4 +DA:324,8 +DA:326,12 +DA:327,4 +DA:332,4 +DA:333,12 +DA:334,8 +DA:335,4 +DA:336,5 +DA:337,5 +DA:339,4 +DA:341,12 +DA:342,4 +DA:343,20 +DA:344,20 +DA:345,12 +DA:351,12 +LF:166 +LH:56 +end_of_record +SF:lib\utils\riverpod_providers.dart +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:43,0 +DA:46,0 +DA:47,0 +DA:55,0 +DA:56,0 +DA:58,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:72,0 +DA:73,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:79,0 +DA:81,0 +DA:82,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:105,0 +DA:106,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:137,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:153,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:184,0 +DA:189,0 +LF:73 +LH:0 +end_of_record +SF:lib\views\add_token_manually_view\add_token_manually_view.dart +DA:21,0 +DA:22,0 +DA:23,0 +DA:25,17 +DA:27,0 +DA:28,0 +DA:52,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:60,0 +DA:62,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:71,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:112,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:136,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:146,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:152,0 +DA:156,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:165,0 +DA:166,0 +DA:168,0 +DA:169,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:189,0 +DA:190,0 +DA:191,0 +DA:193,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:202,0 +DA:206,0 +DA:207,0 +DA:208,0 +DA:210,0 +DA:211,0 +DA:213,0 +DA:217,0 +DA:218,0 +DA:220,0 +DA:221,0 +DA:223,0 +LF:109 +LH:1 +end_of_record +SF:lib\views\import_tokens_view\import_tokens_view.dart +DA:12,0 +DA:23,17 +DA:25,0 +DA:26,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:42,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:53,0 +DA:55,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:66,0 +DA:68,0 +LF:27 +LH:1 +end_of_record +SF:lib\views\license_view\license_view.dart +DA:8,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:25,0 +DA:27,0 +DA:28,0 +LF:12 +LH:0 +end_of_record +SF:lib\views\main_view\main_view.dart +DA:23,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:57,0 +DA:59,0 +DA:60,0 +DA:65,0 +DA:67,0 +DA:69,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:88,0 +DA:89,0 +DA:91,0 +DA:92,0 +DA:93,0 +LF:34 +LH:0 +end_of_record +SF:lib\views\push_token_view\push_tokens_view.dart +DA:8,0 +DA:11,17 +DA:13,0 +DA:15,0 +DA:17,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +LF:10 +LH:1 +end_of_record +SF:lib\views\qr_scanner_view\qr_scanner_view.dart +DA:34,17 +DA:36,0 +DA:37,0 +DA:39,0 +DA:44,0 +DA:46,0 +DA:48,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:77,0 +DA:78,0 +DA:80,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:97,0 +DA:100,0 +LF:36 +LH:1 +end_of_record +SF:lib\views\settings_view\settings_view.dart +DA:17,0 +DA:21,17 +DA:23,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:39,0 +DA:41,0 +DA:43,0 +DA:51,0 +LF:16 +LH:1 +end_of_record +SF:lib\views\splash_screen\splash_screen.dart +DA:23,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:30,0 +DA:34,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:73,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:83,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:102,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:124,0 +DA:125,0 +DA:127,0 +DA:128,0 +DA:131,0 +DA:133,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:140,0 +DA:142,0 +LF:61 +LH:0 +end_of_record +SF:lib\widgets\app_wrapper.dart +DA:19,0 +DA:21,0 +DA:23,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:61,0 +DA:62,0 +DA:64,0 +DA:65,0 +DA:67,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:86,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:94,0 +DA:95,0 +LF:45 +LH:0 +end_of_record +SF:lib\model\enums\app_feature.dart +DA:8,0 +DA:9,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +LF:7 +LH:0 +end_of_record +SF:lib\utils\home_widget_utils.dart +DA:37,0 +DA:40,0 +DA:44,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:66,0 +DA:71,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:141,0 +DA:142,0 +DA:145,0 +DA:147,0 +DA:155,0 +DA:159,0 +DA:160,0 +DA:162,0 +DA:163,0 +DA:166,0 +DA:167,0 +DA:170,0 +DA:171,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:184,0 +DA:190,0 +DA:192,0 +DA:193,0 +DA:195,0 +DA:196,0 +DA:200,0 +DA:207,0 +DA:208,0 +DA:210,0 +DA:213,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:218,0 +DA:221,0 +DA:222,0 +DA:223,0 +DA:226,0 +DA:227,0 +DA:228,0 +DA:229,0 +DA:230,0 +DA:232,0 +DA:233,0 +DA:234,0 +DA:239,0 +DA:240,0 +DA:241,0 +DA:245,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:254,0 +DA:255,0 +DA:256,0 +DA:259,0 +DA:260,0 +DA:263,0 +DA:264,0 +DA:265,0 +DA:267,0 +DA:270,0 +DA:273,0 +DA:274,0 +DA:275,0 +DA:278,0 +DA:279,0 +DA:282,0 +DA:285,0 +DA:287,0 +DA:288,0 +DA:290,0 +DA:291,0 +DA:314,0 +DA:315,0 +DA:316,0 +DA:317,0 +DA:320,0 +DA:322,0 +DA:326,0 +DA:328,0 +DA:329,0 +DA:330,0 +DA:331,0 +DA:334,0 +DA:335,0 +DA:336,0 +DA:341,0 +DA:342,0 +DA:343,0 +DA:345,0 +DA:348,0 +DA:350,0 +DA:351,0 +DA:352,0 +DA:355,0 +DA:356,0 +DA:357,0 +DA:358,0 +DA:359,0 +DA:360,0 +DA:363,0 +DA:364,0 +DA:365,0 +DA:371,0 +DA:372,0 +DA:373,0 +DA:374,0 +DA:375,0 +DA:376,0 +DA:379,0 +DA:380,0 +DA:381,0 +DA:382,0 +DA:383,0 +DA:384,0 +DA:385,0 +DA:386,0 +DA:388,0 +DA:391,0 +DA:392,0 +DA:393,0 +DA:394,0 +DA:399,0 +DA:400,0 +DA:401,0 +DA:404,0 +DA:405,0 +DA:406,0 +DA:409,0 +DA:410,0 +DA:411,0 +DA:412,0 +DA:413,0 +DA:414,0 +DA:415,0 +DA:417,0 +DA:418,0 +DA:422,0 +DA:423,0 +DA:424,0 +DA:425,0 +DA:426,0 +DA:427,0 +DA:429,0 +DA:430,0 +DA:434,0 +DA:435,0 +DA:437,0 +DA:440,0 +DA:441,0 +DA:443,0 +DA:444,0 +DA:445,0 +DA:446,0 +DA:447,0 +DA:448,0 +DA:453,0 +DA:454,0 +DA:455,0 +DA:456,0 +DA:457,0 +DA:458,0 +DA:459,0 +DA:461,0 +DA:465,0 +DA:466,0 +DA:467,0 +DA:468,0 +DA:474,0 +DA:475,0 +DA:477,0 +DA:482,0 +DA:483,0 +DA:484,0 +DA:488,0 +DA:490,0 +DA:492,0 +DA:493,0 +DA:495,0 +DA:497,0 +DA:499,0 +DA:501,0 +DA:502,0 +DA:504,0 +DA:506,0 +DA:508,0 +DA:510,0 +DA:511,0 +DA:513,0 +DA:515,0 +DA:517,0 +DA:518,0 +DA:519,0 +DA:520,0 +DA:521,0 +DA:522,0 +DA:524,0 +DA:526,0 +DA:528,0 +DA:529,0 +DA:530,0 +DA:531,0 +DA:532,0 +DA:533,0 +DA:535,0 +DA:537,0 +DA:539,0 +DA:540,0 +DA:541,0 +DA:545,0 +DA:547,0 +DA:548,0 +DA:549,0 +DA:553,0 +DA:555,0 +DA:556,0 +DA:557,0 +DA:561,0 +DA:566,0 +DA:568,0 +DA:569,0 +DA:571,0 +DA:573,0 +DA:575,0 +DA:576,0 +DA:577,0 +DA:578,0 +DA:581,0 +DA:595,0 +DA:596,0 +DA:597,0 +DA:598,0 +DA:599,0 +DA:600,0 +DA:601,0 +DA:602,0 +DA:603,0 +DA:604,0 +DA:605,0 +DA:609,0 +DA:610,0 +DA:611,0 +DA:612,0 +DA:625,0 +DA:626,0 +DA:627,0 +DA:628,0 +DA:629,0 +DA:630,0 +DA:631,0 +DA:632,0 +DA:633,0 +DA:634,0 +DA:635,0 +DA:637,0 +DA:638,0 +DA:639,0 +DA:640,0 +DA:641,0 +DA:642,0 +DA:643,0 +DA:644,0 +DA:645,0 +DA:647,0 +DA:649,0 +DA:651,0 +DA:653,0 +DA:655,0 +DA:657,0 +DA:659,0 +DA:661,0 +DA:663,0 +DA:665,0 +DA:667,0 +DA:669,0 +DA:671,0 +DA:673,0 +DA:675,0 +DA:677,0 +DA:679,0 +DA:681,0 +DA:682,0 +DA:683,0 +DA:685,0 +DA:687,0 +DA:688,0 +DA:689,0 +DA:690,0 +DA:691,0 +DA:693,0 +DA:694,0 +DA:695,0 +DA:696,0 +DA:697,0 +DA:699,0 +DA:701,0 +DA:703,0 +DA:705,0 +DA:707,0 +DA:709,0 +DA:711,0 +DA:713,0 +DA:715,0 +DA:716,0 +DA:717,0 +DA:719,0 +DA:720,0 +DA:721,0 +DA:722,0 +DA:723,0 +DA:725,0 +LF:334 +LH:0 +end_of_record +SF:lib\views\feedback_view\feedback_view.dart +DA:17,0 +DA:20,17 +DA:22,0 +DA:23,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:68,0 +DA:70,0 +DA:71,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:87,0 +DA:92,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:106,0 +DA:114,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:122,0 +DA:123,0 +DA:125,0 +DA:126,0 +DA:128,0 +DA:132,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:139,0 +DA:140,0 +DA:148,0 +DA:149,0 +DA:151,0 +DA:152,0 +DA:154,0 +DA:156,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:173,0 +DA:175,0 +DA:176,0 +DA:190,0 +DA:191,0 +LF:84 +LH:1 +end_of_record +SF:lib\model\encryption\aes_encrypted.dart +DA:8,0 +DA:23,0 +DA:32,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:54,0 +DA:60,0 +DA:69,0 +DA:70,0 +DA:72,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:87,0 +DA:88,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:102,0 +DA:106,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:117,0 +LF:45 +LH:0 +end_of_record +SF:lib\model\encryption\token_encryption.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +LF:20 +LH:0 +end_of_record +SF:lib\processors\scheme_processors\token_import_scheme_processors\privacyidea_authenticator_qr_processor.dart +DA:10,34 +DA:14,0 +DA:15,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:26,0 +DA:28,0 +DA:30,0 +LF:11 +LH:1 +end_of_record +SF:lib\model\encryption\uint_8_buffer.dart +DA:6,0 +DA:7,0 +DA:8,0 +DA:12,0 +DA:13,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:34,0 +LF:12 +LH:0 +end_of_record +SF:lib\model\enums\algorithms.dart +DA:15,2 +DA:23,6 +DA:24,2 +DA:25,6 +DA:26,2 +DA:27,6 +DA:28,2 +DA:31,3 +DA:38,6 +DA:39,3 +DA:40,3 +DA:41,3 +DA:42,3 +DA:45,0 +DA:46,0 +DA:49,4 +DA:50,4 +DA:51,2 +DA:52,1 +DA:53,1 +DA:54,0 +DA:55,1 +DA:66,1 +DA:73,1 +DA:75,0 +DA:81,1 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:107,1 +DA:109,0 +DA:110,0 +LF:39 +LH:23 +end_of_record +SF:lib\model\extensions\enum_extension.dart +DA:2,28 +DA:4,0 +DA:5,0 +DA:6,0 +DA:10,0 +DA:13,5 +LF:6 +LH:2 +end_of_record +SF:lib\model\enums\encodings.dart +DA:14,5 +DA:15,6 +DA:16,10 +DA:17,2 +DA:20,0 +DA:22,2 +DA:23,3 +DA:24,6 +DA:25,3 +DA:28,1 +DA:30,1 +DA:37,0 +DA:39,0 +DA:46,1 +DA:48,1 +DA:54,0 +DA:56,0 +DA:62,0 +DA:63,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +LF:29 +LH:12 +end_of_record +SF:lib\model\enums\patch_note_type.dart +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +LF:4 +LH:0 +end_of_record +SF:lib\model\enums\push_token_rollout_state.dart +DA:17,1 +DA:18,1 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +LF:18 +LH:2 +end_of_record +SF:lib\model\enums\token_import_type.dart +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +LF:11 +LH:0 +end_of_record +SF:lib\model\enums\token_origin_source_type.dart +DA:17,2 +DA:20,0 +DA:22,1 +DA:25,1 +DA:26,2 +LF:5 +LH:4 +end_of_record +SF:lib\model\token_import\token_origin_data.dart +DA:16,1 +DA:25,1 +DA:33,1 +DA:38,0 +DA:42,0 +DA:45,0 +DA:48,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:65,0 +DA:66,0 +DA:69,0 +DA:70,0 +DA:72,0 +DA:74,0 +LF:20 +LH:3 +end_of_record +SF:lib\model\token_import\token_origin_data.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +LF:18 +LH:0 +end_of_record +SF:lib\model\extensions\color_extension.dart +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:8,0 +DA:9,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +LF:12 +LH:0 +end_of_record +SF:lib\model\extensions\int_extension.dart +DA:5,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:17,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:25,0 +LF:11 +LH:0 +end_of_record +SF:lib\model\processor_result.dart +DA:2,0 +DA:3,0 +DA:4,0 +DA:9,1 +DA:11,0 +DA:13,0 +DA:19,17 +DA:21,0 +DA:23,0 +LF:9 +LH:2 +end_of_record +SF:lib\utils\identifiers.dart +DA:69,0 +DA:70,0 +DA:71,0 +LF:3 +LH:0 +end_of_record +SF:lib\utils\rsa_utils.dart +DA:38,87 +DA:47,2 +DA:48,6 +DA:49,6 +DA:50,6 +DA:52,2 +DA:62,2 +DA:63,2 +DA:64,6 +DA:65,6 +DA:67,4 +DA:82,1 +DA:83,3 +DA:85,2 +DA:87,3 +DA:89,2 +DA:90,0 +DA:91,0 +DA:97,2 +DA:99,3 +DA:101,3 +DA:102,3 +DA:104,1 +DA:119,1 +DA:120,1 +DA:121,1 +DA:122,2 +DA:123,2 +DA:125,1 +DA:126,3 +DA:127,3 +DA:129,2 +DA:131,1 +DA:132,1 +DA:133,1 +DA:134,2 +DA:155,2 +DA:156,2 +DA:157,4 +DA:158,6 +DA:159,6 +DA:160,6 +DA:161,6 +DA:162,6 +DA:163,14 +DA:164,14 +DA:165,10 +DA:167,4 +DA:188,2 +DA:189,6 +DA:190,6 +DA:191,6 +DA:192,6 +DA:193,6 +DA:195,2 +DA:199,1 +DA:200,1 +DA:201,2 +DA:205,2 +DA:206,0 +DA:207,0 +DA:219,0 +DA:220,0 +DA:221,0 +DA:225,0 +DA:227,0 +DA:228,0 +DA:229,0 +DA:235,2 +DA:236,2 +DA:237,4 +DA:238,2 +DA:243,2 +DA:244,12 +DA:246,2 +DA:248,6 +DA:251,0 +DA:252,0 +DA:255,1 +DA:256,1 +DA:257,2 +DA:259,2 +LF:82 +LH:69 +end_of_record +SF:lib\model\tokens\push_token.dart +DA:18,2 +DA:23,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,4 +DA:43,0 +DA:44,4 +DA:46,3 +DA:72,6 +DA:74,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:86,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:96,2 +DA:121,2 +DA:122,2 +DA:123,2 +DA:124,2 +DA:125,2 +DA:126,2 +DA:127,2 +DA:128,2 +DA:129,2 +DA:130,2 +DA:131,2 +DA:132,2 +DA:133,2 +DA:134,2 +DA:135,2 +DA:136,2 +DA:137,2 +DA:138,2 +DA:139,2 +DA:140,1 +DA:141,3 +DA:142,2 +DA:146,1 +DA:147,7 +DA:149,0 +DA:150,0 +DA:152,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:166,4 +DA:167,2 +DA:168,2 +DA:169,2 +DA:170,2 +DA:171,2 +DA:172,10 +DA:173,2 +DA:174,2 +DA:175,2 +DA:176,2 +DA:177,2 +DA:178,2 +DA:181,1 +DA:182,1 +DA:183,1 +DA:184,1 +DA:185,1 +DA:186,0 +DA:187,0 +DA:188,0 +DA:190,1 +DA:193,1 +DA:194,1 +LF:88 +LH:51 +end_of_record +SF:lib\model\tokens\push_token.g.dart +DA:9,2 +DA:10,1 +DA:11,1 +DA:12,1 +DA:13,1 +DA:14,1 +DA:15,3 +DA:16,1 +DA:18,2 +DA:19,1 +DA:20,1 +DA:21,1 +DA:22,1 +DA:23,1 +DA:24,1 +DA:25,1 +DA:26,1 +DA:27,1 +DA:28,1 +DA:29,1 +DA:30,1 +DA:31,1 +DA:32,1 +DA:33,1 +DA:34,1 +DA:36,0 +DA:39,2 +DA:40,1 +DA:41,1 +DA:42,1 +DA:43,1 +DA:44,1 +DA:45,1 +DA:46,1 +DA:47,1 +DA:48,1 +DA:49,1 +DA:50,1 +DA:51,2 +DA:52,1 +DA:53,1 +DA:54,1 +DA:55,1 +DA:56,2 +DA:57,1 +DA:58,2 +DA:59,1 +DA:60,1 +DA:61,1 +LF:49 +LH:48 +end_of_record +SF:lib\utils\custom_int_buffer.dart +DA:10,37 +DA:12,1 +DA:13,1 +DA:14,1 +DA:15,1 +DA:19,3 +DA:20,1 +DA:21,3 +DA:22,4 +DA:23,2 +DA:26,1 +DA:27,3 +DA:28,3 +DA:29,0 +DA:31,2 +DA:34,3 +DA:35,0 +DA:36,0 +DA:37,6 +DA:39,1 +DA:42,4 +DA:45,0 +DA:46,0 +DA:48,0 +DA:49,0 +LF:25 +LH:18 +end_of_record +SF:lib\utils\custom_int_buffer.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:14,0 +DA:15,0 +DA:16,0 +LF:6 +LH:0 +end_of_record +SF:lib\utils\version.dart +DA:11,19 +DA:16,2 +DA:17,2 +DA:18,4 +DA:19,0 +DA:21,14 +DA:24,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:35,0 +DA:38,0 +DA:39,0 +DA:42,0 +DA:43,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:82,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:89,0 +DA:90,0 +DA:92,0 +DA:94,0 +DA:96,0 +DA:97,0 +LF:45 +LH:5 +end_of_record +SF:lib\utils\version.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +LF:8 +LH:0 +end_of_record +SF:lib\model\states\token_filter.dart +DA:6,0 +DA:7,0 +DA:8,0 +DA:11,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:17,0 +LF:8 +LH:0 +end_of_record +SF:lib\model\states\token_folder_state.dart +DA:12,20 +DA:14,2 +DA:15,4 +DA:16,6 +DA:17,2 +DA:22,2 +DA:23,4 +DA:24,4 +DA:25,10 +DA:26,4 +DA:27,2 +DA:30,2 +DA:33,2 +DA:34,4 +DA:35,10 +DA:36,2 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:55,10 +DA:57,0 +LF:28 +LH:17 +end_of_record +SF:lib\model\states\token_state.dart +DA:16,4 +DA:17,3 +DA:19,4 +DA:20,0 +DA:21,5 +DA:23,1 +DA:24,7 +DA:26,2 +DA:27,2 +DA:28,2 +DA:29,4 +DA:32,0 +DA:34,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:64,2 +DA:65,10 +DA:68,7 +DA:69,0 +DA:71,1 +DA:72,2 +DA:73,1 +DA:74,2 +DA:77,1 +DA:78,2 +DA:79,1 +DA:80,1 +DA:85,2 +DA:86,4 +DA:87,10 +DA:88,2 +DA:91,1 +DA:92,2 +DA:93,7 +DA:94,1 +DA:99,2 +DA:100,4 +DA:101,10 +DA:102,4 +DA:103,2 +DA:105,2 +DA:107,4 +DA:112,1 +DA:113,2 +DA:114,5 +DA:115,2 +DA:116,0 +DA:119,1 +DA:120,2 +DA:125,2 +DA:126,4 +DA:127,2 +DA:128,4 +DA:129,10 +DA:130,4 +DA:131,2 +DA:132,2 +DA:135,2 +DA:136,2 +DA:138,2 +DA:143,1 +DA:144,2 +DA:145,1 +DA:146,1 +DA:147,1 +DA:148,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:154,0 +DA:156,1 +DA:159,0 +DA:160,0 +DA:163,0 +DA:164,0 +DA:166,0 +DA:168,0 +DA:169,0 +DA:172,0 +DA:173,0 +DA:175,0 +LF:84 +LH:59 +end_of_record +SF:lib\model\tokens\otp_token.dart +DA:11,0 +DA:13,0 +DA:17,4 +DA:38,0 +DA:40,0 +DA:60,0 +DA:62,0 +LF:7 +LH:1 +end_of_record +SF:lib\model\token_import\token_import_origin.dart +DA:11,0 +DA:23,0 +LF:2 +LH:0 +end_of_record +SF:lib\processors\mixins\token_import_processor.dart +DA:7,0 +DA:8,0 +DA:9,0 +LF:3 +LH:0 +end_of_record +SF:lib\model\tokens\day_password_token.dart +DA:20,0 +DA:24,1 +DA:41,2 +DA:42,2 +DA:44,0 +DA:47,0 +DA:50,0 +DA:53,0 +DA:56,0 +DA:58,0 +DA:61,0 +DA:62,0 +DA:64,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:101,1 +DA:102,2 +DA:103,1 +DA:104,1 +DA:105,1 +DA:106,1 +DA:110,0 +DA:111,0 +DA:112,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:119,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:146,0 +DA:147,0 +LF:64 +LH:9 +end_of_record +SF:lib\model\tokens\day_password_token.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +LF:38 +LH:0 +end_of_record +SF:lib\model\tokens\hotp_token.dart +DA:17,0 +DA:20,0 +DA:23,4 +DA:39,8 +DA:41,0 +DA:42,0 +DA:44,0 +DA:46,0 +DA:48,3 +DA:49,6 +DA:50,3 +DA:51,3 +DA:52,3 +DA:56,4 +DA:58,2 +DA:75,2 +DA:76,1 +DA:77,2 +DA:78,2 +DA:79,1 +DA:80,2 +DA:81,2 +DA:82,2 +DA:83,2 +DA:84,2 +DA:85,2 +DA:86,2 +DA:87,2 +DA:88,3 +DA:89,2 +DA:92,0 +DA:94,0 +DA:97,1 +DA:98,2 +DA:99,3 +DA:100,1 +DA:101,1 +DA:102,1 +DA:103,1 +DA:104,2 +DA:105,1 +DA:106,2 +DA:107,1 +DA:108,1 +DA:109,1 +DA:110,1 +DA:111,1 +DA:115,0 +DA:116,0 +DA:117,0 +LF:50 +LH:39 +end_of_record +SF:lib\model\tokens\hotp_token.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +LF:33 +LH:0 +end_of_record +SF:lib\model\tokens\steam_token.dart +DA:20,0 +DA:22,0 +DA:25,0 +DA:39,0 +DA:40,0 +DA:44,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:82,0 +DA:83,0 +DA:85,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:93,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:114,0 +DA:115,0 +DA:116,0 +LF:47 +LH:0 +end_of_record +SF:lib\model\tokens\steam_token.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +LF:30 +LH:0 +end_of_record +SF:lib\model\tokens\totp_token.dart +DA:18,4 +DA:23,0 +DA:25,0 +DA:26,0 +DA:32,1 +DA:33,2 +DA:34,1 +DA:35,1 +DA:36,1 +DA:37,2 +DA:41,2 +DA:57,2 +DA:58,4 +DA:64,0 +DA:65,0 +DA:67,2 +DA:84,2 +DA:85,1 +DA:86,1 +DA:87,1 +DA:88,1 +DA:89,1 +DA:90,1 +DA:91,1 +DA:92,1 +DA:93,1 +DA:94,1 +DA:95,1 +DA:96,2 +DA:97,2 +DA:98,1 +DA:102,0 +DA:104,0 +DA:107,2 +DA:108,3 +DA:109,7 +DA:110,7 +DA:111,2 +DA:112,2 +DA:113,2 +DA:114,2 +DA:115,4 +DA:116,2 +DA:117,2 +DA:118,4 +DA:119,2 +DA:120,2 +DA:121,2 +DA:122,2 +DA:126,0 +DA:127,0 +DA:128,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:136,1 +DA:137,1 +DA:138,2 +LF:58 +LH:45 +end_of_record +SF:lib\model\tokens\totp_token.g.dart +DA:9,2 +DA:10,1 +DA:11,1 +DA:12,2 +DA:13,1 +DA:14,1 +DA:15,1 +DA:16,1 +DA:17,1 +DA:18,1 +DA:19,1 +DA:20,1 +DA:21,1 +DA:22,1 +DA:24,0 +DA:25,1 +DA:26,1 +DA:29,2 +DA:30,1 +DA:31,1 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:36,1 +DA:37,1 +DA:38,1 +DA:39,1 +DA:40,1 +DA:41,2 +DA:42,1 +DA:43,1 +DA:44,1 +LF:33 +LH:32 +end_of_record +SF:lib\processors\scheme_processors\token_import_scheme_processors\google_authenticator_qr_processor.dart +DA:19,51 +DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:52,0 +DA:55,0 +DA:58,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:71,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:88,0 +DA:90,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:101,0 +DA:104,0 +DA:106,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:118,0 +DA:121,0 +LF:48 +LH:1 +end_of_record +SF:lib\processors\token_import_file_processor\token_import_file_processor_interface.dart +DA:11,34 +DA:13,0 +DA:15,0 +DA:20,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:32,0 +DA:36,0 +LF:10 +LH:1 +end_of_record +SF:lib\processors\scheme_processors\home_widget_processor.dart +DA:6,51 +DA:8,0 +DA:14,0 +DA:16,0 +DA:17,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +LF:22 +LH:1 +end_of_record +SF:lib\processors\scheme_processors\scheme_processor_interface.dart +DA:6,0 +DA:10,0 +DA:12,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +LF:8 +LH:0 +end_of_record +SF:lib\processors\scheme_processors\navigation_scheme_processors\home_widget_navigate_processor.dart +DA:11,0 +DA:13,0 +DA:18,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:28,0 +DA:29,0 +DA:32,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:41,0 +DA:45,0 +DA:46,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:63,0 +DA:64,0 +DA:67,0 +DA:68,0 +DA:70,0 +DA:72,0 +DA:75,0 +DA:77,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:85,0 +LF:36 +LH:0 +end_of_record +SF:lib\views\link_home_widget_view\link_home_widget_view.dart +DA:12,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:55,0 +LF:27 +LH:0 +end_of_record +SF:lib\processors\scheme_processors\navigation_scheme_processors\navigation_scheme_processor_interface.dart +DA:9,0 +DA:11,0 +DA:12,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:32,0 +DA:35,0 +LF:15 +LH:0 +end_of_record +SF:lib\processors\scheme_processors\token_import_scheme_processors\token_import_scheme_processor_interface.dart +DA:10,119 +DA:17,0 +DA:21,0 +DA:26,1 +DA:27,2 +DA:28,3 +DA:29,1 +LF:7 +LH:5 +end_of_record +SF:lib\processors\scheme_processors\token_import_scheme_processors\free_otp_plus_qr_processor.dart +DA:10,34 +DA:12,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:35,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:54,0 +DA:55,0 +LF:27 +LH:1 +end_of_record +SF:lib\utils\token_import_origins.dart +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:27,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:36,0 +DA:39,0 +DA:43,0 +DA:46,0 +DA:47,0 +DA:50,0 +DA:52,0 +DA:55,0 +DA:59,0 +DA:62,0 +DA:63,0 +DA:66,0 +DA:68,0 +DA:71,0 +DA:73,0 +DA:76,0 +DA:80,0 +DA:83,0 +DA:84,0 +DA:87,0 +DA:91,0 +DA:94,0 +DA:95,0 +DA:98,0 +DA:102,0 +DA:105,0 +DA:106,0 +DA:109,0 +DA:111,0 +DA:114,0 +LF:42 +LH:0 +end_of_record +SF:lib\processors\scheme_processors\token_import_scheme_processors\otp_auth_processor.dart +DA:20,102 +DA:21,1 +DA:22,1 +DA:24,1 +DA:26,3 +DA:27,1 +DA:30,1 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,1 +DA:37,0 +DA:38,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:51,0 +DA:53,0 +DA:57,1 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:66,2 +DA:72,1 +DA:73,1 +DA:74,1 +DA:76,1 +DA:78,3 +DA:79,1 +DA:81,0 +DA:82,0 +DA:84,0 +DA:88,1 +DA:90,1 +DA:93,2 +DA:97,3 +DA:98,3 +DA:101,3 +DA:103,1 +DA:108,1 +DA:109,1 +DA:110,1 +DA:113,3 +DA:114,0 +DA:117,2 +DA:118,0 +DA:121,2 +DA:122,2 +DA:124,1 +DA:127,2 +DA:129,1 +DA:130,0 +DA:133,0 +DA:137,1 +DA:139,1 +DA:142,2 +DA:143,1 +DA:149,3 +DA:150,1 +DA:152,1 +DA:153,1 +DA:155,0 +DA:158,0 +DA:162,1 +DA:164,2 +DA:166,0 +DA:169,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:180,0 +DA:185,2 +DA:187,2 +DA:189,1 +DA:191,0 +DA:193,1 +DA:196,1 +DA:198,0 +DA:199,0 +DA:200,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:209,0 +DA:213,0 +DA:214,0 +DA:215,0 +DA:218,0 +DA:222,0 +DA:223,0 +DA:224,0 +DA:227,0 +DA:235,1 +DA:245,1 +DA:247,2 +DA:250,2 +DA:253,0 +DA:257,1 +DA:259,2 +DA:261,1 +DA:262,0 +DA:263,0 +DA:265,0 +DA:268,0 +DA:269,0 +DA:270,0 +DA:272,0 +DA:276,2 +DA:277,0 +DA:280,1 +DA:281,1 +DA:282,1 +DA:283,3 +DA:284,2 +DA:286,2 +DA:287,1 +DA:289,2 +DA:290,0 +DA:291,0 +DA:294,2 +DA:296,2 +DA:297,0 +DA:298,0 +DA:301,3 +DA:302,2 +DA:304,4 +DA:307,3 +DA:308,0 +DA:315,1 +DA:318,2 +DA:319,1 +DA:322,1 +DA:323,1 +DA:324,1 +DA:325,1 +DA:327,1 +DA:330,0 +DA:337,1 +DA:339,2 +DA:342,1 +DA:343,0 +DA:350,1 +DA:351,6 +LF:149 +LH:83 +end_of_record +SF:lib\proto\generated\GoogleAuthenticatorImport.pb.dart +DA:23,0 +DA:32,0 +DA:34,0 +DA:37,0 +DA:40,0 +DA:43,0 +DA:46,0 +DA:49,0 +DA:52,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:71,0 +DA:75,0 +DA:79,0 +DA:80,0 +DA:82,0 +DA:85,0 +DA:86,0 +DA:90,0 +DA:92,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:107,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:119,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:131,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:143,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:155,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:167,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:179,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:189,0 +DA:196,0 +DA:198,0 +DA:201,0 +DA:204,0 +DA:207,0 +DA:210,0 +DA:214,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:219,0 +DA:221,0 +DA:223,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:229,0 +DA:232,0 +DA:233,0 +DA:237,0 +DA:239,0 +DA:241,0 +DA:242,0 +DA:243,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:249,0 +DA:250,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:256,0 +DA:259,0 +DA:260,0 +DA:261,0 +DA:262,0 +DA:264,0 +DA:265,0 +DA:266,0 +DA:268,0 +DA:271,0 +DA:272,0 +DA:273,0 +DA:274,0 +DA:276,0 +DA:277,0 +DA:278,0 +DA:280,0 +DA:283,0 +DA:284,0 +DA:285,0 +DA:286,0 +DA:288,0 +DA:289,0 +DA:290,0 +DA:292,0 +DA:295,0 +DA:296,0 +DA:297,0 +DA:298,0 +LF:153 +LH:0 +end_of_record +SF:lib\utils\view_utils.dart +DA:7,0 +DA:11,0 +DA:12,0 +DA:15,0 +DA:16,0 +DA:20,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +LF:11 +LH:0 +end_of_record +SF:lib\widgets\dialog_widgets\two_step_dialog.dart +DA:40,0 +DA:46,0 +DA:48,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:73,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:80,0 +DA:81,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:109,0 +DA:111,0 +DA:112,0 +DA:116,0 +DA:117,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +LF:37 +LH:0 +end_of_record +SF:lib\processors\token_import_file_processor\aegis_import_file_processor.dart +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:41,34 +DA:53,0 +DA:55,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:73,0 +DA:77,0 +DA:78,0 +DA:82,0 +DA:85,0 +DA:89,0 +DA:90,0 +DA:94,0 +DA:97,0 +DA:99,0 +DA:102,0 +DA:104,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:111,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:120,0 +DA:122,0 +DA:124,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:141,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:148,0 +DA:149,0 +DA:155,0 +DA:156,0 +DA:158,0 +DA:160,0 +DA:162,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:173,0 +DA:174,0 +DA:177,0 +DA:178,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:186,0 +DA:188,0 +DA:191,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:199,0 +DA:201,0 +DA:202,0 +DA:203,0 +LF:92 +LH:1 +end_of_record +SF:lib\processors\token_import_file_processor\two_fas_import_file_processor.dart +DA:22,34 +DA:30,0 +DA:32,0 +DA:35,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:43,0 +DA:46,0 +DA:47,0 +DA:53,0 +DA:56,0 +DA:57,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:69,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:83,0 +DA:85,0 +DA:89,0 +DA:91,0 +DA:92,0 +DA:95,0 +DA:97,0 +DA:99,0 +DA:102,0 +DA:104,0 +DA:106,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:113,0 +DA:114,0 +DA:117,0 +DA:118,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:129,0 +DA:130,0 +DA:133,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:149,0 +DA:157,0 +DA:158,0 +DA:160,0 +DA:166,0 +DA:167,0 +DA:169,0 +LF:71 +LH:1 +end_of_record +SF:lib\processors\token_import_file_processor\authenticator_pro_import_file_processor.dart +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:59,17 +DA:61,0 +DA:63,0 +DA:65,0 +DA:67,0 +DA:70,0 +DA:73,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:90,0 +DA:92,0 +DA:94,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:102,0 +DA:109,0 +DA:111,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:123,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:131,0 +DA:133,0 +DA:136,0 +DA:145,0 +DA:152,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:161,0 +DA:163,0 +DA:167,0 +DA:175,0 +DA:176,0 +DA:179,0 +DA:181,0 +DA:182,0 +DA:184,0 +DA:186,0 +DA:187,0 +DA:188,0 +DA:192,0 +DA:194,0 +DA:198,0 +DA:200,0 +DA:201,0 +DA:204,0 +DA:205,0 +DA:207,0 +DA:208,0 +DA:212,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:219,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:224,0 +DA:225,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:235,0 +DA:236,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:240,0 +DA:241,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:247,0 +DA:249,0 +DA:250,0 +DA:256,0 +DA:257,0 +DA:259,0 +DA:260,0 +DA:265,0 +DA:266,0 +DA:267,0 +DA:268,0 +DA:270,0 +DA:271,0 +DA:273,0 +DA:276,0 +DA:278,0 +DA:279,0 +DA:280,0 +DA:281,0 +DA:282,0 +DA:283,0 +DA:284,0 +DA:285,0 +DA:286,0 +DA:288,0 +DA:292,0 +DA:293,0 +DA:294,0 +DA:295,0 +DA:297,0 +DA:298,0 +LF:127 +LH:1 +end_of_record +SF:lib\processors\token_import_file_processor\free_otp_plus_file_processor.dart +DA:32,17 +DA:34,0 +DA:38,0 +DA:43,0 +DA:44,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:56,0 +DA:59,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:71,0 +DA:72,0 +DA:74,0 +DA:75,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:84,0 +DA:86,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:93,0 +DA:95,0 +DA:96,0 +DA:101,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:107,0 +DA:108,0 +DA:112,0 +DA:113,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:126,0 +LF:52 +LH:1 +end_of_record +SF:lib\processors\token_import_file_processor\privacyidea_authenticator_import_file_processor.dart +DA:13,17 +DA:14,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:33,0 +DA:36,0 +DA:39,0 +DA:41,0 +DA:43,0 +DA:46,0 +DA:47,0 +LF:15 +LH:1 +end_of_record +SF:lib\proto\generated\GoogleAuthenticatorImport.pbenum.dart +DA:31,0 +DA:32,0 +DA:34,17 +DA:48,0 +DA:49,0 +DA:51,17 +DA:65,0 +DA:66,0 +DA:68,17 +LF:9 +LH:3 +end_of_record +SF:lib\repo\preference_introduction_repository.dart +DA:12,0 +DA:14,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:24,0 +DA:35,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:42,0 +LF:12 +LH:0 +end_of_record +SF:lib\repo\preference_settings_repository.dart +DA:22,0 +DA:23,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:63,0 +LF:33 +LH:0 +end_of_record +SF:lib\repo\preference_token_folder_repository.dart +DA:12,0 +DA:14,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:23,0 +DA:24,0 +DA:28,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,0 +LF:14 +LH:0 +end_of_record +SF:lib\repo\secure_push_request_repository.dart +DA:14,34 +DA:17,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:36,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:46,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:57,0 +DA:60,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:70,0 +DA:71,0 +DA:75,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:87,0 +DA:92,0 +DA:93,0 +LF:33 +LH:1 +end_of_record +SF:lib\repo\secure_token_repository.dart +DA:44,51 +DA:47,0 +DA:51,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:66,0 +DA:72,0 +DA:73,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:83,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:89,0 +DA:96,0 +DA:97,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:112,0 +DA:114,0 +DA:116,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:139,0 +DA:144,0 +DA:145,0 +DA:147,0 +DA:149,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:173,0 +DA:174,0 +DA:176,0 +DA:178,0 +DA:180,0 +DA:183,0 +DA:191,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:200,0 +DA:202,0 +DA:203,0 +DA:206,0 +DA:207,0 +DA:208,0 +DA:211,0 +DA:212,0 +DA:213,0 +DA:214,0 +DA:215,0 +DA:217,0 +DA:221,0 +DA:222,0 +DA:223,0 +DA:226,0 +DA:234,0 +DA:238,0 +DA:240,0 +DA:241,0 +DA:243,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:256,0 +DA:258,0 +DA:259,0 +DA:260,0 +DA:265,0 +DA:266,0 +DA:268,0 +DA:269,0 +DA:270,0 +LF:102 +LH:1 +end_of_record +SF:lib\views\settings_view\settings_view_widgets\send_error_dialog.dart +DA:11,0 +DA:13,0 +DA:14,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:51,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:66,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:77,0 +DA:78,0 +DA:83,34 +DA:85,0 +DA:87,0 +DA:89,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:99,0 +LF:43 +LH:1 +end_of_record +SF:lib\widgets\dialog_widgets\default_dialog.dart +DA:14,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:61,0 +DA:62,0 +LF:25 +LH:0 +end_of_record +SF:lib\widgets\dialog_widgets\default_dialog_button.dart +DA:7,0 +DA:9,0 +DA:10,0 +LF:3 +LH:0 +end_of_record +SF:lib\state_notifiers\completed_introduction_notifier.dart +DA:12,0 +DA:14,0 +DA:15,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:42,0 +DA:44,0 +DA:46,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +LF:29 +LH:0 +end_of_record +SF:lib\state_notifiers\deeplink_notifier.dart +DA:14,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:31,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:46,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:67,0 +LF:24 +LH:0 +end_of_record +SF:lib\utils\riverpod_state_listener.dart +DA:14,0 +DA:15,0 +DA:16,0 +DA:21,0 +DA:24,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:38,0 +DA:40,0 +DA:45,0 +DA:47,0 +DA:52,0 +DA:54,0 +DA:59,0 +DA:62,0 +DA:66,0 +DA:68,0 +DA:74,0 +LF:20 +LH:0 +end_of_record +SF:lib\state_notifiers\push_request_notifier.dart +DA:48,0 +DA:54,1 +DA:64,1 +DA:67,1 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:76,1 +DA:77,2 +DA:78,3 +DA:79,1 +DA:80,1 +DA:83,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:97,1 +DA:98,2 +DA:101,2 +DA:103,0 +DA:104,0 +DA:105,0 +DA:107,2 +DA:108,1 +DA:109,2 +DA:115,1 +DA:116,2 +DA:117,1 +DA:118,1 +DA:120,2 +DA:122,0 +DA:123,0 +DA:127,0 +DA:130,1 +DA:131,2 +DA:136,1 +DA:137,2 +DA:138,1 +DA:139,1 +DA:141,0 +DA:145,0 +DA:149,2 +DA:151,0 +DA:152,0 +DA:156,0 +DA:159,1 +DA:160,2 +DA:165,1 +DA:166,2 +DA:167,2 +DA:169,2 +DA:171,0 +DA:176,0 +DA:179,1 +DA:180,1 +DA:181,2 +DA:192,1 +DA:193,2 +DA:194,2 +DA:196,0 +DA:197,0 +DA:200,1 +DA:201,1 +DA:202,2 +DA:213,0 +DA:215,0 +DA:220,1 +DA:221,1 +DA:222,0 +DA:226,1 +DA:227,2 +DA:228,1 +DA:229,1 +DA:235,2 +DA:236,1 +DA:240,1 +DA:241,1 +DA:242,0 +DA:245,1 +DA:246,2 +DA:247,1 +DA:248,1 +DA:254,2 +DA:255,1 +DA:259,1 +DA:260,3 +DA:261,0 +DA:268,1 +DA:270,1 +DA:271,3 +DA:275,2 +DA:281,1 +DA:282,1 +DA:283,1 +DA:286,6 +DA:288,1 +DA:289,3 +DA:290,0 +DA:297,1 +DA:298,4 +DA:299,4 +DA:300,1 +DA:301,0 +DA:304,6 +DA:307,1 +DA:308,1 +DA:309,2 +DA:310,4 +DA:311,1 +DA:312,0 +DA:314,6 +DA:318,1 +DA:319,1 +DA:320,3 +DA:322,3 +DA:323,2 +DA:324,1 +DA:326,2 +DA:335,1 +DA:336,1 +DA:337,1 +DA:340,2 +DA:341,1 +DA:345,4 +DA:347,0 +DA:349,0 +DA:351,0 +DA:352,0 +DA:356,2 +DA:357,0 +LF:131 +LH:94 +end_of_record +SF:lib\utils\network_utils.dart +DA:35,51 +DA:40,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:63,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:74,0 +DA:77,0 +DA:78,0 +DA:80,0 +DA:83,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:99,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:110,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:118,0 +DA:119,0 +DA:122,0 +DA:123,0 +DA:126,0 +DA:129,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:143,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:153,0 +DA:155,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:163,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:171,0 +DA:172,0 +DA:175,0 +DA:176,0 +DA:179,0 +LF:82 +LH:1 +end_of_record +SF:lib\utils\push_provider.dart +DA:62,0 +DA:67,0 +DA:71,0 +DA:72,0 +DA:74,0 +DA:78,0 +DA:80,0 +DA:81,0 +DA:84,0 +DA:91,0 +DA:98,0 +DA:101,0 +DA:104,0 +DA:107,0 +DA:111,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:123,0 +DA:126,0 +DA:127,0 +DA:130,0 +DA:132,0 +DA:133,0 +DA:140,0 +DA:141,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:153,0 +DA:155,0 +DA:156,0 +DA:161,0 +DA:163,0 +DA:167,0 +DA:168,0 +DA:174,0 +DA:176,0 +DA:181,0 +DA:185,0 +DA:186,0 +DA:198,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:204,0 +DA:207,0 +DA:208,0 +DA:211,0 +DA:212,0 +DA:213,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:223,0 +DA:225,0 +DA:228,0 +DA:229,0 +DA:234,0 +DA:236,0 +DA:242,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:247,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:261,0 +DA:263,0 +DA:264,0 +DA:267,0 +DA:268,0 +DA:269,0 +DA:270,0 +DA:271,0 +DA:276,0 +DA:277,0 +DA:279,0 +DA:280,0 +DA:281,0 +DA:282,0 +DA:289,0 +DA:290,0 +DA:291,0 +DA:292,0 +DA:294,0 +DA:298,0 +DA:300,0 +DA:303,0 +DA:305,0 +DA:307,0 +DA:308,0 +DA:309,0 +DA:311,0 +DA:312,0 +DA:313,0 +DA:315,0 +DA:318,0 +DA:319,0 +DA:328,0 +DA:329,0 +DA:331,0 +DA:332,0 +DA:338,0 +DA:339,0 +DA:341,0 +DA:344,0 +DA:345,0 +DA:346,0 +DA:355,0 +DA:356,0 +DA:358,0 +DA:359,0 +DA:360,0 +DA:363,0 +DA:364,0 +DA:368,0 +DA:370,0 +DA:371,0 +DA:372,0 +DA:377,0 +DA:378,0 +DA:379,0 +DA:397,0 +DA:398,0 +DA:400,0 +DA:401,0 +DA:417,0 +DA:419,0 +DA:420,0 +DA:421,0 +DA:423,0 +DA:425,0 +DA:426,0 +DA:427,0 +DA:429,0 +DA:431,0 +DA:433,0 +DA:435,0 +DA:438,0 +DA:439,0 +LF:146 +LH:0 +end_of_record +SF:lib\state_notifiers\settings_notifier.dart +DA:18,1 +DA:22,1 +DA:23,1 +DA:25,1 +DA:26,3 +DA:27,2 +DA:28,0 +DA:29,1 +DA:30,2 +DA:35,1 +DA:36,3 +DA:37,3 +DA:38,3 +DA:39,1 +DA:43,1 +DA:44,2 +DA:45,3 +DA:46,3 +DA:47,1 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:68,1 +DA:69,2 +DA:70,3 +DA:71,1 +DA:74,1 +DA:75,2 +DA:76,3 +DA:77,1 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:92,1 +DA:93,2 +DA:94,3 +DA:95,1 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:104,1 +DA:105,2 +DA:106,3 +DA:107,1 +DA:110,1 +DA:111,2 +DA:112,2 +DA:113,3 +DA:114,1 +DA:117,1 +DA:118,2 +DA:119,3 +DA:120,1 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +LF:76 +LH:43 +end_of_record +SF:lib\state_notifiers\token_folder_notifier.dart +DA:12,1 +DA:14,1 +DA:15,1 +DA:18,8 +DA:20,1 +DA:21,3 +DA:22,2 +DA:23,1 +DA:24,0 +DA:25,0 +DA:30,1 +DA:31,2 +DA:32,1 +DA:33,2 +DA:36,1 +DA:37,2 +DA:38,1 +DA:39,2 +DA:42,1 +DA:43,3 +DA:44,1 +DA:45,2 +DA:48,1 +DA:49,2 +DA:50,1 +DA:51,2 +DA:54,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:64,0 +DA:65,0 +DA:66,0 +LF:36 +LH:24 +end_of_record +SF:lib\state_notifiers\token_notifier.dart +DA:40,0 +DA:49,1 +DA:60,1 +DA:61,1 +DA:62,1 +DA:64,1 +DA:67,1 +DA:68,2 +DA:69,1 +DA:70,1 +DA:71,1 +DA:82,1 +DA:83,2 +DA:84,2 +DA:86,0 +DA:87,0 +DA:90,0 +DA:93,3 +DA:94,2 +DA:99,1 +DA:100,2 +DA:101,2 +DA:102,1 +DA:103,0 +DA:104,0 +DA:108,0 +DA:109,0 +DA:113,3 +DA:114,2 +DA:115,1 +DA:119,1 +DA:120,2 +DA:121,2 +DA:123,0 +DA:124,0 +DA:127,2 +DA:129,0 +DA:130,0 +DA:133,0 +DA:136,1 +DA:137,2 +DA:142,1 +DA:143,2 +DA:144,1 +DA:145,2 +DA:146,1 +DA:147,1 +DA:148,0 +DA:150,2 +DA:151,1 +DA:152,0 +DA:153,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:161,2 +DA:162,1 +DA:166,1 +DA:167,2 +DA:168,3 +DA:170,2 +DA:172,0 +DA:173,0 +DA:176,0 +DA:177,0 +DA:180,2 +DA:181,1 +DA:186,1 +DA:187,2 +DA:191,2 +DA:192,1 +DA:193,1 +DA:195,0 +DA:200,0 +DA:201,0 +DA:203,2 +DA:204,1 +DA:208,0 +DA:209,0 +DA:211,0 +DA:213,0 +DA:218,0 +DA:221,0 +DA:233,1 +DA:234,2 +DA:235,2 +DA:236,2 +DA:237,2 +DA:239,0 +DA:240,0 +DA:243,1 +DA:244,1 +DA:245,2 +DA:250,1 +DA:251,2 +DA:252,1 +DA:254,1 +DA:255,1 +DA:256,0 +DA:257,0 +DA:259,1 +DA:260,6 +DA:264,1 +DA:265,0 +DA:266,0 +DA:268,1 +DA:269,2 +DA:282,2 +DA:285,2 +DA:288,0 +DA:290,2 +DA:293,2 +DA:296,0 +DA:299,0 +DA:300,0 +DA:302,0 +DA:303,0 +DA:304,0 +DA:305,0 +DA:306,0 +DA:313,0 +DA:314,0 +DA:316,0 +DA:317,0 +DA:319,0 +DA:320,0 +DA:321,0 +DA:323,0 +DA:326,1 +DA:328,1 +DA:330,0 +DA:335,0 +DA:337,0 +DA:338,0 +DA:341,0 +DA:347,0 +DA:348,0 +DA:349,0 +DA:350,0 +DA:353,1 +DA:354,1 +DA:355,3 +DA:356,1 +DA:357,0 +DA:360,1 +DA:363,1 +DA:364,1 +DA:365,0 +DA:368,1 +DA:371,0 +DA:373,0 +DA:374,0 +DA:375,0 +DA:376,0 +DA:377,0 +DA:378,0 +DA:382,0 +DA:384,0 +DA:385,0 +DA:386,0 +DA:387,0 +DA:388,0 +DA:391,0 +DA:392,0 +DA:395,0 +DA:396,0 +DA:399,1 +DA:401,2 +DA:403,0 +DA:406,2 +DA:407,3 +DA:408,1 +DA:409,0 +DA:412,2 +DA:413,0 +DA:417,2 +DA:418,0 +DA:420,0 +DA:421,0 +DA:422,0 +DA:423,0 +DA:426,0 +DA:430,1 +DA:431,3 +DA:432,3 +DA:434,0 +DA:437,3 +DA:439,2 +DA:440,2 +DA:441,2 +DA:442,1 +DA:443,0 +DA:444,0 +DA:447,0 +DA:449,0 +DA:451,0 +DA:454,0 +DA:459,0 +DA:461,0 +DA:464,0 +DA:465,0 +DA:466,0 +DA:467,0 +DA:468,0 +DA:471,0 +DA:476,0 +DA:477,0 +DA:478,0 +DA:479,0 +DA:480,0 +DA:481,0 +DA:482,0 +DA:483,0 +DA:484,0 +DA:488,0 +DA:489,0 +DA:491,0 +DA:495,0 +DA:496,0 +DA:498,0 +DA:501,0 +DA:502,0 +DA:504,0 +DA:506,0 +DA:509,0 +DA:512,0 +DA:513,0 +DA:514,0 +DA:518,0 +DA:520,0 +DA:523,0 +DA:524,0 +DA:525,0 +DA:528,0 +DA:531,0 +DA:532,0 +DA:533,0 +DA:537,0 +DA:542,0 +DA:545,0 +DA:547,0 +DA:550,0 +DA:551,0 +DA:552,0 +DA:553,0 +DA:556,0 +DA:557,0 +DA:558,0 +DA:559,0 +DA:563,0 +DA:564,0 +DA:565,0 +DA:569,0 +DA:587,1 +DA:588,2 +DA:590,0 +DA:593,0 +DA:594,0 +DA:596,0 +DA:597,0 +DA:599,0 +DA:600,0 +DA:601,0 +DA:611,0 +DA:612,0 +DA:613,0 +DA:614,0 +DA:616,0 +DA:620,0 +DA:621,0 +DA:622,0 +DA:623,0 +DA:624,0 +DA:626,0 +DA:627,0 +DA:633,0 +DA:645,1 +DA:649,1 +DA:651,0 +DA:654,1 +DA:655,4 +DA:656,1 +DA:657,1 +DA:660,0 +DA:661,0 +DA:662,0 +DA:663,0 +DA:664,0 +DA:667,1 +DA:669,1 +DA:670,5 +DA:672,0 +DA:673,0 +DA:681,0 +DA:682,0 +DA:684,0 +DA:685,0 +DA:687,0 +DA:689,0 +DA:690,0 +DA:691,0 +DA:695,1 +DA:696,2 +DA:697,4 +DA:698,1 +DA:699,0 +DA:702,4 +DA:704,1 +DA:706,2 +DA:707,0 +DA:709,3 +DA:710,3 +DA:711,1 +DA:715,1 +DA:716,6 +DA:719,0 +DA:720,0 +DA:721,0 +DA:723,0 +LF:319 +LH:121 +end_of_record +SF:lib\utils\firebase_utils.dart +DA:20,1 +DA:22,1 +DA:23,1 +DA:27,0 +DA:32,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:63,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:69,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:79,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:88,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:96,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:104,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:112,0 +DA:121,1 +DA:124,1 +DA:125,1 +DA:126,1 +DA:127,3 +DA:132,1 +DA:134,0 +DA:135,0 +DA:141,0 +DA:156,3 +DA:157,3 +DA:182,0 +DA:183,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:188,0 +DA:189,0 +DA:190,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:205,4 +DA:208,0 +DA:209,0 +DA:210,0 +DA:212,0 +LF:79 +LH:12 +end_of_record +SF:lib\utils\lock_auth.dart +DA:19,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:38,0 +DA:39,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:70,0 +DA:76,0 +DA:78,0 +LF:32 +LH:0 +end_of_record +SF:lib\utils\utils.dart +DA:35,1 +DA:36,5 +DA:46,1 +DA:47,1 +DA:49,3 +DA:50,7 +DA:53,1 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:76,0 +DA:77,0 +DA:79,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:87,0 +DA:91,0 +DA:92,0 +DA:95,0 +DA:96,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:109,0 +DA:111,0 +DA:112,0 +LF:29 +LH:7 +end_of_record +SF:lib\utils\app_info_utils.dart +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:54,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:86,0 +DA:87,0 +DA:89,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +LF:71 +LH:0 +end_of_record SF:lib\utils\crypto_utils.dart +DA:31,1 +DA:32,1 +DA:33,1 DA:34,1 DA:35,1 -DA:36,1 DA:37,1 DA:38,1 +DA:39,1 DA:40,1 -DA:41,1 -DA:42,1 -DA:43,1 -DA:46,3 -DA:48,1 -DA:52,1 +DA:43,3 +DA:45,1 +DA:49,1 +DA:51,1 +DA:53,4 DA:54,1 -DA:56,4 -DA:57,1 -DA:59,2 -DA:62,1 -DA:64,2 +DA:56,2 +DA:59,1 +DA:61,2 +DA:64,1 DA:67,1 +DA:69,1 DA:70,1 -DA:72,1 -DA:73,1 -DA:76,3 +DA:73,3 +DA:77,2 +DA:78,2 DA:80,2 DA:81,2 -DA:83,2 -DA:84,2 -DA:85,4 -DA:86,4 -DA:88,6 -DA:93,2 -DA:94,4 -DA:95,4 -DA:96,4 -DA:99,5 -DA:100,6 -DA:101,6 -DA:102,10 -DA:105,2 -DA:107,2 -DA:114,3 -DA:115,3 -DA:116,3 -DA:117,3 -LF:44 -LH:44 +DA:82,4 +DA:83,4 +DA:85,6 +LF:30 +LH:30 +end_of_record +SF:lib\widgets\home_widgets\home_widget_action.dart +DA:12,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:39,0 +DA:47,0 +DA:48,0 +DA:58,0 +DA:60,0 +DA:61,0 +LF:18 +LH:0 +end_of_record +SF:lib\widgets\home_widgets\home_widget_background.dart +DA:8,0 +DA:16,0 +DA:17,0 +DA:27,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +LF:11 +LH:0 +end_of_record +SF:lib\widgets\home_widgets\home_widget_configure.dart +DA:9,0 +DA:16,0 +DA:17,0 +DA:27,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +LF:8 +LH:0 +end_of_record +SF:lib\widgets\home_widgets\home_widget_copied.dart +DA:8,0 +DA:14,0 +DA:15,0 +DA:25,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:39,0 +DA:42,0 +LF:11 +LH:0 +end_of_record +SF:lib\widgets\home_widgets\home_widget_hidden.dart +DA:12,0 +DA:22,0 +DA:23,0 +DA:39,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +LF:13 +LH:0 +end_of_record +SF:lib\widgets\home_widgets\home_widget_otp.dart +DA:12,0 +DA:22,0 +DA:23,0 +DA:39,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:65,0 +DA:67,0 +DA:72,0 +DA:74,0 +DA:77,0 +DA:78,0 +DA:79,0 +LF:20 +LH:0 +end_of_record +SF:lib\widgets\home_widgets\home_widget_unlinked.dart +DA:8,0 +DA:14,0 +DA:15,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:37,0 +LF:11 +LH:0 +end_of_record +SF:lib\utils\image_converter.dart +DA:13,0 +DA:15,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:51,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:62,0 +DA:67,0 +DA:68,0 +DA:71,0 +DA:72,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:82,0 +DA:84,0 +DA:86,0 +DA:90,0 +DA:91,0 +DA:93,0 +DA:96,0 +DA:105,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:121,0 +DA:122,0 +DA:127,0 +DA:132,0 +DA:133,0 +DA:139,0 +DA:140,0 +DA:147,0 +DA:152,0 +DA:153,0 +DA:160,0 +DA:161,0 +DA:168,0 +DA:173,0 +DA:174,0 +DA:181,0 +DA:182,0 +DA:195,0 +DA:196,0 +DA:202,0 +DA:203,0 +DA:213,0 +DA:217,0 +DA:218,0 +DA:220,0 +DA:221,0 +DA:223,0 +DA:224,0 +DA:225,0 +DA:228,0 +DA:229,0 +DA:230,0 +DA:233,0 +DA:234,0 +DA:236,0 +DA:237,0 +DA:241,0 +DA:245,0 +DA:246,0 +DA:248,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:256,0 +DA:257,0 +DA:258,0 +DA:261,0 +DA:262,0 +DA:265,0 +DA:266,0 +LF:106 +LH:0 +end_of_record +SF:lib\utils\pi_mailer.dart +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:21,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:52,0 +DA:55,0 +LF:22 +LH:0 +end_of_record +SF:lib\utils\patch_notes_utils.dart +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:39,0 +LF:18 +LH:0 +end_of_record +SF:lib\widgets\dialog_widgets\patch_notes_dialog.dart +DA:15,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:54,0 +DA:55,0 +DA:57,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:66,0 +DA:69,0 +DA:70,0 +DA:80,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:94,0 +LF:46 +LH:0 +end_of_record +SF:lib\utils\pi_notifications.dart +DA:9,0 +DA:11,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:21,0 +DA:24,0 +DA:25,0 +DA:34,0 +DA:37,0 +DA:38,0 +DA:39,0 +LF:12 +LH:0 +end_of_record +SF:lib\views\add_token_manually_view\add_token_manually_view_widgets\labeled_dropdown_button.dart +DA:13,0 +DA:22,0 +DA:23,0 +DA:27,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:41,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +LF:26 +LH:0 +end_of_record +SF:lib\views\view_interface.dart +DA:6,0 +DA:10,17 +DA:14,17 +DA:18,17 +DA:22,17 +LF:5 +LH:4 +end_of_record +SF:lib\views\import_tokens_view\pages\import_start_page.dart +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:30,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:51,0 +DA:57,0 +DA:58,0 +DA:68,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:75,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:81,0 +DA:82,0 +DA:84,0 +DA:86,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:100,0 +DA:101,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:147,0 +DA:148,0 +DA:150,0 +DA:151,0 +DA:153,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:174,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:187,0 +DA:190,0 +DA:192,0 +DA:195,0 +DA:196,0 +DA:198,0 +DA:199,0 +DA:202,0 +DA:203,0 +DA:204,0 +DA:205,0 +DA:208,0 +DA:209,0 +DA:210,0 +DA:211,0 +DA:212,0 +DA:213,0 +DA:215,0 +DA:216,0 +DA:219,0 +DA:220,0 +DA:224,0 +DA:225,0 +DA:227,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:236,0 +DA:241,0 +DA:242,0 +DA:243,0 +DA:246,0 +DA:247,0 +DA:248,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:256,0 +DA:258,0 +DA:261,0 +DA:262,0 +DA:265,0 +DA:266,0 +DA:268,0 +DA:270,0 +DA:273,0 +DA:274,0 +DA:277,0 +DA:281,0 +DA:282,0 +DA:285,0 +DA:289,0 +DA:290,0 +DA:291,0 +DA:294,0 +DA:295,0 +DA:296,0 +DA:299,0 +DA:300,0 +DA:301,0 +DA:302,0 +DA:303,0 +DA:305,0 +DA:307,0 +DA:309,0 +DA:310,0 +DA:311,0 +DA:314,0 +DA:315,0 +DA:316,0 +DA:317,0 +DA:318,0 +DA:319,0 +DA:321,0 +DA:327,0 +DA:328,0 +DA:329,0 +DA:330,0 +DA:331,0 +DA:332,0 +DA:334,0 +LF:164 +LH:0 +end_of_record +SF:lib\views\import_tokens_view\pages\select_import_type_page.dart +DA:15,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:28,0 +DA:30,0 +DA:39,0 +DA:40,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:71,0 +DA:74,0 +DA:83,0 +DA:84,0 +LF:32 +LH:0 +end_of_record +SF:lib\views\import_tokens_view\pages\import_encrypted_data_page.dart +DA:18,0 +DA:26,0 +DA:28,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:47,0 +DA:49,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:71,0 +DA:72,0 +DA:74,0 +DA:78,0 +DA:79,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:100,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:122,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:146,0 +LF:57 +LH:0 +end_of_record +SF:lib\views\import_tokens_view\pages\import_plain_tokens_page.dart +DA:20,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:48,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:61,0 +DA:62,0 +DA:65,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:72,0 +DA:73,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:93,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:104,0 +DA:105,0 +DA:108,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:115,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:128,0 +DA:129,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:145,0 +DA:147,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:153,0 +DA:154,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:160,0 +DA:161,0 +DA:170,0 +DA:171,0 +DA:179,0 +DA:186,0 +DA:188,0 +DA:189,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:206,0 +DA:207,0 +DA:208,0 +DA:210,0 +DA:213,0 +DA:214,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:218,0 +DA:221,0 +DA:222,0 +LF:100 +LH:0 +end_of_record +SF:lib\views\import_tokens_view\widgets\conflicted_import_tokens_list.dart +DA:6,0 +DA:22,0 +DA:24,0 +DA:27,0 +DA:28,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:54,0 +LF:16 +LH:0 +end_of_record +SF:lib\views\import_tokens_view\widgets\conflicted_import_tokens_tile.dart +DA:11,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:37,0 +DA:38,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:57,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:71,0 +DA:74,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:94,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:105,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:120,0 +DA:121,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:138,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:145,0 +DA:147,0 +DA:148,0 +DA:161,0 +LF:79 +LH:0 +end_of_record +SF:lib\views\import_tokens_view\widgets\failed_imports_list.dart +DA:8,0 +DA:13,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:39,0 +DA:41,0 +DA:42,0 +LF:18 +LH:0 +end_of_record +SF:lib\views\import_tokens_view\widgets\no_conflict_import_tokens_list.dart +DA:7,0 +DA:23,0 +DA:24,0 +DA:28,0 +DA:30,0 +DA:33,0 +DA:34,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:54,0 +DA:55,0 +DA:58,0 +LF:15 +LH:0 +end_of_record +SF:lib\views\import_tokens_view\widgets\no_conflict_import_tokens_tile.dart +DA:13,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +LF:13 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\token_widget_builder.dart +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +LF:15 +LH:0 +end_of_record +SF:lib\widgets\push_request_listener.dart +DA:10,0 +DA:12,0 +DA:13,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:34,0 +LF:14 +LH:0 +end_of_record +SF:lib\widgets\status_bar.dart +DA:11,0 +DA:13,0 +DA:14,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:46,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:54,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:64,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:79,0 +DA:80,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:105,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:127,0 +DA:130,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:155,0 +DA:160,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:168,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:187,0 +DA:189,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:207,0 +DA:208,0 +DA:210,0 +DA:212,0 +DA:213,0 +DA:214,0 +DA:217,0 +LF:100 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\app_bar_item.dart +DA:4,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:20,0 +LF:8 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\connectivity_listener.dart +DA:11,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:24,0 +LF:9 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\expandable_appbar.dart +DA:10,0 +DA:17,0 +DA:18,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:60,0 +DA:61,0 +DA:66,0 +DA:68,0 +DA:69,0 +DA:71,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:89,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:111,0 +DA:112,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:122,0 +DA:123,0 +DA:130,0 +DA:131,0 +LF:63 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\main_view_navigation_bar.dart +DA:17,17 +DA:19,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:41,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:53,0 +DA:54,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:84,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:106,0 +DA:110,0 +DA:111,0 +DA:113,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +LF:69 +LH:1 +end_of_record +SF:lib\views\main_view\main_view_widgets\main_view_tokens_list.dart +DA:21,0 +DA:23,0 +DA:24,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:72,0 +DA:80,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:107,0 +DA:109,0 +LF:43 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\main_view_tokens_list_filtered.dart +DA:14,17 +DA:16,0 +DA:18,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +LF:30 +LH:1 +end_of_record +SF:lib\views\main_view\main_view_widgets\custom_paint_navigation_bar.dart +DA:8,0 +DA:23,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:51,0 +LF:24 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\drag_target_divider.dart +DA:20,34 +DA:29,0 +DA:30,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:43,0 +DA:44,0 +DA:48,0 +DA:50,0 +DA:51,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:63,0 +DA:64,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:84,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:97,0 +DA:104,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:115,0 +DA:117,0 +DA:119,0 +DA:122,0 +DA:125,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:133,0 +DA:137,0 +DA:139,0 +DA:140,0 +DA:142,0 +DA:143,0 +DA:145,0 +DA:148,0 +DA:149,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:157,0 +LF:64 +LH:1 +end_of_record +SF:lib\widgets\drag_item_scroller.dart +DA:10,0 +DA:29,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:55,0 +DA:56,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:84,0 +DA:85,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:97,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:107,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:133,0 +DA:134,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:143,0 +LF:71 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\filter_token_widget.dart +DA:9,0 +DA:11,0 +DA:12,0 +DA:16,17 +DA:18,0 +DA:19,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:43,0 +DA:44,0 +DA:46,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +LF:27 +LH:1 +end_of_record +SF:lib\views\main_view\main_view_widgets\folder_widgets\add_token_folder_dialog.dart +DA:12,0 +DA:14,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:50,0 +DA:51,0 +LF:24 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\folder_widgets\token_folder_actions.dart\delete_token_folder_action.dart +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:25,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:67,0 +DA:68,0 +LF:32 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\folder_widgets\token_folder_actions.dart\lock_token_folder_action.dart +DA:13,0 +DA:14,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:26,0 +DA:28,0 +DA:29,0 +LF:12 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\folder_widgets\token_folder_actions.dart\rename_token_folder_action.dart +DA:15,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:70,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:83,0 +DA:86,0 +DA:89,0 +LF:41 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\folder_widgets\token_folder_expandable.dart +DA:29,0 +DA:31,0 +DA:32,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:64,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:72,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:85,0 +DA:87,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:97,0 +DA:98,0 +DA:101,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:107,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:116,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:136,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:149,0 +DA:150,0 +DA:153,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:161,0 +DA:164,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:176,0 +DA:178,0 +DA:180,0 +DA:182,0 +DA:183,0 +DA:185,0 +DA:187,0 +DA:188,0 +DA:189,0 +DA:190,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:222,0 +DA:224,0 +DA:226,0 +DA:227,0 +DA:232,0 +DA:233,0 +DA:245,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:253,0 +DA:254,0 +DA:255,0 +LF:120 +LH:0 +end_of_record +SF:lib\widgets\custom_trailing.dart +DA:15,17 +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +LF:10 +LH:1 +end_of_record +SF:lib\views\main_view\main_view_widgets\folder_widgets\token_folder_widget.dart +DA:14,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:38,0 +DA:40,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:58,0 +DA:60,0 +LF:26 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\loading_indicator.dart +DA:10,0 +DA:12,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:27,17 +DA:29,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +LF:21 +LH:1 +end_of_record +SF:lib\widgets\focused_item_as_overlay.dart +DA:23,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:55,0 +DA:64,0 +DA:65,0 +DA:81,0 +DA:82,0 +DA:84,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:106,0 +DA:109,0 +DA:111,0 +DA:112,0 +DA:114,0 +DA:117,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:145,0 +DA:146,0 +DA:147,0 +DA:149,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:166,0 +DA:172,0 +DA:173,0 +DA:175,0 +DA:178,0 +DA:179,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:188,0 +DA:189,0 +DA:190,0 +DA:191,0 +DA:192,0 +DA:194,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:207,0 +DA:209,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:218,0 +DA:219,0 +DA:220,0 +DA:222,0 +DA:234,0 +DA:235,0 +DA:236,0 +DA:243,0 +DA:244,0 +DA:245,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:258,0 +DA:265,0 +DA:266,0 +DA:267,0 +DA:268,0 +DA:269,0 +DA:273,0 +DA:274,0 +DA:275,0 +DA:280,0 +DA:283,0 +DA:285,0 +DA:288,0 +DA:291,0 +LF:129 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\main_view_navigation_buttons\license_push_view_button.dart +DA:13,17 +DA:15,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:29,0 +DA:30,0 +DA:31,0 +LF:13 +LH:1 +end_of_record +SF:lib\views\main_view\main_view_widgets\main_view_navigation_buttons\qr_scanner_button.dart +DA:13,17 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:27,0 +DA:30,0 +DA:31,0 +DA:34,0 +LF:13 +LH:1 +end_of_record +SF:lib\widgets\deactivateable_refresh_indicator.dart +DA:8,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:24,0 +LF:8 +LH:0 +end_of_record +SF:lib\widgets\introduction_widgets\token_introduction.dart +DA:11,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:35,0 +LF:15 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\no_token_screen.dart +DA:6,17 +DA:8,0 +DA:10,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:34,0 +DA:35,0 +LF:18 +LH:1 +end_of_record +SF:lib\views\main_view\main_view_widgets\sortable_widget_builder.dart +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +LF:4 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\day_password_token_widgets\actions\edit_day_password_token_action.dart +DA:21,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:69,0 +DA:70,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:81,0 +DA:82,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:91,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:98,0 +DA:99,0 +DA:101,0 +DA:105,0 +DA:106,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:116,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:142,0 +DA:143,0 +LF:66 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\token_action.dart +DA:6,0 +LF:1 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\day_password_token_widgets\day_password_token_widget.dart +DA:12,0 +DA:14,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:20,0 +LF:6 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\token_widget.dart +DA:6,0 +LF:1 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\token_widget_base.dart +DA:25,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:54,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:64,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:86,0 +DA:88,0 +DA:89,0 +DA:91,0 +DA:92,0 +LF:36 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\day_password_token_widgets\day_password_token_widget_tile.dart +DA:22,0 +DA:24,0 +DA:25,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:48,0 +DA:50,0 +DA:51,0 +DA:54,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:64,0 +DA:65,0 +DA:69,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:97,0 +DA:98,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:114,0 +DA:116,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:124,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:131,0 +DA:132,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:146,0 +DA:152,0 +DA:154,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:161,0 +LF:92 +LH:0 end_of_record -SF:lib\utils\utils.dart -DA:37,1 -DA:38,5 -DA:48,1 -DA:49,1 -DA:51,3 -DA:52,7 -DA:55,1 -DA:58,4 -DA:59,8 -DA:60,8 -DA:65,2 -DA:73,8 -DA:74,8 -DA:75,8 -DA:76,48 -DA:77,16 -DA:80,5 -DA:81,15 -DA:85,1 -DA:86,2 +SF:lib\widgets\custom_texts.dart +DA:48,0 +DA:60,0 +DA:62,0 +DA:63,0 +DA:65,0 +DA:66,0 +LF:6 +LH:0 +end_of_record +SF:lib\widgets\hideable_widget_.dart +DA:12,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:27,0 +LF:7 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\token_widget_tile.dart +DA:6,0 +DA:19,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:56,0 +DA:67,0 +DA:71,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:77,0 +DA:78,0 +DA:83,0 +DA:87,0 +DA:88,0 +DA:93,0 +DA:97,0 +DA:99,0 +DA:100,0 +DA:107,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:113,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:128,0 +DA:132,0 +DA:133,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:139,0 +DA:140,0 +DA:144,0 +DA:145,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:167,0 +DA:169,0 +LF:65 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\default_token_actions\default_delete_action.dart +DA:17,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:28,0 +DA:30,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:45,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:81,0 +DA:82,0 +DA:83,0 +LF:38 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\default_token_actions\default_edit_action.dart +DA:19,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:60,0 +DA:61,0 +DA:65,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:84,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:97,0 +DA:100,0 +DA:103,0 +LF:45 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\default_token_actions\default_lock_action.dart +DA:17,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:39,0 +DA:41,0 +DA:42,0 +LF:17 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\hotp_token_widgets\actions\edit_hotp_token_action.dart +DA:22,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:77,0 +DA:78,0 +DA:81,0 +DA:82,0 +DA:83,0 DA:87,0 -DA:88,0 -DA:92,0 -DA:93,0 +DA:89,0 +DA:90,0 +DA:91,0 DA:94,0 +DA:95,0 DA:97,0 DA:101,0 DA:102,0 -DA:103,0 DA:104,0 DA:105,0 DA:106,0 DA:107,0 -DA:108,0 -DA:109,0 DA:112,0 -DA:113,0 +DA:114,0 +DA:115,0 DA:116,0 DA:117,0 DA:122,0 +DA:123,0 DA:124,0 -DA:126,0 -DA:132,0 -LF:43 -LH:20 +DA:127,0 +DA:128,0 +DA:129,0 +DA:133,0 +DA:134,0 +LF:61 +LH:0 end_of_record -SF:lib\model\tokens\otp_token.dart -DA:10,4 +SF:lib\views\main_view\main_view_widgets\token_widgets\hotp_token_widgets\hotp_token_widget.dart +DA:13,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:24,0 +LF:6 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\hotp_token_widgets\hotp_token_widget_tile.dart +DA:19,0 +DA:21,0 +DA:22,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:36,0 +DA:37,0 +DA:38,0 DA:40,0 +DA:41,0 DA:42,0 -LF:3 -LH:1 +DA:43,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:59,0 +DA:60,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:101,0 +DA:102,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:113,0 +LF:60 +LH:0 end_of_record -SF:lib\model\tokens\hotp_token.dart -DA:16,4 -DA:30,8 -DA:32,3 -DA:33,3 -DA:34,3 -DA:35,3 -DA:36,3 -DA:37,6 -DA:41,4 -DA:43,2 -DA:58,2 -DA:59,1 -DA:60,2 -DA:61,2 -DA:62,2 -DA:63,2 -DA:64,2 -DA:65,2 -DA:66,2 -DA:67,2 -DA:68,2 -DA:69,2 -DA:70,3 +SF:lib\views\main_view\main_view_widgets\token_widgets\push_token_widgets\actions\edit_push_token_action.dart +DA:20,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:64,0 +DA:65,0 +DA:67,0 +DA:68,0 +DA:72,0 DA:73,0 +DA:74,0 DA:75,0 -DA:78,2 -DA:79,3 -DA:80,5 -DA:83,2 -DA:84,2 -DA:85,2 -DA:86,2 -DA:87,4 -DA:88,2 -DA:89,4 -DA:90,2 -DA:91,2 -DA:92,2 -DA:93,2 -DA:96,0 +DA:79,0 +DA:80,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:98,0 DA:101,0 DA:103,0 -LF:42 -LH:37 +DA:107,0 +DA:108,0 +DA:113,0 +DA:116,0 +DA:117,0 +DA:120,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:130,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:174,0 +DA:175,0 +LF:81 +LH:0 end_of_record -SF:lib\model\tokens\hotp_token.g.dart -DA:9,0 -DA:10,0 +SF:lib\widgets\enable_text_form_field_after_many_taps.dart DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 +DA:19,0 +DA:20,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +LF:18 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\push_token_widgets\push_token_widget.dart +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:26,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +LF:18 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\push_token_widgets\push_token_widget_tile.dart DA:15,0 -DA:16,0 DA:17,0 -DA:18,0 DA:19,0 DA:20,0 DA:21,0 DA:22,0 +DA:23,0 +DA:24,0 DA:25,0 -DA:26,0 -DA:27,0 -DA:28,0 -DA:29,0 DA:30,0 DA:31,0 -DA:32,0 DA:33,0 DA:34,0 -DA:35,0 DA:36,0 DA:37,0 DA:38,0 -LF:28 +LF:16 LH:0 end_of_record -SF:lib\utils\custom_int_buffer.dart -DA:9,4 -DA:14,3 -DA:15,5 -DA:16,3 -DA:19,2 -DA:20,2 -DA:21,0 -DA:24,6 -DA:25,0 -DA:28,2 -DA:31,3 -DA:32,14 -DA:33,6 -DA:36,3 -DA:38,6 -DA:40,2 -DA:42,2 -LF:17 -LH:15 -end_of_record -SF:lib\utils\custom_int_buffer.g.dart -DA:9,1 -DA:10,1 -DA:11,4 -DA:13,1 -DA:14,1 -DA:15,1 -LF:6 -LH:6 -end_of_record -SF:lib\utils\rsa_utils.dart -DA:34,66 -DA:43,2 -DA:44,6 -DA:45,6 -DA:46,6 -DA:48,2 -DA:58,2 -DA:59,2 -DA:60,6 -DA:61,6 -DA:63,4 -DA:78,1 -DA:79,3 -DA:81,2 -DA:83,3 -DA:85,2 -DA:86,0 -DA:87,0 -DA:93,2 -DA:95,3 -DA:97,3 -DA:98,3 -DA:100,1 -DA:115,1 -DA:116,1 -DA:117,1 -DA:118,2 -DA:119,2 -DA:121,1 -DA:122,3 -DA:123,3 -DA:125,2 -DA:127,1 -DA:128,1 -DA:129,1 -DA:130,2 -DA:151,2 -DA:152,2 -DA:153,4 -DA:154,6 -DA:155,6 -DA:156,6 -DA:157,6 -DA:158,6 -DA:159,14 -DA:160,14 -DA:161,10 -DA:163,4 -DA:184,1 -DA:185,3 -DA:186,3 -DA:187,3 -DA:188,3 -DA:189,3 -DA:191,1 -DA:195,1 -DA:196,1 -DA:197,2 -DA:201,2 -DA:202,0 -DA:203,0 -DA:215,0 -DA:217,0 -DA:220,0 -DA:222,0 -DA:223,0 -DA:231,0 -DA:237,2 -DA:238,2 -DA:239,4 -DA:240,2 -DA:245,2 -DA:246,12 -DA:248,2 -DA:250,6 -DA:253,0 -DA:254,0 -DA:257,1 -DA:258,1 -DA:259,2 -DA:261,2 -LF:81 -LH:69 -end_of_record -SF:lib\model\tokens\totp_token.dart -DA:19,1 -DA:20,1 -DA:21,1 -DA:22,2 -DA:23,1 -DA:24,2 -DA:25,1 -DA:29,1 -DA:43,1 -DA:44,2 -DA:46,1 -DA:61,1 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:70,0 -DA:71,0 -DA:72,0 -DA:73,1 -DA:77,0 -DA:79,0 -DA:82,1 -DA:83,2 -DA:84,3 -DA:85,3 -DA:88,1 -DA:89,1 -DA:90,1 -DA:91,1 -DA:92,2 -DA:93,1 -DA:94,1 -DA:95,2 -DA:96,1 -DA:97,1 -DA:98,1 -DA:101,0 -DA:106,2 -DA:108,0 -DA:109,0 -DA:110,0 -DA:113,0 -DA:114,0 -DA:115,0 -DA:118,2 -LF:50 -LH:30 -end_of_record -SF:lib\model\tokens\totp_token.g.dart -DA:9,2 -DA:10,1 -DA:11,1 -DA:12,1 -DA:13,1 -DA:14,2 -DA:15,1 -DA:16,1 -DA:17,1 -DA:18,1 -DA:19,1 -DA:20,1 -DA:21,1 -DA:22,1 -DA:25,2 -DA:26,1 -DA:27,1 -DA:28,1 -DA:29,1 -DA:30,1 -DA:31,1 -DA:32,1 -DA:33,1 -DA:34,1 -DA:35,2 -DA:36,1 -DA:37,1 -DA:38,1 -LF:28 -LH:28 -end_of_record -SF:lib\repo\preference_settings_repository.dart +SF:lib\views\main_view\main_view_widgets\token_widgets\push_token_widgets\rollout_failed_widget.dart +DA:14,0 +DA:16,0 +DA:18,0 DA:19,0 DA:20,0 +DA:22,0 DA:23,0 DA:25,0 DA:26,0 DA:27,0 DA:28,0 -DA:29,0 -DA:31,0 -DA:32,0 DA:33,0 -DA:34,0 -DA:44,0 -DA:47,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 DA:49,0 DA:50,0 DA:51,0 DA:52,0 DA:53,0 DA:54,0 +DA:55,0 DA:56,0 -DA:57,0 -DA:59,0 -DA:60,0 -DA:61,0 -LF:25 +DA:69,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:75,0 +DA:76,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:93,0 +DA:94,0 +LF:46 LH:0 end_of_record -SF:lib\repo\preference_token_folder_repository.dart -DA:12,0 -DA:14,0 +SF:lib\views\main_view\main_view_widgets\token_widgets\push_token_widgets\rollout_widget.dart +DA:8,0 +DA:10,0 +DA:11,0 +DA:13,0 +DA:15,0 +DA:16,0 DA:17,0 +LF:7 +LH:0 +end_of_record +SF:lib\widgets\press_button.dart +DA:9,0 +DA:11,0 +DA:12,0 DA:18,0 DA:19,0 DA:20,0 +DA:21,0 DA:23,0 DA:24,0 -DA:28,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +LF:17 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\token_widget_slideable.dart +DA:13,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:30,0 DA:31,0 -DA:32,0 DA:33,0 -DA:34,0 DA:36,0 -LF:14 +LF:11 LH:0 end_of_record -SF:lib\utils\logger.dart +SF:lib\views\main_view\main_view_widgets\token_widgets\totp_token_widgets\totp_token_widget.dart +DA:15,0 DA:22,0 -DA:27,0 +DA:24,0 +DA:25,0 +DA:26,0 DA:28,0 -DA:29,15 -DA:30,5 -DA:38,5 -DA:40,5 -DA:41,5 +LF:6 +LH:0 +end_of_record +SF:lib\views\main_view\main_view_widgets\token_widgets\totp_token_widgets\totp_token_widget_tile.dart +DA:20,0 +DA:22,0 +DA:23,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:42,0 DA:46,0 -DA:47,0 DA:48,0 DA:49,0 DA:51,0 +DA:53,0 DA:54,0 DA:55,0 DA:56,0 -DA:57,0 +DA:59,0 +DA:61,0 +DA:62,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 DA:70,0 DA:71,0 -DA:72,0 DA:73,0 DA:74,0 -DA:75,0 -DA:76,5 +DA:77,0 DA:78,0 DA:81,0 -DA:82,0 DA:83,0 -DA:88,5 -DA:93,0 -DA:99,15 -DA:100,5 -DA:101,15 +DA:84,0 +DA:87,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:105,0 DA:106,0 +DA:107,0 DA:108,0 DA:110,0 -DA:115,5 -DA:116,10 -DA:117,10 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:117,0 DA:118,0 -DA:120,5 -DA:123,1 -DA:124,2 -DA:125,2 +DA:123,0 +DA:124,0 +DA:125,0 DA:126,0 -DA:128,1 +DA:127,0 +DA:128,0 +DA:129,0 DA:131,0 DA:132,0 DA:133,0 -DA:134,0 DA:135,0 +DA:136,0 DA:137,0 DA:138,0 DA:139,0 +DA:141,0 DA:142,0 DA:143,0 -DA:144,0 -DA:145,0 +DA:147,0 DA:148,0 DA:149,0 +DA:150,0 DA:151,0 -DA:153,0 -DA:157,0 -DA:158,0 -DA:161,0 -DA:162,0 -DA:163,0 -DA:164,0 -DA:170,0 -DA:171,0 -DA:172,0 -DA:173,0 -DA:174,0 -DA:175,0 -DA:179,0 -DA:180,0 -DA:181,0 -DA:182,0 -DA:183,0 -DA:184,0 -DA:187,0 -DA:189,0 -DA:195,0 -DA:196,0 -DA:199,0 -DA:200,0 -DA:201,0 -DA:202,0 -DA:203,0 -DA:204,0 -DA:205,0 -DA:206,0 -DA:216,5 -DA:217,5 -DA:218,5 -DA:219,5 -DA:220,5 -DA:223,5 -DA:224,10 -DA:225,5 -DA:228,0 -DA:229,0 -DA:230,0 -DA:231,0 -DA:232,0 -DA:233,0 -DA:234,0 -DA:235,0 -DA:238,0 -DA:239,0 -DA:242,0 -DA:246,5 -DA:247,10 -DA:250,5 -DA:251,10 -DA:252,0 -DA:255,5 -DA:256,10 -DA:257,0 -DA:258,0 -DA:261,5 -DA:262,0 -DA:263,0 -DA:268,10 -DA:269,5 -DA:271,0 -DA:272,0 -DA:273,0 -DA:274,0 -DA:276,5 -DA:279,5 -DA:284,5 -DA:286,10 -DA:289,5 -DA:291,10 -DA:294,0 -DA:296,0 -DA:301,0 -DA:302,0 -DA:303,0 -DA:304,0 -DA:305,0 -DA:306,0 -DA:307,0 -DA:309,0 -DA:310,0 -DA:311,0 -DA:312,0 -DA:313,0 -DA:322,0 -DA:323,0 -DA:324,0 -DA:325,0 -DA:326,0 -DA:327,0 -DA:334,0 -DA:335,0 -DA:336,0 -DA:337,0 -DA:342,5 -DA:343,15 -DA:344,10 -DA:345,8 -DA:346,5 -DA:348,5 -DA:350,15 -DA:351,5 -DA:352,25 -DA:353,25 -DA:354,15 -DA:359,0 -DA:360,0 -DA:361,0 -DA:362,0 -DA:363,0 -DA:364,0 -DA:365,0 -DA:366,0 -DA:367,0 -DA:368,0 -DA:369,0 -DA:370,0 -DA:371,0 -DA:372,0 -DA:373,0 -DA:374,0 -DA:375,0 -DA:376,0 -DA:377,0 -DA:378,0 -DA:379,0 -DA:380,0 -DA:381,0 -DA:382,0 -DA:383,0 -DA:384,0 -DA:385,0 -DA:386,0 -DA:387,0 -DA:388,0 -DA:389,0 -DA:390,0 -DA:391,0 -DA:392,0 -DA:395,0 -DA:396,0 -DA:397,0 -DA:398,0 -DA:399,0 -DA:400,0 -DA:401,0 -DA:402,0 -DA:403,0 -DA:404,0 -DA:405,0 -DA:406,0 -DA:408,0 -LF:217 -LH:52 +LF:84 +LH:0 end_of_record -SF:lib\repo\secure_token_repository.dart -DA:33,32 -DA:36,0 +SF:lib\views\main_view\main_view_widgets\token_widgets\totp_token_widgets\actions\edit_totp_token_action.dart +DA:19,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:38,0 +DA:39,0 DA:40,0 -DA:53,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:54,0 DA:55,0 DA:56,0 DA:57,0 DA:58,0 -DA:61,0 +DA:60,0 DA:62,0 DA:63,0 DA:65,0 +DA:66,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:77,0 +DA:78,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:87,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:94,0 +DA:95,0 +DA:97,0 +DA:101,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:112,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:138,0 +DA:139,0 +LF:64 +LH:0 +end_of_record +SF:lib\views\push_token_view\widgets\push_tokens_view_list.dart +DA:16,17 +DA:18,0 +DA:19,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 DA:70,0 +DA:71,0 DA:72,0 -DA:82,0 +DA:73,0 +DA:80,0 +DA:81,0 DA:84,0 +DA:87,0 +DA:89,0 +LF:37 +LH:1 +end_of_record +SF:lib\views\qr_scanner_view\qr_scanner_view_widgets\qr_scanner_widget.dart +DA:13,0 +DA:15,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:41,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:55,0 +DA:58,0 +DA:59,0 +DA:66,17 +DA:68,0 +DA:69,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:83,0 +DA:85,0 DA:86,0 -DA:88,0 DA:89,0 DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 DA:96,0 DA:97,0 -DA:98,0 -DA:101,0 DA:102,0 +DA:104,0 +DA:106,0 +DA:107,0 +DA:109,0 DA:111,0 DA:112,0 -DA:113,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:121,0 +DA:124,0 DA:125,0 -DA:126,0 -DA:128,0 +DA:129,0 +DA:130,0 DA:131,0 +DA:134,0 DA:135,0 -DA:137,0 +DA:136,0 DA:138,0 DA:139,0 DA:140,0 +DA:141,0 +DA:142,0 DA:143,0 DA:144,0 -DA:145,0 -DA:151,0 -DA:153,0 -DA:155,0 -DA:158,0 -DA:168,0 -DA:170,0 -DA:175,0 -DA:177,0 -LF:48 +DA:149,0 +DA:150,0 +LF:70 LH:1 end_of_record -SF:lib\state_notifiers\app_state_notifier.dart -DA:6,2 -DA:8,1 -DA:9,1 -LF:3 -LH:3 -end_of_record -SF:lib\state_notifiers\deeplink_notifier.dart -DA:13,0 -DA:14,0 -DA:15,0 +SF:lib\views\qr_scanner_view\qr_scanner_view_widgets\qr_code_scanner_overlay.dart +DA:11,0 DA:18,0 -DA:20,0 DA:21,0 -DA:26,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:42,0 -DA:45,0 -DA:47,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:57,0 -LF:25 -LH:0 -end_of_record -SF:lib\state_notifiers\push_request_notifier.dart -DA:41,1 -DA:48,0 -DA:50,1 -DA:51,2 -DA:55,1 -DA:56,2 -DA:57,1 -DA:58,1 -DA:59,1 -DA:61,0 -DA:64,1 -DA:68,1 -DA:69,2 -DA:70,1 -DA:71,1 -DA:72,1 -DA:74,0 -DA:77,1 -DA:81,0 -DA:83,1 -DA:84,1 -DA:86,3 -DA:87,2 -DA:90,3 -DA:91,2 -DA:92,1 -DA:94,3 -DA:95,2 -DA:105,1 -DA:106,1 -DA:107,1 -DA:110,2 -DA:111,1 -DA:114,4 -DA:115,2 -DA:116,0 -DA:119,0 -LF:37 -LH:31 -end_of_record -SF:lib\utils\firebase_utils.dart -DA:15,1 -DA:17,1 -DA:18,1 DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 DA:27,0 +DA:28,0 +DA:29,0 DA:30,0 DA:31,0 +DA:32,0 DA:34,0 -DA:39,0 +DA:38,0 DA:40,0 -DA:41,0 DA:42,0 -DA:43,0 -DA:48,0 +DA:46,0 +DA:47,0 DA:49,0 DA:50,0 -DA:51,0 DA:53,0 +DA:55,0 DA:56,0 DA:57,0 DA:60,0 -DA:62,0 -DA:63,0 +DA:61,0 +DA:64,0 DA:65,0 -DA:66,0 +DA:68,0 DA:69,0 -DA:70,0 -DA:71,0 DA:72,0 -DA:76,0 +DA:73,0 +DA:77,0 DA:78,0 DA:79,0 DA:80,0 -DA:81,0 -DA:85,0 -DA:87,0 +DA:82,0 +DA:83,0 +DA:84,0 DA:88,0 DA:89,0 +DA:90,0 +DA:91,0 DA:93,0 +DA:94,0 +DA:95,0 DA:96,0 -DA:97,0 DA:98,0 -DA:101,0 -DA:103,0 -DA:104,0 +DA:100,0 DA:105,0 -DA:109,0 -DA:118,0 -DA:121,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:115,0 +DA:117,0 DA:122,0 DA:123,0 DA:124,0 DA:125,0 -DA:126,0 -DA:131,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 DA:132,0 -DA:137,0 +DA:134,0 DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:144,0 DA:145,0 -LF:59 -LH:3 +DA:146,0 +DA:147,0 +DA:149,0 +DA:151,0 +DA:156,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +LF:86 +LH:0 end_of_record -SF:lib\utils\network_utils.dart -DA:31,48 +SF:lib\views\settings_view\settings_groups\import_export_tokens_widgets\dialogs\export_tokens_to_file_dialog.dart +DA:17,0 +DA:19,0 +DA:20,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:35,0 DA:36,0 +DA:37,0 DA:38,0 DA:39,0 DA:40,0 DA:41,0 DA:42,0 +DA:43,0 DA:44,0 -DA:47,0 -DA:48,0 -DA:51,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:60,0 +DA:45,0 +DA:46,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 DA:61,0 DA:62,0 DA:63,0 DA:64,0 +DA:65,0 DA:66,0 -DA:70,0 -DA:71,0 -DA:72,0 +DA:67,0 +DA:68,0 +DA:69,0 DA:73,0 -DA:74,0 +DA:75,0 DA:76,0 -DA:80,0 -DA:81,0 -DA:82,0 +DA:77,0 +DA:78,0 DA:85,0 DA:86,0 -DA:89,0 -DA:92,0 +DA:88,0 +DA:96,0 DA:97,0 DA:98,0 DA:99,0 DA:100,0 -DA:101,0 DA:102,0 DA:103,0 +DA:104,0 DA:105,0 +DA:108,0 DA:109,0 DA:110,0 -DA:111,0 -DA:112,0 DA:113,0 DA:115,0 DA:117,0 -DA:119,0 DA:120,0 DA:121,0 -DA:125,0 -DA:127,0 +DA:123,0 +DA:126,0 DA:128,0 DA:129,0 DA:130,0 -DA:131,0 DA:132,0 +DA:135,0 DA:136,0 -DA:137,0 -DA:140,0 -DA:144,0 -LF:62 -LH:1 -end_of_record -SF:lib\utils\push_provider.dart -DA:54,0 -DA:58,0 -DA:59,0 -DA:60,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:70,0 -DA:76,0 -DA:79,0 -DA:82,0 -DA:87,0 -DA:95,0 -DA:97,0 -DA:99,0 -DA:103,0 -DA:104,0 -DA:105,0 -DA:107,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:111,0 -DA:113,0 -DA:114,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:124,0 -DA:125,0 -DA:126,0 -DA:127,0 -DA:129,0 -DA:130,0 -DA:138,0 -DA:139,0 -DA:140,0 DA:141,0 +DA:142,0 DA:143,0 -DA:144,0 DA:145,0 DA:146,0 -DA:148,0 +DA:149,0 DA:150,0 -DA:152,0 DA:155,0 DA:156,0 -DA:159,0 -DA:161,0 -DA:167,0 -DA:168,0 -DA:169,0 -DA:170,0 -DA:172,0 -DA:173,0 -DA:174,0 -DA:175,0 -DA:177,0 -DA:179,0 -DA:181,0 -DA:184,0 -DA:185,0 -DA:188,0 -DA:189,0 -DA:192,0 -DA:193,0 -DA:194,0 -DA:195,0 -DA:197,0 -DA:200,0 -DA:201,0 -DA:202,0 -DA:203,0 -DA:206,0 -DA:208,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:215,0 -DA:216,0 -DA:217,0 -DA:218,0 -DA:225,0 -DA:226,0 -DA:227,0 -DA:228,0 -DA:229,0 -DA:233,0 -DA:236,0 -DA:237,0 -DA:238,0 -DA:243,0 -DA:244,0 -DA:245,0 -DA:247,0 -DA:248,0 -DA:255,0 -DA:256,0 -DA:258,0 -DA:260,0 -DA:261,0 -DA:262,0 -DA:264,0 -DA:267,0 -DA:268,0 -DA:275,0 -DA:276,0 -DA:278,0 -DA:279,0 -DA:282,0 -DA:283,0 -DA:285,0 -DA:286,0 -DA:287,0 -DA:291,0 -DA:292,0 -DA:293,0 -DA:297,0 -DA:298,0 -DA:301,0 -DA:307,0 -DA:308,0 -DA:310,0 -DA:312,0 -DA:314,0 -DA:315,0 -DA:316,0 -DA:321,0 -DA:334,0 -DA:340,0 -DA:344,0 -DA:353,0 -DA:355,0 -DA:358,0 -DA:363,0 -DA:364,0 -DA:365,0 -DA:366,0 -DA:368,0 -DA:369,0 -DA:371,0 -DA:377,0 -LF:143 +DA:157,0 +DA:158,0 +LF:77 +LH:0 +end_of_record +SF:lib\views\settings_view\settings_groups\import_export_tokens_widgets\dialogs\select_export_type_dialog.dart +DA:12,17 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:39,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:46,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:54,0 +DA:57,0 +LF:26 +LH:1 +end_of_record +SF:lib\views\settings_view\settings_view_widgets\settings_list_tile_button.dart +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:31,0 +LF:14 +LH:0 +end_of_record +SF:lib\views\settings_view\settings_groups\import_export_tokens_widgets\dialogs\select_tokens_dialog.dart +DA:14,0 +DA:16,0 +DA:17,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:49,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:60,0 +DA:62,0 +DA:64,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:91,0 +DA:92,0 +DA:97,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:121,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:137,0 +LF:70 LH:0 end_of_record -SF:lib\state_notifiers\settings_notifier.dart -DA:16,1 -DA:20,1 -DA:21,1 -DA:23,1 -DA:24,3 -DA:25,3 -DA:26,3 -DA:30,1 -DA:31,3 -DA:32,3 -DA:35,1 -DA:36,2 -DA:37,3 -DA:38,3 -DA:39,1 +SF:lib\views\settings_view\settings_groups\import_export_tokens_widgets\dialogs\show_qr_code_dialog.dart +DA:18,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 DA:42,0 DA:43,0 DA:44,0 DA:45,0 +DA:47,0 DA:48,0 DA:49,0 -DA:50,0 -DA:51,0 -DA:54,0 DA:55,0 DA:56,0 -DA:57,0 -DA:60,1 -DA:61,2 -DA:62,3 -DA:63,1 -DA:66,1 -DA:67,2 -DA:68,3 -DA:69,1 +DA:58,0 +DA:59,0 +DA:60,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 DA:72,0 DA:73,0 DA:74,0 -DA:75,0 -DA:78,0 +DA:77,0 DA:79,0 DA:80,0 DA:81,0 -DA:84,1 -DA:85,2 -DA:86,3 -DA:87,1 -DA:90,0 -DA:91,0 -DA:92,0 +DA:82,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 DA:93,0 -DA:96,1 -DA:97,2 -DA:98,3 -DA:99,1 -DA:102,1 -DA:103,2 -DA:104,2 -DA:105,3 -DA:106,1 -DA:109,1 -DA:110,2 -DA:111,3 -DA:112,1 -LF:64 -LH:40 +LF:46 +LH:0 end_of_record -SF:lib\state_notifiers\token_folder_notifier.dart -DA:11,1 -DA:13,1 -DA:14,1 -DA:17,8 -DA:19,1 -DA:20,3 -DA:21,2 -DA:22,1 +SF:lib\views\settings_view\settings_groups\settings_group_error_log.dart +DA:8,17 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:22,0 DA:23,0 -DA:28,1 -DA:29,2 -DA:30,1 -DA:31,2 -DA:34,1 -DA:35,2 -DA:36,1 -DA:37,2 -DA:40,1 -DA:41,3 -DA:42,1 -DA:43,2 -DA:46,1 -DA:47,2 -DA:48,1 -DA:49,2 +DA:24,0 +DA:28,0 +DA:30,0 +LF:14 +LH:1 +end_of_record +SF:lib\views\settings_view\settings_view_widgets\logging_menu.dart +DA:12,17 +DA:14,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:37,0 +DA:39,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:54,0 +LF:22 +LH:1 +end_of_record +SF:lib\views\settings_view\settings_view_widgets\settings_groups.dart +DA:29,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:49,0 +DA:51,0 +LF:11 +LH:0 +end_of_record +SF:lib\views\settings_view\settings_groups\settings_group_general.dart +DA:14,17 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:50,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:61,0 +LF:28 +LH:1 +end_of_record +SF:lib\views\settings_view\settings_groups\settings_group_import_export_tokens.dart +DA:12,17 +DA:14,0 +DA:15,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:61,0 +LF:23 +LH:1 +end_of_record +SF:lib\views\settings_view\settings_groups\settings_group_language.dart +DA:9,17 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:46,0 +DA:47,0 LF:25 -LH:24 -end_of_record -SF:lib\state_notifiers\token_notifier.dart -DA:45,1 -DA:58,1 -DA:59,1 -DA:60,1 -DA:62,1 -DA:65,1 -DA:66,3 -DA:67,2 -DA:68,1 -DA:69,0 -DA:70,0 -DA:73,0 -DA:75,2 -DA:76,4 -DA:81,1 -DA:82,3 -DA:83,2 -DA:84,3 -DA:88,1 -DA:91,3 -DA:92,2 -DA:93,1 -DA:94,1 -DA:95,1 -DA:98,4 -DA:99,2 -DA:100,2 -DA:101,1 -DA:110,1 -DA:111,1 -DA:114,2 -DA:118,5 -DA:119,2 -DA:120,3 -DA:124,1 -DA:125,6 -DA:128,1 -DA:129,3 -DA:130,3 -DA:131,2 -DA:134,1 -DA:135,3 -DA:136,2 -DA:139,1 -DA:140,3 -DA:141,2 -DA:145,1 -DA:146,3 -DA:147,1 -DA:150,0 -DA:151,0 -DA:152,0 -DA:155,0 -DA:156,0 -DA:159,0 -DA:162,0 -DA:164,0 -DA:167,1 -DA:170,1 -DA:177,2 -DA:179,3 -DA:181,0 -DA:182,0 -DA:184,0 -DA:185,0 -DA:186,0 -DA:187,0 -DA:188,0 -DA:194,1 -DA:195,0 -DA:196,0 -DA:197,0 -DA:201,1 -DA:202,0 -DA:205,1 -DA:208,0 -DA:210,0 -DA:211,0 -DA:216,1 -DA:217,1 -DA:218,9 -DA:219,5 -DA:221,0 -DA:224,1 -DA:225,2 -DA:226,1 -DA:227,1 -DA:228,1 -DA:229,1 -DA:230,1 -DA:233,1 -DA:234,0 -DA:236,1 -DA:237,0 -DA:238,5 -DA:241,0 -DA:244,0 -DA:248,1 -DA:250,2 -DA:251,0 -DA:252,0 -DA:253,0 -DA:259,1 -DA:260,1 -DA:262,4 -DA:266,1 -DA:267,3 -DA:268,8 -DA:271,0 -DA:274,1 -DA:275,1 -DA:277,4 -DA:281,1 -DA:282,2 -DA:283,2 -DA:284,3 -DA:285,1 -DA:286,2 -DA:287,2 -DA:288,2 -DA:289,2 -DA:290,4 -DA:294,2 -DA:295,0 -DA:296,0 -DA:297,0 -DA:298,0 -DA:302,0 -DA:305,1 -DA:306,0 -DA:309,1 -DA:310,2 -DA:312,2 -DA:313,2 -DA:314,2 -DA:315,1 -DA:316,4 -DA:317,1 -DA:319,0 -DA:320,0 -DA:325,2 -DA:328,2 -DA:329,1 -DA:330,1 -DA:331,1 -DA:332,1 -DA:333,1 -DA:334,2 -DA:335,3 -DA:339,2 -DA:340,2 -DA:342,1 -DA:343,1 -DA:344,0 -DA:345,0 -DA:346,0 -DA:347,0 -DA:350,1 -DA:351,1 -DA:352,1 -DA:356,0 -DA:358,0 -DA:362,0 -DA:363,0 -DA:364,0 -DA:366,0 -DA:367,0 -DA:368,0 -DA:371,0 -DA:375,0 -DA:376,0 -DA:377,0 -DA:378,0 -DA:381,0 -DA:382,0 -DA:383,0 -DA:384,0 -DA:385,0 -DA:388,0 -DA:390,0 -DA:391,0 -DA:392,0 -DA:396,0 -DA:397,0 -DA:403,1 -DA:404,2 -DA:406,4 -DA:407,1 -DA:409,1 -DA:411,2 -DA:412,0 -DA:413,0 -LF:192 -LH:120 +LH:1 end_of_record -SF:lib\utils\qr_parser.dart -DA:27,17 -DA:30,1 -DA:31,1 -DA:32,1 -DA:40,2 -DA:41,1 -DA:44,2 -DA:48,1 -DA:49,2 -DA:50,2 -DA:51,2 -DA:52,1 -DA:53,2 +SF:lib\views\settings_view\settings_groups\settings_group_push_token.dart +DA:13,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:40,0 +DA:44,0 +DA:45,0 +DA:51,0 +DA:52,0 +DA:53,0 DA:54,0 -DA:57,1 -DA:60,1 +DA:55,0 +DA:56,0 +DA:57,0 +DA:60,0 +DA:61,0 +DA:63,0 DA:64,0 +DA:65,0 DA:77,0 -DA:79,0 +DA:78,0 +DA:81,0 DA:82,0 -DA:85,0 +DA:83,0 +DA:86,0 +DA:87,0 +DA:88,0 DA:89,0 +DA:90,0 DA:91,0 -DA:93,0 -DA:94,0 -DA:96,0 -DA:100,0 +DA:92,0 +DA:97,0 +DA:98,0 DA:101,0 +DA:102,0 +DA:103,0 DA:104,0 -DA:105,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:112,0 DA:113,0 -DA:115,0 +DA:114,0 DA:116,0 +DA:117,0 DA:118,0 DA:119,0 DA:120,0 -DA:123,0 -DA:125,0 -DA:126,0 -DA:127,0 -DA:130,0 -DA:131,0 -DA:133,0 -DA:136,0 -DA:137,0 -DA:145,1 -DA:148,1 -DA:151,2 -DA:155,3 -DA:156,3 -DA:158,1 -DA:163,1 -DA:164,1 -DA:165,1 -DA:168,3 -DA:169,0 -DA:172,2 -DA:173,0 -DA:176,3 -DA:178,2 -DA:179,2 -DA:180,2 -DA:181,1 -DA:184,1 -DA:188,1 -DA:191,2 -DA:193,2 -DA:194,1 -DA:197,1 -DA:201,1 -DA:203,1 -DA:206,2 -DA:207,1 -DA:213,3 -DA:214,0 -DA:216,1 -DA:217,1 -DA:218,1 -DA:221,2 -DA:225,1 -DA:227,1 -DA:229,2 -DA:231,2 -DA:234,1 -DA:240,2 -DA:241,1 -DA:242,1 -DA:245,1 -DA:250,4 -DA:252,2 -DA:254,1 -DA:256,2 -DA:258,1 -DA:261,1 -DA:263,0 -DA:264,0 -DA:265,0 -DA:269,0 -DA:270,0 -DA:271,0 -DA:274,0 -DA:278,0 -DA:279,0 -DA:280,0 -DA:283,0 -DA:287,0 -DA:288,0 -DA:289,0 -DA:292,0 -DA:301,1 -DA:304,2 -DA:305,1 -DA:308,1 -DA:309,1 -DA:310,1 -DA:311,1 -DA:314,1 -DA:316,0 -DA:323,1 -DA:325,2 -DA:328,1 -DA:329,1 -DA:336,1 -DA:337,6 -LF:128 -LH:75 +DA:122,0 +DA:123,0 +DA:124,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:133,0 +LF:65 +LH:0 +end_of_record +SF:lib\views\settings_view\settings_view_widgets\update_firebase_token_dialog.dart +DA:33,17 +DA:35,0 +DA:36,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:51,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:66,0 +DA:67,0 +DA:71,0 +DA:73,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:85,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:91,0 +DA:92,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:113,0 +DA:115,0 +LF:40 +LH:1 end_of_record -SF:lib\utils\customizations.dart +SF:lib\views\settings_view\settings_groups\settings_group_theme.dart +DA:9,17 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:23,0 DA:25,0 DA:26,0 -LF:2 -LH:0 +DA:27,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +LF:29 +LH:1 end_of_record -SF:lib\utils\view_utils.dart -DA:7,0 +SF:lib\views\settings_view\settings_view_widgets\dialogs\ask_log_sended_dialog.dart +DA:9,17 DA:11,0 -DA:12,0 +DA:13,0 +DA:14,0 DA:15,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:47,0 +DA:48,0 +LF:23 +LH:1 +end_of_record +SF:lib\views\settings_view\settings_view_widgets\errorlog_buttons\delete_errorlog_button.dart +DA:8,17 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:16,0 +DA:17,0 +DA:18,0 +LF:8 +LH:1 +end_of_record +SF:lib\views\settings_view\settings_view_widgets\errorlog_buttons\errorlog_button.dart +DA:6,0 +DA:8,0 +DA:9,0 +DA:10,0 +DA:12,0 +DA:14,0 DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +LF:10 +LH:0 +end_of_record +SF:lib\views\settings_view\settings_view_widgets\errorlog_buttons\send_errorlog_button.dart +DA:9,17 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:18,0 +DA:19,0 DA:20,0 DA:23,0 +DA:26,0 +DA:29,0 +LF:11 +LH:1 +end_of_record +SF:lib\views\settings_view\settings_view_widgets\errorlog_buttons\show_errorlog_button.dart +DA:10,17 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:19,0 +DA:20,0 +DA:21,0 DA:24,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:42,0 +DA:45,0 +DA:48,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:58,0 +LF:30 +LH:1 +end_of_record +SF:lib\widgets\app_wrappers\single_touch_recognizer.dart +DA:6,0 +DA:8,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:17,0 DA:25,0 -DA:27,0 DA:28,0 -LF:11 +DA:30,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:38,0 +DA:41,0 +DA:44,0 +DA:46,0 +DA:47,0 +LF:19 +LH:0 +end_of_record +SF:lib\widgets\app_wrappers\state_observer.dart +DA:10,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:17,0 +LF:5 LH:0 end_of_record -SF:lib\widgets\two_step_dialog.dart +SF:lib\widgets\dialog_widgets\push_request_dialog.dart +DA:17,0 +DA:19,0 +DA:20,0 +DA:26,0 +DA:31,0 +DA:33,0 +DA:34,0 DA:36,0 +DA:37,0 +DA:38,0 DA:42,0 -DA:43,0 -DA:55,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 DA:57,0 -DA:58,0 +DA:59,0 DA:61,0 +DA:62,0 DA:63,0 -DA:65,0 +DA:64,0 DA:68,0 -DA:70,0 DA:71,0 DA:72,0 DA:73,0 +DA:74,0 DA:75,0 DA:77,0 -DA:78,0 +DA:81,0 DA:82,0 DA:83,0 -DA:84,0 DA:85,0 -DA:86,0 -DA:87,0 -DA:98,0 +DA:89,0 +DA:91,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:99,0 DA:100,0 -DA:103,0 -DA:105,0 +DA:102,0 DA:106,0 DA:107,0 -DA:111,0 -DA:112,0 +DA:108,0 +DA:109,0 DA:114,0 -DA:115,0 DA:116,0 -DA:121,0 -LF:35 -LH:0 -end_of_record -SF:lib\utils\app_customizer.dart -DA:13,0 -DA:33,16 -DA:64,16 -DA:120,0 -DA:139,0 +DA:122,0 +DA:123,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:136,0 DA:140,0 DA:141,0 DA:142,0 DA:143,0 -DA:144,0 -DA:145,0 -DA:146,0 DA:147,0 -DA:148,0 -DA:149,0 -DA:150,0 -DA:151,0 -DA:152,0 -DA:153,0 -DA:154,0 -DA:155,0 -DA:156,0 +DA:157,0 DA:159,0 DA:160,0 DA:161,0 @@ -3162,237 +11591,121 @@ DA:162,0 DA:163,0 DA:164,0 DA:165,0 -DA:166,0 -DA:167,0 -DA:168,0 DA:169,0 -DA:170,0 -DA:171,0 DA:172,0 DA:173,0 DA:174,0 DA:175,0 -DA:176,0 DA:179,0 DA:180,0 -DA:181,0 -DA:182,0 DA:183,0 -DA:184,0 DA:185,0 -DA:186,0 DA:187,0 DA:188,0 DA:189,0 DA:190,0 DA:191,0 -DA:192,0 DA:193,0 -DA:194,0 DA:195,0 DA:196,0 -DA:199,0 -DA:200,0 -DA:201,0 +DA:197,0 +DA:198,0 DA:202,0 -DA:203,0 DA:204,0 DA:205,0 DA:206,0 DA:207,0 -DA:208,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:212,0 -DA:213,0 -DA:214,0 -DA:215,0 -DA:258,0 -DA:265,0 -DA:266,0 -DA:268,0 -DA:269,0 -DA:271,0 -DA:273,0 -DA:274,0 -DA:277,0 -DA:281,0 -DA:290,0 -DA:291,0 -DA:292,0 -DA:293,0 -DA:294,0 -DA:295,0 -DA:296,0 -DA:299,0 -DA:301,0 -DA:303,0 -DA:304,0 -DA:305,0 -DA:306,0 -DA:307,0 -DA:308,0 -DA:309,0 -DA:312,0 -DA:313,0 -DA:314,0 -DA:315,0 -DA:316,0 -DA:317,0 -DA:318,0 -DA:319,0 -DA:324,0 -DA:325,0 -DA:327,0 -DA:328,0 -DA:329,0 -DA:330,0 -DA:331,0 -DA:332,0 -DA:333,0 -DA:334,0 -DA:335,0 -DA:336,0 -DA:337,0 -DA:338,0 -DA:339,0 -DA:340,0 -DA:342,0 -DA:343,0 -DA:344,0 -DA:345,0 -DA:346,0 -DA:347,0 -DA:350,0 -DA:351,0 -DA:352,0 -DA:353,0 -DA:354,0 -DA:357,0 -DA:358,0 -DA:359,0 -DA:361,0 -DA:362,0 -DA:363,0 -DA:364,0 -DA:365,0 -DA:367,0 -DA:368,0 -DA:369,0 -DA:370,0 -DA:371,0 -DA:372,0 -DA:373,0 -DA:375,0 -DA:376,0 -DA:377,0 -DA:378,0 -DA:379,0 -DA:380,0 -DA:382,0 -DA:383,0 -DA:384,0 -DA:387,0 -DA:388,0 -DA:393,0 -DA:394,0 -DA:395,0 -DA:398,0 -DA:399,0 -DA:404,0 -DA:405,0 -DA:406,0 -DA:409,0 -DA:410,0 -DA:414,0 -DA:415,0 -DA:418,0 -DA:419,0 -DA:424,0 -DA:425,0 -DA:426,0 -DA:427,0 -DA:428,0 -DA:429,0 -DA:439,0 -DA:446,0 -DA:447,0 -DA:448,0 -DA:449,0 -DA:450,0 -DA:451,0 -DA:454,0 -DA:455,0 -DA:456,0 -DA:457,0 -DA:458,0 -DA:459,0 -LF:195 -LH:2 +DA:218,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:228,0 +DA:230,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:237,0 +DA:238,0 +DA:240,0 +LF:113 +LH:0 end_of_record -SF:lib\views\settings_view\settings_view_widgets\send_error_dialog.dart -DA:10,16 -DA:12,0 -DA:13,0 -DA:17,0 +SF:lib\widgets\pulse_icon.dart +DA:4,0 DA:19,0 DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:25,0 -DA:26,0 -DA:30,0 +DA:29,0 +DA:31,0 DA:32,0 DA:33,0 +DA:34,0 DA:35,0 -DA:37,0 DA:38,0 DA:39,0 -DA:40,0 DA:45,0 DA:47,0 DA:48,0 DA:51,0 +DA:52,0 +DA:53,0 DA:54,0 +DA:55,0 +DA:56,0 DA:57,0 +DA:58,0 DA:59,0 DA:60,0 DA:61,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:80,0 -DA:82,0 -DA:84,0 -DA:85,0 -DA:97,0 -DA:99,0 -DA:101,0 -DA:103,0 -DA:104,0 -DA:106,0 -DA:107,0 -DA:108,0 -DA:109,0 -DA:113,0 -LF:47 -LH:1 +DA:62,0 +DA:63,0 +DA:65,0 +DA:66,0 +DA:71,0 +LF:30 +LH:0 end_of_record -SF:lib\widgets\default_dialog.dart -DA:11,0 +SF:lib\widgets\tooltip_container.dart +DA:9,0 +DA:18,0 DA:19,0 DA:20,0 DA:21,0 DA:22,0 DA:23,0 DA:24,0 -DA:30,0 -DA:31,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:34,0 +DA:35,0 +DA:36,0 +LF:15 +LH:0 +end_of_record +SF:lib\widgets\home_widgets\interfaces\flutter_home_widget_base.dart +DA:11,0 +LF:1 +LH:0 +end_of_record +SF:lib\widgets\home_widgets\interfaces\flutter_home_widget_builder.dart +DA:14,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 DA:32,0 -LF:10 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +LF:14 LH:0 end_of_record diff --git a/integration_test/add_tokens_test.dart b/integration_test/add_tokens_test.dart index ff1ccd675..30e224114 100644 --- a/integration_test/add_tokens_test.dart +++ b/integration_test/add_tokens_test.dart @@ -14,7 +14,7 @@ import 'package:privacyidea_authenticator/state_notifiers/completed_introduction import 'package:privacyidea_authenticator/state_notifiers/settings_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_folder_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_notifier.dart'; -import 'package:privacyidea_authenticator/utils/app_customizer.dart'; +import 'package:privacyidea_authenticator/utils/customization/application_customization.dart'; import 'package:privacyidea_authenticator/utils/riverpod_providers.dart'; import 'package:privacyidea_authenticator/views/add_token_manually_view/add_token_manually_view.dart'; import 'package:privacyidea_authenticator/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart'; @@ -45,7 +45,7 @@ void main() { when(mockTokenRepository.deleteTokens(any)).thenAnswer((_) async => []); mockTokenFolderRepository = MockTokenFolderRepository(); when(mockTokenFolderRepository.loadFolders()).thenAnswer((_) async => []); - when(mockTokenFolderRepository.saveOrReplaceFolders(any)).thenAnswer((_) async => []); + when(mockTokenFolderRepository.saveReplaceList(any)).thenAnswer((_) async => true); mockIntroductionRepository = MockIntroductionRepository(); final introductions = {...Introduction.values}..remove(Introduction.introductionScreen); when(mockIntroductionRepository.loadCompletedIntroductions()).thenAnswer((_) async => IntroductionState(completedIntroductions: introductions)); @@ -60,7 +60,7 @@ void main() { tokenFolderProvider.overrideWith((ref) => TokenFolderNotifier(repository: mockTokenFolderRepository)), introductionProvider.overrideWith((ref) => IntroductionNotifier(repository: mockIntroductionRepository)), ], - child: PrivacyIDEAAuthenticator(customization: ApplicationCustomization.defaultCustomization), + child: PrivacyIDEAAuthenticator(ApplicationCustomization.defaultCustomization), )); await _introToMainView(tester); diff --git a/integration_test/copy_to_clipboard_test.dart b/integration_test/copy_to_clipboard_test.dart index 3afe300b4..a020a2c29 100644 --- a/integration_test/copy_to_clipboard_test.dart +++ b/integration_test/copy_to_clipboard_test.dart @@ -12,9 +12,9 @@ import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; import 'package:privacyidea_authenticator/state_notifiers/settings_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_folder_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_notifier.dart'; -import 'package:privacyidea_authenticator/utils/app_customizer.dart'; +import 'package:privacyidea_authenticator/utils/customization/application_customization.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)); @@ -50,7 +50,7 @@ void main() { tokenProvider.overrideWith((ref) => TokenNotifier(repository: mockTokenRepository)), tokenFolderProvider.overrideWith((ref) => TokenFolderNotifier(repository: mockTokenFolderRepository)), ], - child: PrivacyIDEAAuthenticator(customization: ApplicationCustomization.defaultCustomization), + child: PrivacyIDEAAuthenticator(ApplicationCustomization.defaultCustomization), )); await tester.pumpAndSettle(); await pumpUntilFindNWidgets(tester, find.text('356 306'), 1, const Duration(seconds: 10)); diff --git a/integration_test/rename_and_delete_test.dart b/integration_test/rename_and_delete_test.dart index b5c388b44..c33f191bb 100644 --- a/integration_test/rename_and_delete_test.dart +++ b/integration_test/rename_and_delete_test.dart @@ -13,9 +13,9 @@ import 'package:privacyidea_authenticator/state_notifiers/completed_introduction import 'package:privacyidea_authenticator/state_notifiers/settings_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_folder_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_notifier.dart'; -import 'package:privacyidea_authenticator/utils/app_customizer.dart'; +import 'package:privacyidea_authenticator/utils/customization/application_customization.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)); @@ -55,7 +55,7 @@ void main() { tokenFolderProvider.overrideWith((ref) => TokenFolderNotifier(repository: mockTokenFolderRepository)), introductionProvider.overrideWith((ref) => IntroductionNotifier(repository: mockIntroductionRepository)), ], - child: PrivacyIDEAAuthenticator(customization: ApplicationCustomization.defaultCustomization), + child: PrivacyIDEAAuthenticator(ApplicationCustomization.defaultCustomization), )); await _renameToken(tester, 'Renamed Token'); await _renameToken(tester, 'Renamed Token Again'); diff --git a/integration_test/two_step_rollout_test.dart b/integration_test/two_step_rollout_test.dart index dc61107e3..bb1a400b4 100644 --- a/integration_test/two_step_rollout_test.dart +++ b/integration_test/two_step_rollout_test.dart @@ -10,10 +10,10 @@ import 'package:privacyidea_authenticator/model/states/settings_state.dart'; import 'package:privacyidea_authenticator/state_notifiers/settings_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_folder_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_notifier.dart'; -import 'package:privacyidea_authenticator/utils/app_customizer.dart'; +import 'package:privacyidea_authenticator/utils/customization/application_customization.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)); @@ -73,7 +53,7 @@ void main() { tokenProvider.overrideWith((ref) => TokenNotifier(repository: mockTokenRepository)), tokenFolderProvider.overrideWith((ref) => TokenFolderNotifier(repository: mockTokenFolderRepository)), ], - child: PrivacyIDEAAuthenticator(customization: ApplicationCustomization.defaultCustomization), + child: PrivacyIDEAAuthenticator(ApplicationCustomization.defaultCustomization), )); await _addTwoStepHotpTokenTest(tester); await _addTwoStepTotpTokenTest(tester); diff --git a/integration_test/views_test.dart b/integration_test/views_test.dart index 7e189b26f..35015e40b 100644 --- a/integration_test/views_test.dart +++ b/integration_test/views_test.dart @@ -12,10 +12,10 @@ import 'package:privacyidea_authenticator/model/states/settings_state.dart'; import 'package:privacyidea_authenticator/state_notifiers/settings_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_folder_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_notifier.dart'; -import 'package:privacyidea_authenticator/utils/app_customizer.dart'; +import 'package:privacyidea_authenticator/utils/customization/application_customization.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() @@ -72,7 +72,7 @@ void main() { )), tokenFolderProvider.overrideWith((ref) => TokenFolderNotifier(repository: mockTokenFolderRepository)), ], - child: PrivacyIDEAAuthenticator(customization: ApplicationCustomization.defaultCustomization), + child: PrivacyIDEAAuthenticator(ApplicationCustomization.defaultCustomization), )); await _licensesViewTest(tester); diff --git a/ios/Flutter/Flutter.podspec b/ios/Flutter/Flutter.podspec deleted file mode 100644 index 98e163395..000000000 --- a/ios/Flutter/Flutter.podspec +++ /dev/null @@ -1,18 +0,0 @@ -# -# This podspec is NOT to be published. It is only used as a local source! -# This is a generated file; do not edit or check into version control. -# - -Pod::Spec.new do |s| - s.name = 'Flutter' - s.version = '1.0.0' - s.summary = 'A UI toolkit for beautiful and fast apps.' - s.homepage = 'https://flutter.dev' - s.license = { :type => 'BSD' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } - s.ios.deployment_target = '12.0' - # Framework linking is handled by Flutter tooling, not CocoaPods. - # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. - s.vendored_frameworks = 'path/to/nothing' -end diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh index 0d9da0fd3..f8c05ec23 100755 --- a/ios/Flutter/flutter_export_environment.sh +++ b/ios/Flutter/flutter_export_environment.sh @@ -5,9 +5,8 @@ export "FLUTTER_APPLICATION_PATH=/Users/frankmerkel/Documents/GitHub/pi-authenti export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_TARGET=/Users/frankmerkel/Documents/GitHub/pi-authenticator/lib/mains/main_netknights.dart" export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=4.3.1" -export "FLUTTER_BUILD_NUMBER=403106" -export "DART_DEFINES=RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==,RkxVVFRFUl9XRUJfQ0FOVkFTS0lUX1VSTD1odHRwczovL3d3dy5nc3RhdGljLmNvbS9mbHV0dGVyLWNhbnZhc2tpdC8yZTRiYTljNmZiNDk5Y2NkNGU4MTQyMDU0Mzc4M2NjNzI2N2FlNDA2Lw==,RkxVVFRFUl9BUFBfRkxBVk9SPW5ldGtuaWdodHM=" +export "FLUTTER_BUILD_NAME=4.4.0" +export "FLUTTER_BUILD_NUMBER=404001" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index cf370fc4d..6b4d03e97 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 0DCCE7B526CE7AA30029E1D5 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 5155DCDE5F4DD400F5DF6ECC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F4640C1CB92CE5806393BD0 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -41,6 +42,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 02E06BAE53E4F69214BE5161 /* Pods-Runner.release-netknights.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-netknights.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-netknights.xcconfig"; sourceTree = ""; }; 0D32FBC526A84C0B0033DD09 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = ""; }; 0D32FBC626A84C0B0033DD09 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/LaunchScreen.strings; sourceTree = ""; }; 0D32FBC726A84E6D0033DD09 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Main.strings; sourceTree = ""; }; @@ -62,7 +64,7 @@ 92455F34F4CD3178F657D07D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* privacyIDEA Authenticator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "privacyIDEA Authenticator.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146EE1CF9000F007C117D /* (debug) privacyIDEA Authenticator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "(debug) privacyIDEA Authenticator.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -154,7 +156,7 @@ 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( - 97C146EE1CF9000F007C117D /* privacyIDEA Authenticator.app */, + 97C146EE1CF9000F007C117D /* (debug) privacyIDEA Authenticator.app */, ); name = Products; sourceTree = ""; @@ -207,7 +209,7 @@ ); name = Runner; productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* privacyIDEA Authenticator.app */; + productReference = 97C146EE1CF9000F007C117D /* (debug) privacyIDEA Authenticator.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -254,7 +256,7 @@ files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 0DCCE7B526CE7AA30029E1D5 /* BuildFile in Resources */, + 0DCCE7B526CE7AA30029E1D5 /* (null) in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -263,6 +265,98 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 19B242C1346AC637BC733F43 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseInstallations/FirebaseInstallations.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseMessaging/FirebaseMessaging.framework", + "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", + "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", + "${BUILT_PRODUCTS_DIR}/SwiftyRSA/SwiftyRSA.framework", + "${BUILT_PRODUCTS_DIR}/app_minimizer/app_minimizer.framework", + "${BUILT_PRODUCTS_DIR}/camera_avfoundation/camera_avfoundation.framework", + "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", + "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", + "${BUILT_PRODUCTS_DIR}/file_selector_ios/file_selector_ios.framework", + "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", + "${BUILT_PRODUCTS_DIR}/flutter_mailer/flutter_mailer.framework", + "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", + "${BUILT_PRODUCTS_DIR}/home_widget/home_widget.framework", + "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", + "${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", + "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", + "${BUILT_PRODUCTS_DIR}/pi_authenticator_legacy/pi_authenticator_legacy.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", + "${BUILT_PRODUCTS_DIR}/uni_links/uni_links.framework", + "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseInstallations.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseMessaging.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyRSA.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/app_minimizer.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera_avfoundation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_mailer.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/home_widget.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/pi_authenticator_legacy.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/uni_links.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 2AD8F18CAC5ADF0FEB8B4915 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 36AD5FCC2B83563A00FB5A82 /* Copy GoogleService-Info.plist */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -533,7 +627,7 @@ ); MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = privacyidea.authenticator; - PRODUCT_NAME = "privacyIDEA Authenticator"; + PRODUCT_NAME = "(debug) privacyIDEA Authenticator"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.0; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8a1ae9722..48db00e09 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -84,5 +84,11 @@ UIViewControllerBasedStatusBarAppearance + UISupportsDocumentBrowser + + LSSupportsOpeningDocumentsInPlace + + UIFileSharingEnabled + diff --git a/lib/extensions/color_extension.dart b/lib/extensions/color_extension.dart deleted file mode 100644 index a8a207cb3..000000000 --- a/lib/extensions/color_extension.dart +++ /dev/null @@ -1,21 +0,0 @@ -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 opposite() { - return Color.fromARGB( - alpha, - 255 - red, - 255 - green, - 255 - blue, - ); - } -} diff --git a/lib/extensions/theme_mode_extension.dart b/lib/extensions/theme_mode_extension.dart deleted file mode 100644 index 664912904..000000000 --- a/lib/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/interfaces/repo/push_request_repository.dart b/lib/interfaces/repo/push_request_repository.dart index 040bf906d..09b126888 100644 --- a/lib/interfaces/repo/push_request_repository.dart +++ b/lib/interfaces/repo/push_request_repository.dart @@ -1,7 +1,11 @@ -import '../../model/push_request.dart'; +import '../../model/push_request.dart' show PushRequest; +import '../../model/states/push_request_state.dart' show PushRequestState; abstract class PushRequestRepository { - Future saveOrReplacePushRequests(List pushRequests); - Future> loadPushRequests(); - Future deletePushRequest(PushRequest pushRequest); + Future loadState(); + Future saveState(PushRequestState pushRequestState); + Future clearState(); + + Future add(PushRequest pushRequest, {PushRequestState? state}); + Future remove(PushRequest pushRequest, {PushRequestState? state}); } 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/interfaces/repo/token_repository.dart b/lib/interfaces/repo/token_repository.dart index c15d34211..cda5e9752 100644 --- a/lib/interfaces/repo/token_repository.dart +++ b/lib/interfaces/repo/token_repository.dart @@ -1,9 +1,21 @@ import '../../model/tokens/token.dart'; abstract class TokenRepository { - Future> saveOrReplaceTokens(List tokens); + /// Returns the saved Token with the given id. + Future loadToken(String id); + + /// Returns all saved Tokens. Future> loadTokens(); - //Returns the tokens that were not deleted - Future> deleteTokens(List tokens); + /// Returns true if the Token was saved successfully. + Future saveOrReplaceToken(Token token); + + /// Returns the tokens that were not saved successfully. + Future> saveOrReplaceTokens(List tokens); + + /// Returns true if the Token was deleted successfully. + Future deleteToken(Token token); + + /// Returns the tokens that were not deleted successfully. + Future> deleteTokens(List tokens); } diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 111a234dd..5a6caa7d6 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -163,6 +163,12 @@ "@systemTheme": { "description": "The systems theme." }, + "someTokensDoNotSupportPolling": "Některé tokeny jsou zastaralé a nepodporují polling", + "@someTokensDoNotSupportPolling": { + "description": "Tells the user, that the following tokens do not support polling.", + "type": "text", + "placeholders": {} + }, "enablePolling": "Povolit polling", "@enablePolling": { "description": "Name of the setting switch that enables polling." @@ -536,6 +542,7 @@ "invalidQrScan": "Naskenovaný QR kód není platnou zálohou {appName}.", "invalidQrFile": "Vybraný soubor neobsahuje platný QR kód z {appName}.", "invalidLink": "Zadaný odkaz není platným tokenem {appName} nebo není podporován.", + "importFailedToken": "{count, plural, zero{Žádný token Nepodařilo se importovat.} one{Nepodařilo se importovat token.} other{Nepodařilo se importovat {count} tokenů.}}", "importExistingToken": "{count, plural, zero{Nebyl nalezen žádný token, který by se již v aplikaci nacházel.} one{Byl nalezen token, který již v aplikaci existuje.} other{{count} byly nalezeny tokeny, které se již v aplikaci nacházejí.}}", "importConflictToken": "{count, plural, zero{Není žádný konflikt s tokeny, které již existují.} one{Je konflikt s tokeny, které již existují.\nProsím, vyberte, který z nich chcete zachovat.} other{Je konflikt s tokeny, které již existují.\nProsím, vyberte, který z nich chcete zachovat.}}", "importNewToken": "{count, plural, zero{Nenalezen žádný nový token.} one{Nalezen nový token, který bude importován.} other{Nalezen nový token {count}, který bude importován.}}", @@ -545,6 +552,9 @@ "importHintAegisLink": "Zadejte odkaz, který obdržíte při přenosu záznamů ze systému Aegis.", "importHintGoogleQrScan": "Naskenujte QR kód, který obdržíte při exportu účtů z Google Authenticator.", "importHintGoogleQrFile": "Vyberte obrazový soubor s QR kódem, který obdržíte při exportu účtů z Google Authenticator.\n!! Upozorňujeme, že není bezpečné ukládat QR kód do zařízení, protože tokeny nejsou šifrovány !!", + "importHintAuthenticatorProFile": "Chcete-li vytvořit zálohu aplikace Authenticator Pro, přejděte do nastavení a klepněte na položku \"Automatické zálohování\". Vyberte umístění úložiště a nastavte heslo. Poté stiskněte \"Zálohovat nyní\" a exportujte tokeny.", + "importHintFreeOtpPlusQrScan": "Naskenujte QR kód, který obdržíte po stisknutí tří teček na dlaždici tokenu, a vyberte možnost \"Sdílet QR kód\".", + "importHintFreeOtpPlusFile": "Chcete-li vytvořit zálohu aplikace FreeOTP+, klepněte na tři tečky v pravém horním rohu a vyberte možnost \"Exportovat\". Můžete si vybrat mezi formátem JSON a URI. Zálohu doporučujeme po importu odstranit, protože není šifrovaná.", "qrFileDecodeError": "Z vybraného obrázku nebylo možné dekódovat QR kód, použijte prosím místo toho skener QR kódů.", "tokenLink": "Token link", "feedback": "Zpětná vazba", @@ -575,5 +585,55 @@ "example": "GitHub" } } - } + }, + "errorUnlinkingPushToken": "Nepodařilo se odlinkovat push token {label}.", + "@errorUnlinkingPushToken": { + "description": "Error message when unlinking a push token failed.", + "placeholders": { + "label": { + "example": "PUSH1234A" + } + } + }, + "pleaseSyncManuallyWhenNetworkIsAvailable": "Synchronizujte prosím push tokeny ručně prostřednictvím nastavení, když je k dispozici síťové připojení.", + "pushTokens": "Žetony Push", + "continueButton": "Pokračovat", + "addTokenManually": "Přidat token ručně", + "addFolder": "Přidat složku", + "searchTokens": "Hledat tokeny", + "closeSearchTokens": "Zavřít vyhledávání", + "increaseCounter": "Zvýšit počítadla", + "copyOTPToClipboard": "Zkopírovat OTP do schránky", + "licenses": "Licence", + "optionalMessage": "Volitelná zpráva", + "confirmation": "Potvrzení", + "askLogSendedDescription": "Odeslali jste protokol a chcete jej nyní vymazat?", + "algorithmUnsupported": "Algoritmus {algorithm} není podporován", + "@algorithmUnsupported": { + "placeholders": { + "algorithm": { + "example": "MD5" + } + } + }, + "thisAppIsOpenSource": "Tato aplikace má otevřený zdrojový kód\nNavštivte nás na GitHub", + "invalidArgument": "{argument} není platná hodnota pro {type}", + "importExportTokens": "Import/Exportovat žetony", + "exportNonPrivacyIDEATokens": "Exportovat ne-privacyIDEA žetony", + "selectTokensToExport": "{count, plural, zero{} one{Vyberte žeton k exportu} other{Vyberte žetony k exportu}}", + "noTokensToExport": "Žádné žetony k exportu", + "exportAllTokens": "Exportovat všechny žetony", + "export": "Export", + "exportingTokens": "Probíhá export žetonů...", + "exportTokens": "Exportovat žetony", + "enterPasswordToEncrypt": "Zadejte heslo pro šifrování žetonů. Toto heslo bude vyžadováno k importu žetonů.", + "exportLockedTokenReason": "Prosím, ověřte se, abyste mohli exportovat uzamčené žetony.", + "fileSavedToDownloadsFolder": "Soubor uložen do složky Stažené soubory", + "errorSavingFile": "Chyba při ukládání souboru", + "toFile": "Do souboru", + "asQrCode": "Jako QR kód", + "scanThisQrWithNewDevice": "Naskenujte tento QR kód svým novým zařízením pro import žetonu.", + "oneMore": "Ještě jeden", + "done": "Hotovo", + "confirmPassword": "Potvrďte heslo" } \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 04a386aa5..9b32055d6 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -7,7 +7,7 @@ "patchNotesV4_3_1Improvement1": "Der QR-Code-Scanner wurde verbessert.", "patchNotesV4_3_0NewFeatures1": "Unterstützung für den Import von Token von Google, Aegis und 2FAS Authenticator hinzugefügt. Weitere Importquellen werden in Zukunft hinzugefügt.", "patchNotesV4_3_0NewFeatures2": "Feedback-Option zu den Einstellungen hinzugefügt.", - "patchNotesV4_3_0NewFeatures3": "Push-Tokens können jetzt aus der Token-Liste ausgeblendet werden.", + "patchNotesV4_3_0NewFeatures3": "Push-Token können jetzt aus der Token-Liste ausgeblendet werden.", "patchNotesV4_3_0NewFeatures4": "Es wurden Einführungen hinzugefügt, um neuen Benutzern den Einstieg zu erleichtern.", "patchNotesV4_3_0NewFeatures5": "Sie können jetzt nach Token suchen, indem Sie auf die Lupe in der oberen rechten Ecke tippen.", "patchNotesV4_3_0NewFeatures6": "Ab Android 12 kann für einen Token ein Widget auf dem Homescreen erstellt werden.", @@ -161,6 +161,12 @@ "@systemTheme": { "description": "The systems theme." }, + "someTokensDoNotSupportPolling": "Einige der Token sind veraltet und unterstützen keine aktiven Anfragen", + "@someTokensDoNotSupportPolling": { + "description": "Tells the user, that the following tokens do not support polling.", + "type": "text", + "placeholders": {} + }, "enablePolling": "Aktives Stellen von Push-Anfragen", "@enablePolling": { "description": "Name of the setting switch that enables polling." @@ -515,8 +521,9 @@ "invalidQrScan": "Der gescannte QR-Code ist kein gültiges Backup von {appName}.", "invalidQrFile": "Die ausgewählte Datei enthällt kein gültigen QR-Code von {appName}.", "invalidLink": "Der eingegebene Link ist kein gültiger Token von {appName}, oder er wird nicht unterstützt.", + "importFailedToken": "{count, plural, zero{Kein Token konnte nicht importiert werden.} one{Importieren eines Tokens fehlgeschlagen.} other{Der Import von {count} Token ist fehlgeschlagen.}}", "importExistingToken": "{count, plural, zero{Es wurde kein Token gefunden, das sich bereits in der App befindet.} one{Es wurde ein Token gefunden, das sich bereits in der Anwendung befindet.} other{Es wurden {count} Token gefunden, die sich bereits in der Anwendung befinden.}}", - "importConflictToken": "{count, plural, zero{Es besteht kein Konflikt mit bereits existierenden Token.} one{Es besteht ein Konflikt mit bereits existierenden Token.\nBitte wählen Sie aus, welches Sie behalten möchten.} other{Es besteht ein Konflikt mit bereits existierenden Token.\nBitte wählen Sie aus, welches Sie behalten möchten.}}", + "importConflictToken": "{count, plural, zero{Es besteht kein Konflikt mit bereits existierenden Token.} one{Es besteht ein Konflikt mit bereits vorhandenen Token.\nBitte wählen Sie aus, welches Sie behalten möchten.} other{Es bestehen Konflikte mit bereits vorhandenen Token.\nBitte wählen Sie die Token aus, die Sie behalten möchten.}}", "importNewToken": "{count, plural, zero{Es wurde kein neues Token gefunden.} one{Es wurde ein neues Token gefunden, das importiert wird.} other{Es wurden {count} neue Token gefunden, die importiert werden.}}", "importHint2FAS": "Wählen Sie das 2FAS-Backup aus.\nFalls Sie kein Backup haben, erstellen Sie eins in der 2FAS-App. Wir empfehlen die Verwendung eines Passworts.", "importHintAegisBackupFile": "Wähle dein Aegis-Export (.json) aus.\nWenn Sie keinen Export haben, erstellen Sie bitte eins über das Einstellungen Menu in der Aegis-App. Wir empfehlen die Verwendung eines Passworts.", @@ -524,6 +531,9 @@ "importHintAegisLink": "Geben Sie den Link ein, den Sie erhalten, wenn Sie Einträge aus Aegis übertragen.", "importHintGoogleQrScan": "Scannen Sie den QR-Code, den Sie erhalten, wenn Sie Ihre Konten aus Google Authenticator exportieren.", "importHintGoogleQrFile": "Wählen Sie eine Bilddatei mit dem QR-Code, den Sie erhalten, wenn Sie Ihre Konten aus dem Google Authenticator exportieren.\n!! Der QR-Code enthält die Token in unverschlüsselter Form. Es ist deshalb nicht sicher, diesen länger als nötig aufzubewahren !!", + "importHintAuthenticatorProFile": "Um ein Backup der Authenticator Pro-App zu erstellen navigieren Sie zu den Einstellungen und tippen Sie auf \"Automatische Sicherung\". Wählen Sie einen Speicherort und setzen Sie ein Passwort. Anschließend drücken Sie auf \"Jetzt sichern\" um die Token zu exportieren.", + "importHintFreeOtpPlusQrScan": "Scannen Sie den QR-Code, den Sie erhalten, wenn Sie auf die drei Punkte in der Kachel des Tokens drücken, und wählen Sie \"QR-Code teilen\".", + "importHintFreeOtpPlusFile": "Um ein Backup der FreeOTP+ App zu erstellen, tippen Sie auf die drei Punkte in der oberen rechten Ecke und wählen Sie \"Exportieren\". Sie können zwischen dem JSON- und dem URI-Format wählen. Wir empfehlen, das Backup nach dem Importieren zu löschen, da es nicht verschlüsselt ist.", "qrFileDecodeError": "Es war nicht möglich, den QR-Code aus dem ausgewählten Bild zu dekodieren. Bitte verwenden Sie stattdessen den QR-Code-Scanner.", "tokenLink": "Token Link", "feedback": "Feedback", @@ -554,5 +564,55 @@ "example": "GitHub" } } - } + }, + "errorUnlinkingPushToken": "Entkoppeln des Push Tokens {label} fehlgeschlagen.", + "@errorUnlinkingPushToken": { + "description": "Error message when unlinking a push token failed.", + "placeholders": { + "label": { + "example": "PUSH1234A" + } + } + }, + "pleaseSyncManuallyWhenNetworkIsAvailable": "Bitte synchronisieren Sie die Push Token über die Einstellungen manuell, wenn eine Netzwerkverbindung verfügbar ist.", + "pushTokens": "Push-Token", + "continueButton": "Weiter", + "addTokenManually": "Token manuell hinzufügen", + "addFolder": "Ordner hinzufügen", + "searchTokens": "Token suchen", + "closeSearchTokens": "Suche schließen", + "increaseCounter": "Zähler erhöhen", + "copyOTPToClipboard": "OTP in die Zwischenablage kopieren", + "licenses": "Lizenzen", + "optionalMessage": "Optionale Nachricht", + "confirmation": "Confirmation", + "askLogSendedDescription": "Haben Sie das Protokoll gesendet, und möchten Sie es jetzt löschen?", + "algorithmUnsupported": "Der Algorithmus {algorithm} wird nicht unterstützt", + "@algorithmUnsupported": { + "placeholders": { + "algorithm": { + "example": "MD5" + } + } + }, + "thisAppIsOpenSource": "Diese App ist Open Source\nBesuchen Sie uns auf GitHub", + "invalidArgument": "{argument} ist kein gültiger Wert für {type}", + "importExportTokens": "Token importieren/exportieren", + "exportNonPrivacyIDEATokens": "Nicht-privacyIDEA-Token exportieren", + "selectTokensToExport": "{count, plural, zero{} one{Wählen Sie das zu exportierende Token aus} other{Wählen Sie die zu exportierenden Tokens aus}}", + "noTokensToExport": "Keine Tokens zum Exportieren", + "exportAllTokens": "Alle Tokens exportieren", + "export": "Exportieren", + "exportingTokens": "Tokens werden exportiert...", + "exportTokens": "Tokens exportieren", + "enterPasswordToEncrypt": "Geben Sie ein Passwort ein, um die Tokens zu verschlüsseln. Dieses Passwort wird benötigt, um die Tokens zu importieren.", + "exportLockedTokenReason": "Bitte authentifizieren Sie sich, um gesperrte Tokens zu exportieren.", + "fileSavedToDownloadsFolder": "Datei wurde im Download-Ordner gespeichert", + "errorSavingFile": "Fehler beim Speichern der Datei", + "toFile": "In Datei", + "asQrCode": "Als QR-Code", + "scanThisQrWithNewDevice": "Scannen Sie diesen QR-Code mit Ihrem neuen Gerät, um das Token zu importieren.", + "oneMore": "Noch eins", + "done": "Fertig", + "confirmPassword": "Passwort bestätigen" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c9e0d0cb7..2937cd9e0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -153,6 +153,12 @@ "@systemTheme": { "description": "The systems theme." }, + "someTokensDoNotSupportPolling": "Some of the tokens are outdated and do not support polling", + "@someTokensDoNotSupportPolling": { + "description": "Tells the user, that the following tokens do not support polling.", + "type": "text", + "placeholders": {} + }, "enablePolling": "Enable polling", "@enablePolling": { "description": "Name of the setting switch that enables polling." @@ -526,8 +532,9 @@ "invalidQrScan": "The scanned QR code is not a valid backup of {appName}.", "invalidQrFile": "The selected file does not contain a valid QR code from {appName}.", "invalidLink": "The link entered is not a valid token of {appName}, or it is not supported.", + "importFailedToken": "{count, plural, zero{No token Failed to import.} one{Failed to import a token.} other{Failed to import {count} tokens.}}", "importExistingToken": "{count, plural, zero{No token was found that is already in the application.} one{A token was found that already exists in the application.} other{{count} tokens was found that are already in the application.}}", - "importConflictToken": "{count, plural, zero{There is no conflict with existing tokens.} one{There is a conflict with an existing token.\nPlease choose which one you want to keep.} other{There are conflicts with existing tokens.\nPlease choose which one you want to keep.}}", + "importConflictToken": "{count, plural, zero{There is no conflict with existing tokens.} one{There is a conflict with existing tokens.\nPlease select which one you would like to keep.} other{There are conflicts with existing tokens.\nPlease select the tokens you wish to keep.}}", "importNewToken": "{count, plural, zero{No new token has been found.} one{A new token has been found and is being imported.} other{{count} new tokens have been found and will be imported.}}", "importHint2FAS": "Select your 2FAS backup.\nIf you do not have a backup, create one in the 2FAS app. We recommend using a password.", "importHintAegisBackupFile": "Select your Aegis export (.JSON).\nIf you do not have an export, please create one via the settings menu in the Aegis app. The use of a password is recommended.", @@ -535,6 +542,9 @@ "importHintAegisLink": "Enter the link you receive when you transfer entries from Aegis.", "importHintGoogleQrScan": "Scan the QR code you receive when you export your accounts from Google Authenticator.", "importHintGoogleQrFile": "Select an image file with the QR code you receive when you export your accounts from Google Authenticator.\n!! Note that it is not safe to save the QR code on your device as the tokens are not encrypted !!", + "importHintAuthenticatorProFile": "To create a backup of the Authenticator Pro app, navigate to the settings and tap on \"Auto backup\". Select a storage location and set a password. Then press \"Back up now\" to export the tokens.", + "importHintFreeOtpPlusQrScan": "Scan the QR code you receive when you press the three dots in the tile of the token and select \"Share QR code\".", + "importHintFreeOtpPlusFile": "To create a backup of the FreeOTP+ app, tap on the three dots in the upper right corner and select \"Export\". You can choose between JSON and URI format. We recommend to delete the backup after importing it, because it is not encrypted.", "qrFileDecodeError": "It was not possible to decode the QR code from the selected image, please use the QR code scanner instead.", "tokenLink": "Token link", "feedback": "Feedback", @@ -566,5 +576,86 @@ "example": "GitHub" } } + }, + "errorUnlinkingPushToken": "Failed to unlink the push token {label}.", + "@errorUnlinkingPushToken": { + "description": "Error message when unlinking a push token failed.", + "placeholders": { + "label": { + "example": "PUSH1234A" + } + } + }, + "pleaseSyncManuallyWhenNetworkIsAvailable": "Please synchronize the push tokens manually via the settings when a network connection is available.", + "pushTokens": "Push Tokens", + "continueButton": "Continue", + "addTokenManually": "Add token manually", + "addFolder": "Add folder", + "searchTokens": "Search tokens", + "closeSearchTokens": "Close search", + "increaseCounter": "Increase counter", + "copyOTPToClipboard": "Copy OTP to clipboard", + "licenses": "Licenses", + "optionalMessage": "Optional message", + "confirmation": "Confirmation", + "askLogSendedDescription": "Did you send the log, and do you want to clear it now?", + "algorithmUnsupported": "The algorithm {algorithm} is not supported", + "@algorithmUnsupported": { + "placeholders": { + "algorithm": { + "example": "MD5" + } + } + }, + "thisAppIsOpenSource": "This Application is Open Source\nVisit us on GitHub", + "importExportTokens": "Import/Export tokens", + "exportNonPrivacyIDEATokens": "Export non-privacyIDEA tokens", + "selectTokensToExport": "{count, plural, zero{} one{Select token to export} other{Select tokens to export}}", + "noTokensToExport": "No tokens to export", + "exportAllTokens": "Export all tokens", + "export": "Export", + "exportingTokens": "Exporting tokens...", + "exportTokens": "Export tokens", + "enterPasswordToEncrypt": "Enter a password to encrypt the tokens. This password will be required to import the tokens.", + "exportLockedTokenReason": "Please authenticate to export locked tokens.", + "fileSavedToDownloadsFolder": "File saved to Downloads folder", + "errorSavingFile": "Saving to file failed", + "toFile": "To file", + "asQrCode": "As QR code", + "scanThisQrWithNewDevice": "Scan this QR code with your new device to import the token.", + "oneMore": "One more", + "done": "Done", + "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_es.arb b/lib/l10n/app_es.arb index 578fe5cd9..702a7a056 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -161,6 +161,12 @@ "@systemTheme": { "description": "The systems theme." }, + "someTokensDoNotSupportPolling": "Algunos tokens están obsoletos y no admiten la consulta activa para la autenticación mediante mensaje push.", + "@someTokensDoNotSupportPolling": { + "description": "Tells the user, that the following tokens do not support polling.", + "type": "text", + "placeholders": {} + }, "enablePolling": "Activar polling", "@enablePolling": { "description": "Name of the setting switch that enables polling." @@ -532,6 +538,7 @@ "invalidQrScan": "El código QR escaneado no es una copia de seguridad válida de {appName}", "invalidQrFile": "El archivo seleccionado no contiene un código QR válido de {appName}", "invalidLink": "El enlace introducido no es un token válido de {appName}, o no es compatible", + "importFailedToken": "{count, plural, zero{No token Fallo al importar.} one{Error al importar un token.} other{Error al importar {count} tokens.}}", "importExistingToken": "{count, plural, zero{No se ha encontrado ningún token que ya esté en la aplicación.} one{Se ha encontrado un token que ya existe en la aplicación.} other{Se han encontrado {count} tokens que ya están en la aplicación.}}", "importConflictToken": "{count, plural, zero{No hay conflicto con tokens que ya existen.} one{Hay un conflicto con tokens que ya existen.\nPor favor, seleccione cuál le gustaría conservar.} other{Hay un conflicto con tokens que ya existen.\nPor favor, seleccione cuál le gustaría conservar.}}", "importNewToken": "{count, plural, zero{No se ha encontrado un nuevo token.} one{Se ha encontrado un nuevo token que se importará.} other{Se ha encontrado un nuevo token {count} que se importará.}}", @@ -541,6 +548,9 @@ "importHintAegisLink": "Introduzca el enlace que recibe al transferir entradas desde Aegis", "importHintGoogleQrScan": "Escanea el código QR que recibes al exportar tus cuentas desde Google Authenticator", "importHintGoogleQrFile": "Selecciona un archivo de imagen con el código QR que recibes al exportar tus cuentas desde Google Authenticator.\n!! Tenga en cuenta que no es seguro guardar el código QR en su dispositivo, ya que los tokens no están cifrados !!", + "importHintAuthenticatorProFile": "Para crear una copia de seguridad de la aplicación Authenticator Pro, vaya a la configuración y pulse en \"Copia de seguridad automática\". Seleccione una ubicación de almacenamiento y establezca una contraseña. A continuación, pulse \"Hacer copia de seguridad ahora\" para exportar los tokens.", + "importHintFreeOtpPlusQrScan": "Escanea el código QR que recibes al pulsar los tres puntos en el azulejo de la ficha y selecciona \"Compartir código QR\".", + "importHintFreeOtpPlusFile": "Para crear una copia de seguridad de la app FreeOTP+, pulse los tres puntos de la esquina superior derecha y seleccione \"Exportar\". Puede elegir entre los formatos JSON y URI. Recomendamos eliminar la copia de seguridad después de importarla, ya que no está cifrada.", "qrFileDecodeError": "No fue posible decodificar el código QR de la imagen seleccionada, por favor utilice el escáner de código QR en su lugar.", "tokenLink": "Enlace token", "feedback": "Comentarios", @@ -571,5 +581,55 @@ "example": "GitHub" } } - } + }, + "errorUnlinkingPushToken": "Error al desvincular el token push {label}", + "@errorUnlinkingPushToken": { + "description": "Error message when unlinking a push token failed.", + "placeholders": { + "label": { + "example": "PUSH1234A" + } + } + }, + "pleaseSyncManuallyWhenNetworkIsAvailable": "Por favor, sincronice los tokens push manualmente a través de los ajustes cuando haya una conexión de red disponible.", + "pushTokens": "Push Tokens", + "continueButton": "Continue", + "addTokenManually": "Añadir token manualmente", + "addFolder": "Añadir carpeta", + "searchTokens": "Buscar tokens", + "closeSearchTokens": "Cerrar búsqueda", + "increaseCounter": "Incrementar contador", + "copyOTPToClipboard": "Copiar OTP al portapapeles", + "licenses": "Licencias", + "optionalMessage": "Mensaje opcional", + "confirmation": "confirmación", + "askLogSendedDescription": "¿Ha enviado el registro y desea borrarlo ahora?", + "algorithmUnsupported": "El algoritmo {algorithm} no es compatible", + "@algorithmUnsupported": { + "placeholders": { + "algorithm": { + "example": "MD5" + } + } + }, + "thisAppIsOpenSource": "Esta aplicación es de código abierto\nVisítanos en GitHub", + "invalidArgument": "{argument} no es un valor válido para {type}", + "importExportTokens": "Importar/Exportar tokens", + "exportNonPrivacyIDEATokens": "Exportar tokens no privacyIDEA", + "selectTokensToExport": "{count, plural, zero{} one{Seleccionar token para exportar} other{Seleccionar tokens para exportar}}", + "noTokensToExport": "No hay tokens para exportar", + "exportAllTokens": "Exportar todos los tokens", + "export": "Exportar", + "exportingTokens": "Exportando tokens...", + "exportTokens": "Exportar tokens", + "enterPasswordToEncrypt": "Ingrese una contraseña para cifrar los tokens. Esta contraseña será necesaria para importar los tokens.", + "exportLockedTokenReason": "Por favor, autentíquese para exportar tokens bloqueados.", + "fileSavedToDownloadsFolder": "Archivo guardado en la carpeta de descargas", + "errorSavingFile": "Error al guardar el archivo", + "toFile": "A archivo", + "asQrCode": "Como código QR", + "scanThisQrWithNewDevice": "Escanee este código QR con su nuevo dispositivo para importar el token.", + "oneMore": "Uno más", + "done": "Hecho", + "confirmPassword": "Confirmar contraseña" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 772b34fba..c6962a79d 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -163,6 +163,12 @@ "@systemTheme": { "description": "The systems theme." }, + "someTokensDoNotSupportPolling": "Certains jetons sont obsolètes et ne supportent pas l'interrogation due serveur.", + "@someTokensDoNotSupportPolling": { + "description": "Tells the user, that the following tokens do not support polling.", + "type": "text", + "placeholders": {} + }, "enablePolling": "Activer l'interrogation du serveur.", "@enablePolling": { "description": "Name of the setting switch that enables polling." @@ -537,6 +543,7 @@ "invalidQrScan": "Le code QR scanné n'est pas une sauvegarde valide de {appName}", "invalidQrFile": "Le fichier sélectionné ne contient pas de code QR valide de {appName}", "invalidLink": "Le lien saisi n'est pas un jeton valide de {appName}, ou il n'est pas pris en charge", + "importFailedToken": "{count, plural, zero{Pas de jeton Échec de l'importation.} one{Échec de l'importation d'un jeton.} other{Échec de l'importation des jetons {count}.}}", "importExistingToken": "{count, plural, zero{Aucun jeton déjà présent dans l'application n'a été trouvé.} one{Un jeton qui existe déjà dans l'application a été trouvé.} other{Des jetons {count} déjà présents dans l'application ont été trouvés.}}", "importConflictToken": "{count, plural, zero{Il n'y a pas de conflit avec des tokens déjà existants.} one{Il y a un conflit avec des tokens déjà existants.\nVeuillez choisir celui que vous voulez garder.} other{Il y a un conflit avec des tokens déjà existants.\nVeuillez choisir celui que vous voulez garder.}}", "importNewToken": "{count, plural, zero{Aucun nouveau token n'a été trouvé.} one{Un nouveau token a été trouvé et sera importé.} other{Aucun {count} nouveau token a été trouvé et sera importé.}}", @@ -546,6 +553,9 @@ "importHintAegisLink": "Saisissez le lien que vous recevez lorsque vous transférez des entrées depuis Aegis", "importHintGoogleQrScan": "Scannez le code QR que vous recevez lorsque vous exportez vos comptes depuis Google Authenticator", "importHintGoogleQrFile": "Sélectionnez un fichier image avec le code QR que vous obtenez lorsque vous exportez vos comptes depuis Google Authenticator.\n!! Notez qu'il n'est pas sûr d'enregistrer le code QR sur votre appareil, car les jetons ne sont pas cryptés !!", + "importHintAuthenticatorProFile": "Pour créer une sauvegarde de l'application Authenticator Pro, accédez aux paramètres et appuyez sur \"Sauvegarde automatique\". Sélectionnez un emplacement de stockage et définissez un mot de passe. Puis appuyez sur \"Sauvegarder maintenant\" pour exporter les tokens.", + "importHintFreeOtpPlusQrScan": "Scannez le code QR que vous recevez lorsque vous appuyez sur les trois points dans la tuile du jeton et sélectionnez \"Partager le code QR\".", + "importHintFreeOtpPlusFile": "Pour créer une sauvegarde de l'application FreeOTP+, appuyez sur les trois points dans le coin supérieur droit et sélectionnez \"Exporter\". Vous pouvez choisir entre les formats JSON et URI. Nous recommandons de supprimer la sauvegarde après l'avoir importée, car elle n'est pas cryptée.", "qrFileDecodeError": "Il n'a pas été possible de décoder le code QR à partir de l'image sélectionnée, veuillez utiliser le scanner de code QR à la place", "tokenLink": "Lien vers le token", "feedback": "Retour d'information", @@ -576,5 +586,55 @@ "exemple": "GitHub" } } - } + }, + "errorUnlinkingPushToken": "Echec du découplage du push token {label}", + "@errorUnlinkingPushToken": { + "description": "Error message when unlinking a push token failed.", + "placeholders": { + "label": { + "example": "PUSH1234A" + } + } + }, + "pleaseSyncManuallyWhenNetworkIsAvailable": "Veuillez synchroniser manuellement les jetons Push via les paramètres lorsqu'une connexion réseau est disponible", + "pushTokens": "Push Tokens", + "continueButton": "Continue", + "addTokenManually": "Add token manually", + "addFolder": "Ajouter un dossier", + "searchTokens": "Jetons de recherche", + "closeSearchTokens": "Fermer la recherche", + "increaseCounter": "Augmenter le compteur", + "copyOTPToClipboard": "Copier l'OTP dans le presse-papiers", + "licenses": "Licences", + "optionalMessage": "Message optionnel", + "confirmation": "Confirmation", + "askLogSendedDescription": "Avez-vous envoyé le journal et voulez-vous l'effacer maintenant ?", + "algorithmUnsupported": "L'algorithme {algorithm} n'est pas pris en charge", + "@algorithmUnsupported": { + "placeholders": { + "algorithm": { + "example": "MD5" + } + } + }, + "thisAppIsOpenSource": "Cette application est open source\nRendez-nous visite sur GitHub", + "invalidArgument": "{argument} n'est pas une valeur valide pour {type}", + "importExportTokens": "Importer/Exporter les jetons", + "exportNonPrivacyIDEATokens": "Exporter les jetons non privacyIDEA", + "selectTokensToExport": "{count, plural, zero{} one{Sélectionner le jeton à exporter} other{Sélectionner les jetons à exporter}}", + "noTokensToExport": "Aucun jeton à exporter", + "exportAllTokens": "Exporter tous les jetons", + "export": "Exporter", + "exportingTokens": "Exportation des jetons en cours...", + "exportTokens": "Exporter les jetons", + "enterPasswordToEncrypt": "Entrez un mot de passe pour chiffrer les jetons. Ce mot de passe sera requis pour importer les jetons.", + "exportLockedTokenReason": "Veuillez vous authentifier pour exporter les jetons verrouillés.", + "fileSavedToDownloadsFolder": "Fichier enregistré dans le dossier Téléchargements", + "errorSavingFile": "Erreur lors de l'enregistrement du fichier", + "toFile": "Vers fichier", + "asQrCode": "Sous forme de code QR", + "scanThisQrWithNewDevice": "Scannez ce code QR avec votre nouvel appareil pour importer le jeton.", + "oneMore": "Encore un", + "done": "Terminé", + "confirmPassword": "Confirmer le mot de passe" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8b175eb7c..f608e8c83 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -367,6 +367,12 @@ abstract class AppLocalizations { /// **'Use device\'s theme'** String get systemTheme; + /// Tells the user, that the following tokens do not support polling. + /// + /// In en, this message translates to: + /// **'Some of the tokens are outdated and do not support polling'** + String get someTokensDoNotSupportPolling; + /// Name of the setting switch that enables polling. /// /// In en, this message translates to: @@ -1243,6 +1249,12 @@ abstract class AppLocalizations { /// **'The link entered is not a valid token of {appName}, or it is not supported.'** String invalidLink(Object appName); + /// No description provided for @importFailedToken. + /// + /// In en, this message translates to: + /// **'{count, plural, zero{No token Failed to import.} one{Failed to import a token.} other{Failed to import {count} tokens.}}'** + String importFailedToken(num count); + /// No description provided for @importExistingToken. /// /// In en, this message translates to: @@ -1252,7 +1264,7 @@ abstract class AppLocalizations { /// No description provided for @importConflictToken. /// /// In en, this message translates to: - /// **'{count, plural, zero{There is no conflict with existing tokens.} one{There is a conflict with an existing token.\nPlease choose which one you want to keep.} other{There are conflicts with existing tokens.\nPlease choose which one you want to keep.}}'** + /// **'{count, plural, zero{There is no conflict with existing tokens.} one{There is a conflict with existing tokens.\nPlease select which one you would like to keep.} other{There are conflicts with existing tokens.\nPlease select the tokens you wish to keep.}}'** String importConflictToken(num count); /// No description provided for @importNewToken. @@ -1297,6 +1309,24 @@ abstract class AppLocalizations { /// **'Select an image file with the QR code you receive when you export your accounts from Google Authenticator.\n!! Note that it is not safe to save the QR code on your device as the tokens are not encrypted !!'** String get importHintGoogleQrFile; + /// No description provided for @importHintAuthenticatorProFile. + /// + /// In en, this message translates to: + /// **'To create a backup of the Authenticator Pro app, navigate to the settings and tap on \"Auto backup\". Select a storage location and set a password. Then press \"Back up now\" to export the tokens.'** + String get importHintAuthenticatorProFile; + + /// No description provided for @importHintFreeOtpPlusQrScan. + /// + /// In en, this message translates to: + /// **'Scan the QR code you receive when you press the three dots in the tile of the token and select \"Share QR code\".'** + String get importHintFreeOtpPlusQrScan; + + /// No description provided for @importHintFreeOtpPlusFile. + /// + /// In en, this message translates to: + /// **'To create a backup of the FreeOTP+ app, tap on the three dots in the upper right corner and select \"Export\". You can choose between JSON and URI format. We recommend to delete the backup after importing it, because it is not encrypted.'** + String get importHintFreeOtpPlusFile; + /// No description provided for @qrFileDecodeError. /// /// In en, this message translates to: @@ -1404,6 +1434,240 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Sent by {issuer} for your account: \"{account}\"'** String requestInfo(Object issuer, Object account); + + /// Error message when unlinking a push token failed. + /// + /// In en, this message translates to: + /// **'Failed to unlink the push token {label}.'** + String errorUnlinkingPushToken(Object label); + + /// No description provided for @pleaseSyncManuallyWhenNetworkIsAvailable. + /// + /// In en, this message translates to: + /// **'Please synchronize the push tokens manually via the settings when a network connection is available.'** + String get pleaseSyncManuallyWhenNetworkIsAvailable; + + /// No description provided for @pushTokens. + /// + /// In en, this message translates to: + /// **'Push Tokens'** + String get pushTokens; + + /// No description provided for @continueButton. + /// + /// In en, this message translates to: + /// **'Continue'** + String get continueButton; + + /// No description provided for @addTokenManually. + /// + /// In en, this message translates to: + /// **'Add token manually'** + String get addTokenManually; + + /// No description provided for @addFolder. + /// + /// In en, this message translates to: + /// **'Add folder'** + String get addFolder; + + /// No description provided for @searchTokens. + /// + /// In en, this message translates to: + /// **'Search tokens'** + String get searchTokens; + + /// No description provided for @closeSearchTokens. + /// + /// In en, this message translates to: + /// **'Close search'** + String get closeSearchTokens; + + /// No description provided for @increaseCounter. + /// + /// In en, this message translates to: + /// **'Increase counter'** + String get increaseCounter; + + /// No description provided for @copyOTPToClipboard. + /// + /// In en, this message translates to: + /// **'Copy OTP to clipboard'** + String get copyOTPToClipboard; + + /// No description provided for @licenses. + /// + /// In en, this message translates to: + /// **'Licenses'** + String get licenses; + + /// No description provided for @optionalMessage. + /// + /// In en, this message translates to: + /// **'Optional message'** + String get optionalMessage; + + /// No description provided for @confirmation. + /// + /// In en, this message translates to: + /// **'Confirmation'** + String get confirmation; + + /// No description provided for @askLogSendedDescription. + /// + /// In en, this message translates to: + /// **'Did you send the log, and do you want to clear it now?'** + String get askLogSendedDescription; + + /// No description provided for @algorithmUnsupported. + /// + /// In en, this message translates to: + /// **'The algorithm {algorithm} is not supported'** + String algorithmUnsupported(Object algorithm); + + /// No description provided for @thisAppIsOpenSource. + /// + /// In en, this message translates to: + /// **'This Application is Open Source\nVisit us on GitHub'** + String get thisAppIsOpenSource; + + /// No description provided for @importExportTokens. + /// + /// In en, this message translates to: + /// **'Import/Export tokens'** + String get importExportTokens; + + /// No description provided for @exportNonPrivacyIDEATokens. + /// + /// In en, this message translates to: + /// **'Export non-privacyIDEA tokens'** + String get exportNonPrivacyIDEATokens; + + /// No description provided for @selectTokensToExport. + /// + /// In en, this message translates to: + /// **'{count, plural, zero{} one{Select token to export} other{Select tokens to export}}'** + String selectTokensToExport(num count); + + /// No description provided for @noTokensToExport. + /// + /// In en, this message translates to: + /// **'No tokens to export'** + String get noTokensToExport; + + /// No description provided for @exportAllTokens. + /// + /// In en, this message translates to: + /// **'Export all tokens'** + String get exportAllTokens; + + /// No description provided for @export. + /// + /// In en, this message translates to: + /// **'Export'** + String get export; + + /// No description provided for @exportingTokens. + /// + /// In en, this message translates to: + /// **'Exporting tokens...'** + String get exportingTokens; + + /// No description provided for @exportTokens. + /// + /// In en, this message translates to: + /// **'Export tokens'** + String get exportTokens; + + /// No description provided for @enterPasswordToEncrypt. + /// + /// In en, this message translates to: + /// **'Enter a password to encrypt the tokens. This password will be required to import the tokens.'** + String get enterPasswordToEncrypt; + + /// No description provided for @exportLockedTokenReason. + /// + /// In en, this message translates to: + /// **'Please authenticate to export locked tokens.'** + String get exportLockedTokenReason; + + /// No description provided for @fileSavedToDownloadsFolder. + /// + /// In en, this message translates to: + /// **'File saved to Downloads folder'** + String get fileSavedToDownloadsFolder; + + /// No description provided for @errorSavingFile. + /// + /// In en, this message translates to: + /// **'Saving to file failed'** + String get errorSavingFile; + + /// No description provided for @toFile. + /// + /// In en, this message translates to: + /// **'To file'** + String get toFile; + + /// No description provided for @asQrCode. + /// + /// In en, this message translates to: + /// **'As QR code'** + String get asQrCode; + + /// No description provided for @scanThisQrWithNewDevice. + /// + /// In en, this message translates to: + /// **'Scan this QR code with your new device to import the token.'** + String get scanThisQrWithNewDevice; + + /// No description provided for @oneMore. + /// + /// In en, this message translates to: + /// **'One more'** + String get oneMore; + + /// No description provided for @done. + /// + /// In en, this message translates to: + /// **'Done'** + String get done; + + /// No description provided for @confirmPassword. + /// + /// 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 af40ff1a4..f5ec56d65 100644 --- a/lib/l10n/app_localizations_cs.dart +++ b/lib/l10n/app_localizations_cs.dart @@ -142,6 +142,9 @@ class AppLocalizationsCs extends AppLocalizations { @override String get systemTheme => 'Použít nastavení systému'; + @override + String get someTokensDoNotSupportPolling => 'Některé tokeny jsou zastaralé a nepodporují polling'; + @override String get enablePolling => 'Povolit polling'; @@ -606,6 +609,18 @@ class AppLocalizationsCs extends AppLocalizations { return 'Zadaný odkaz není platným tokenem $appName nebo není podporován.'; } + @override + String importFailedToken(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Nepodařilo se importovat $count tokenů.', + one: 'Nepodařilo se importovat token.', + zero: 'Žádný token Nepodařilo se importovat.', + ); + return '$_temp0'; + } + @override String importExistingToken(num count) { String _temp0 = intl.Intl.pluralLogic( @@ -660,6 +675,15 @@ class AppLocalizationsCs extends AppLocalizations { @override String get importHintGoogleQrFile => 'Vyberte obrazový soubor s QR kódem, který obdržíte při exportu účtů z Google Authenticator.\n!! Upozorňujeme, že není bezpečné ukládat QR kód do zařízení, protože tokeny nejsou šifrovány !!'; + @override + String get importHintAuthenticatorProFile => 'Chcete-li vytvořit zálohu aplikace Authenticator Pro, přejděte do nastavení a klepněte na položku \"Automatické zálohování\". Vyberte umístění úložiště a nastavte heslo. Poté stiskněte \"Zálohovat nyní\" a exportujte tokeny.'; + + @override + String get importHintFreeOtpPlusQrScan => 'Naskenujte QR kód, který obdržíte po stisknutí tří teček na dlaždici tokenu, a vyberte možnost \"Sdílet QR kód\".'; + + @override + String get importHintFreeOtpPlusFile => 'Chcete-li vytvořit zálohu aplikace FreeOTP+, klepněte na tři tečky v pravém horním rohu a vyberte možnost \"Exportovat\". Můžete si vybrat mezi formátem JSON a URI. Zálohu doporučujeme po importu odstranit, protože není šifrovaná.'; + @override String get qrFileDecodeError => 'Z vybraného obrázku nebylo možné dekódovat QR kód, použijte prosím místo toho skener QR kódů.'; @@ -715,4 +739,140 @@ class AppLocalizationsCs extends AppLocalizations { String requestInfo(Object issuer, Object account) { return 'Odesláno $issuer pro váš účet: \"$account\"'; } + + @override + String errorUnlinkingPushToken(Object label) { + return 'Nepodařilo se odlinkovat push token $label.'; + } + + @override + String get pleaseSyncManuallyWhenNetworkIsAvailable => 'Synchronizujte prosím push tokeny ručně prostřednictvím nastavení, když je k dispozici síťové připojení.'; + + @override + String get pushTokens => 'Žetony Push'; + + @override + String get continueButton => 'Pokračovat'; + + @override + String get addTokenManually => 'Přidat token ručně'; + + @override + String get addFolder => 'Přidat složku'; + + @override + String get searchTokens => 'Hledat tokeny'; + + @override + String get closeSearchTokens => 'Zavřít vyhledávání'; + + @override + String get increaseCounter => 'Zvýšit počítadla'; + + @override + String get copyOTPToClipboard => 'Zkopírovat OTP do schránky'; + + @override + String get licenses => 'Licence'; + + @override + String get optionalMessage => 'Volitelná zpráva'; + + @override + String get confirmation => 'Potvrzení'; + + @override + String get askLogSendedDescription => 'Odeslali jste protokol a chcete jej nyní vymazat?'; + + @override + String algorithmUnsupported(Object algorithm) { + return 'Algoritmus $algorithm není podporován'; + } + + @override + String get thisAppIsOpenSource => 'Tato aplikace má otevřený zdrojový kód\nNavštivte nás na GitHub'; + + @override + String get importExportTokens => 'Import/Exportovat žetony'; + + @override + String get exportNonPrivacyIDEATokens => 'Exportovat ne-privacyIDEA žetony'; + + @override + String selectTokensToExport(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Vyberte žetony k exportu', + one: 'Vyberte žeton k exportu', + zero: '', + ); + return '$_temp0'; + } + + @override + String get noTokensToExport => 'Žádné žetony k exportu'; + + @override + String get exportAllTokens => 'Exportovat všechny žetony'; + + @override + String get export => 'Export'; + + @override + String get exportingTokens => 'Probíhá export žetonů...'; + + @override + String get exportTokens => 'Exportovat žetony'; + + @override + String get enterPasswordToEncrypt => 'Zadejte heslo pro šifrování žetonů. Toto heslo bude vyžadováno k importu žetonů.'; + + @override + String get exportLockedTokenReason => 'Prosím, ověřte se, abyste mohli exportovat uzamčené žetony.'; + + @override + String get fileSavedToDownloadsFolder => 'Soubor uložen do složky Stažené soubory'; + + @override + String get errorSavingFile => 'Chyba při ukládání souboru'; + + @override + String get toFile => 'Do souboru'; + + @override + String get asQrCode => 'Jako QR kód'; + + @override + String get scanThisQrWithNewDevice => 'Naskenujte tento QR kód svým novým zařízením pro import žetonu.'; + + @override + String get oneMore => 'Ještě jeden'; + + @override + String get done => 'Hotovo'; + + @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 bab37b6ca..5109fb4d8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -28,7 +28,7 @@ class AppLocalizationsDe extends AppLocalizations { String get patchNotesV4_3_0NewFeatures2 => 'Feedback-Option zu den Einstellungen hinzugefügt.'; @override - String get patchNotesV4_3_0NewFeatures3 => 'Push-Tokens können jetzt aus der Token-Liste ausgeblendet werden.'; + String get patchNotesV4_3_0NewFeatures3 => 'Push-Token können jetzt aus der Token-Liste ausgeblendet werden.'; @override String get patchNotesV4_3_0NewFeatures4 => 'Es wurden Einführungen hinzugefügt, um neuen Benutzern den Einstieg zu erleichtern.'; @@ -142,6 +142,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get systemTheme => 'Nutze Farbschema des Geräts'; + @override + String get someTokensDoNotSupportPolling => 'Einige der Token sind veraltet und unterstützen keine aktiven Anfragen'; + @override String get enablePolling => 'Aktives Stellen von Push-Anfragen'; @@ -606,6 +609,18 @@ class AppLocalizationsDe extends AppLocalizations { return 'Der eingegebene Link ist kein gültiger Token von $appName, oder er wird nicht unterstützt.'; } + @override + String importFailedToken(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Der Import von $count Token ist fehlgeschlagen.', + one: 'Importieren eines Tokens fehlgeschlagen.', + zero: 'Kein Token konnte nicht importiert werden.', + ); + return '$_temp0'; + } + @override String importExistingToken(num count) { String _temp0 = intl.Intl.pluralLogic( @@ -623,8 +638,8 @@ class AppLocalizationsDe extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Es besteht ein Konflikt mit bereits existierenden Token.\nBitte wählen Sie aus, welches Sie behalten möchten.', - one: 'Es besteht ein Konflikt mit bereits existierenden Token.\nBitte wählen Sie aus, welches Sie behalten möchten.', + other: 'Es bestehen Konflikte mit bereits vorhandenen Token.\nBitte wählen Sie die Token aus, die Sie behalten möchten.', + one: 'Es besteht ein Konflikt mit bereits vorhandenen Token.\nBitte wählen Sie aus, welches Sie behalten möchten.', zero: 'Es besteht kein Konflikt mit bereits existierenden Token.', ); return '$_temp0'; @@ -660,6 +675,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get importHintGoogleQrFile => 'Wählen Sie eine Bilddatei mit dem QR-Code, den Sie erhalten, wenn Sie Ihre Konten aus dem Google Authenticator exportieren.\n!! Der QR-Code enthält die Token in unverschlüsselter Form. Es ist deshalb nicht sicher, diesen länger als nötig aufzubewahren !!'; + @override + String get importHintAuthenticatorProFile => 'Um ein Backup der Authenticator Pro-App zu erstellen navigieren Sie zu den Einstellungen und tippen Sie auf \"Automatische Sicherung\". Wählen Sie einen Speicherort und setzen Sie ein Passwort. Anschließend drücken Sie auf \"Jetzt sichern\" um die Token zu exportieren.'; + + @override + String get importHintFreeOtpPlusQrScan => 'Scannen Sie den QR-Code, den Sie erhalten, wenn Sie auf die drei Punkte in der Kachel des Tokens drücken, und wählen Sie \"QR-Code teilen\".'; + + @override + String get importHintFreeOtpPlusFile => 'Um ein Backup der FreeOTP+ App zu erstellen, tippen Sie auf die drei Punkte in der oberen rechten Ecke und wählen Sie \"Exportieren\". Sie können zwischen dem JSON- und dem URI-Format wählen. Wir empfehlen, das Backup nach dem Importieren zu löschen, da es nicht verschlüsselt ist.'; + @override String get qrFileDecodeError => 'Es war nicht möglich, den QR-Code aus dem ausgewählten Bild zu dekodieren. Bitte verwenden Sie stattdessen den QR-Code-Scanner.'; @@ -715,4 +739,140 @@ class AppLocalizationsDe extends AppLocalizations { String requestInfo(Object issuer, Object account) { return 'Gesendet von $issuer für Ihr Konto: \"$account\"'; } + + @override + String errorUnlinkingPushToken(Object label) { + return 'Entkoppeln des Push Tokens $label fehlgeschlagen.'; + } + + @override + String get pleaseSyncManuallyWhenNetworkIsAvailable => 'Bitte synchronisieren Sie die Push Token über die Einstellungen manuell, wenn eine Netzwerkverbindung verfügbar ist.'; + + @override + String get pushTokens => 'Push-Token'; + + @override + String get continueButton => 'Weiter'; + + @override + String get addTokenManually => 'Token manuell hinzufügen'; + + @override + String get addFolder => 'Ordner hinzufügen'; + + @override + String get searchTokens => 'Token suchen'; + + @override + String get closeSearchTokens => 'Suche schließen'; + + @override + String get increaseCounter => 'Zähler erhöhen'; + + @override + String get copyOTPToClipboard => 'OTP in die Zwischenablage kopieren'; + + @override + String get licenses => 'Lizenzen'; + + @override + String get optionalMessage => 'Optionale Nachricht'; + + @override + String get confirmation => 'Confirmation'; + + @override + String get askLogSendedDescription => 'Haben Sie das Protokoll gesendet, und möchten Sie es jetzt löschen?'; + + @override + String algorithmUnsupported(Object algorithm) { + return 'Der Algorithmus $algorithm wird nicht unterstützt'; + } + + @override + String get thisAppIsOpenSource => 'Diese App ist Open Source\nBesuchen Sie uns auf GitHub'; + + @override + String get importExportTokens => 'Token importieren/exportieren'; + + @override + String get exportNonPrivacyIDEATokens => 'Nicht-privacyIDEA-Token exportieren'; + + @override + String selectTokensToExport(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Wählen Sie die zu exportierenden Tokens aus', + one: 'Wählen Sie das zu exportierende Token aus', + zero: '', + ); + return '$_temp0'; + } + + @override + String get noTokensToExport => 'Keine Tokens zum Exportieren'; + + @override + String get exportAllTokens => 'Alle Tokens exportieren'; + + @override + String get export => 'Exportieren'; + + @override + String get exportingTokens => 'Tokens werden exportiert...'; + + @override + String get exportTokens => 'Tokens exportieren'; + + @override + String get enterPasswordToEncrypt => 'Geben Sie ein Passwort ein, um die Tokens zu verschlüsseln. Dieses Passwort wird benötigt, um die Tokens zu importieren.'; + + @override + String get exportLockedTokenReason => 'Bitte authentifizieren Sie sich, um gesperrte Tokens zu exportieren.'; + + @override + String get fileSavedToDownloadsFolder => 'Datei wurde im Download-Ordner gespeichert'; + + @override + String get errorSavingFile => 'Fehler beim Speichern der Datei'; + + @override + String get toFile => 'In Datei'; + + @override + String get asQrCode => 'Als QR-Code'; + + @override + String get scanThisQrWithNewDevice => 'Scannen Sie diesen QR-Code mit Ihrem neuen Gerät, um das Token zu importieren.'; + + @override + String get oneMore => 'Noch eins'; + + @override + String get done => 'Fertig'; + + @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 ddd482bad..ca4f6163b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -142,6 +142,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get systemTheme => 'Use device\'s theme'; + @override + String get someTokensDoNotSupportPolling => 'Some of the tokens are outdated and do not support polling'; + @override String get enablePolling => 'Enable polling'; @@ -606,6 +609,18 @@ class AppLocalizationsEn extends AppLocalizations { return 'The link entered is not a valid token of $appName, or it is not supported.'; } + @override + String importFailedToken(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Failed to import $count tokens.', + one: 'Failed to import a token.', + zero: 'No token Failed to import.', + ); + return '$_temp0'; + } + @override String importExistingToken(num count) { String _temp0 = intl.Intl.pluralLogic( @@ -623,8 +638,8 @@ class AppLocalizationsEn extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'There are conflicts with existing tokens.\nPlease choose which one you want to keep.', - one: 'There is a conflict with an existing token.\nPlease choose which one you want to keep.', + other: 'There are conflicts with existing tokens.\nPlease select the tokens you wish to keep.', + one: 'There is a conflict with existing tokens.\nPlease select which one you would like to keep.', zero: 'There is no conflict with existing tokens.', ); return '$_temp0'; @@ -660,6 +675,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get importHintGoogleQrFile => 'Select an image file with the QR code you receive when you export your accounts from Google Authenticator.\n!! Note that it is not safe to save the QR code on your device as the tokens are not encrypted !!'; + @override + String get importHintAuthenticatorProFile => 'To create a backup of the Authenticator Pro app, navigate to the settings and tap on \"Auto backup\". Select a storage location and set a password. Then press \"Back up now\" to export the tokens.'; + + @override + String get importHintFreeOtpPlusQrScan => 'Scan the QR code you receive when you press the three dots in the tile of the token and select \"Share QR code\".'; + + @override + String get importHintFreeOtpPlusFile => 'To create a backup of the FreeOTP+ app, tap on the three dots in the upper right corner and select \"Export\". You can choose between JSON and URI format. We recommend to delete the backup after importing it, because it is not encrypted.'; + @override String get qrFileDecodeError => 'It was not possible to decode the QR code from the selected image, please use the QR code scanner instead.'; @@ -715,4 +739,140 @@ class AppLocalizationsEn extends AppLocalizations { String requestInfo(Object issuer, Object account) { return 'Sent by $issuer for your account: \"$account\"'; } + + @override + String errorUnlinkingPushToken(Object label) { + return 'Failed to unlink the push token $label.'; + } + + @override + String get pleaseSyncManuallyWhenNetworkIsAvailable => 'Please synchronize the push tokens manually via the settings when a network connection is available.'; + + @override + String get pushTokens => 'Push Tokens'; + + @override + String get continueButton => 'Continue'; + + @override + String get addTokenManually => 'Add token manually'; + + @override + String get addFolder => 'Add folder'; + + @override + String get searchTokens => 'Search tokens'; + + @override + String get closeSearchTokens => 'Close search'; + + @override + String get increaseCounter => 'Increase counter'; + + @override + String get copyOTPToClipboard => 'Copy OTP to clipboard'; + + @override + String get licenses => 'Licenses'; + + @override + String get optionalMessage => 'Optional message'; + + @override + String get confirmation => 'Confirmation'; + + @override + String get askLogSendedDescription => 'Did you send the log, and do you want to clear it now?'; + + @override + String algorithmUnsupported(Object algorithm) { + return 'The algorithm $algorithm is not supported'; + } + + @override + String get thisAppIsOpenSource => 'This Application is Open Source\nVisit us on GitHub'; + + @override + String get importExportTokens => 'Import/Export tokens'; + + @override + String get exportNonPrivacyIDEATokens => 'Export non-privacyIDEA tokens'; + + @override + String selectTokensToExport(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Select tokens to export', + one: 'Select token to export', + zero: '', + ); + return '$_temp0'; + } + + @override + String get noTokensToExport => 'No tokens to export'; + + @override + String get exportAllTokens => 'Export all tokens'; + + @override + String get export => 'Export'; + + @override + String get exportingTokens => 'Exporting tokens...'; + + @override + String get exportTokens => 'Export tokens'; + + @override + String get enterPasswordToEncrypt => 'Enter a password to encrypt the tokens. This password will be required to import the tokens.'; + + @override + String get exportLockedTokenReason => 'Please authenticate to export locked tokens.'; + + @override + String get fileSavedToDownloadsFolder => 'File saved to Downloads folder'; + + @override + String get errorSavingFile => 'Saving to file failed'; + + @override + String get toFile => 'To file'; + + @override + String get asQrCode => 'As QR code'; + + @override + String get scanThisQrWithNewDevice => 'Scan this QR code with your new device to import the token.'; + + @override + String get oneMore => 'One more'; + + @override + String get done => 'Done'; + + @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 420cd7d7a..046d5d766 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -142,6 +142,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get systemTheme => 'Utilizar el tema del teléfono'; + @override + String get someTokensDoNotSupportPolling => 'Algunos tokens están obsoletos y no admiten la consulta activa para la autenticación mediante mensaje push.'; + @override String get enablePolling => 'Activar polling'; @@ -606,6 +609,18 @@ class AppLocalizationsEs extends AppLocalizations { return 'El enlace introducido no es un token válido de $appName, o no es compatible'; } + @override + String importFailedToken(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Error al importar $count tokens.', + one: 'Error al importar un token.', + zero: 'No token Fallo al importar.', + ); + return '$_temp0'; + } + @override String importExistingToken(num count) { String _temp0 = intl.Intl.pluralLogic( @@ -660,6 +675,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get importHintGoogleQrFile => 'Selecciona un archivo de imagen con el código QR que recibes al exportar tus cuentas desde Google Authenticator.\n!! Tenga en cuenta que no es seguro guardar el código QR en su dispositivo, ya que los tokens no están cifrados !!'; + @override + String get importHintAuthenticatorProFile => 'Para crear una copia de seguridad de la aplicación Authenticator Pro, vaya a la configuración y pulse en \"Copia de seguridad automática\". Seleccione una ubicación de almacenamiento y establezca una contraseña. A continuación, pulse \"Hacer copia de seguridad ahora\" para exportar los tokens.'; + + @override + String get importHintFreeOtpPlusQrScan => 'Escanea el código QR que recibes al pulsar los tres puntos en el azulejo de la ficha y selecciona \"Compartir código QR\".'; + + @override + String get importHintFreeOtpPlusFile => 'Para crear una copia de seguridad de la app FreeOTP+, pulse los tres puntos de la esquina superior derecha y seleccione \"Exportar\". Puede elegir entre los formatos JSON y URI. Recomendamos eliminar la copia de seguridad después de importarla, ya que no está cifrada.'; + @override String get qrFileDecodeError => 'No fue posible decodificar el código QR de la imagen seleccionada, por favor utilice el escáner de código QR en su lugar.'; @@ -715,4 +739,140 @@ class AppLocalizationsEs extends AppLocalizations { String requestInfo(Object issuer, Object account) { return 'Enviado por $issuer para su cuenta: \"$account\"'; } + + @override + String errorUnlinkingPushToken(Object label) { + return 'Error al desvincular el token push $label'; + } + + @override + String get pleaseSyncManuallyWhenNetworkIsAvailable => 'Por favor, sincronice los tokens push manualmente a través de los ajustes cuando haya una conexión de red disponible.'; + + @override + String get pushTokens => 'Push Tokens'; + + @override + String get continueButton => 'Continue'; + + @override + String get addTokenManually => 'Añadir token manualmente'; + + @override + String get addFolder => 'Añadir carpeta'; + + @override + String get searchTokens => 'Buscar tokens'; + + @override + String get closeSearchTokens => 'Cerrar búsqueda'; + + @override + String get increaseCounter => 'Incrementar contador'; + + @override + String get copyOTPToClipboard => 'Copiar OTP al portapapeles'; + + @override + String get licenses => 'Licencias'; + + @override + String get optionalMessage => 'Mensaje opcional'; + + @override + String get confirmation => 'confirmación'; + + @override + String get askLogSendedDescription => '¿Ha enviado el registro y desea borrarlo ahora?'; + + @override + String algorithmUnsupported(Object algorithm) { + return 'El algoritmo $algorithm no es compatible'; + } + + @override + String get thisAppIsOpenSource => 'Esta aplicación es de código abierto\nVisítanos en GitHub'; + + @override + String get importExportTokens => 'Importar/Exportar tokens'; + + @override + String get exportNonPrivacyIDEATokens => 'Exportar tokens no privacyIDEA'; + + @override + String selectTokensToExport(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Seleccionar tokens para exportar', + one: 'Seleccionar token para exportar', + zero: '', + ); + return '$_temp0'; + } + + @override + String get noTokensToExport => 'No hay tokens para exportar'; + + @override + String get exportAllTokens => 'Exportar todos los tokens'; + + @override + String get export => 'Exportar'; + + @override + String get exportingTokens => 'Exportando tokens...'; + + @override + String get exportTokens => 'Exportar tokens'; + + @override + String get enterPasswordToEncrypt => 'Ingrese una contraseña para cifrar los tokens. Esta contraseña será necesaria para importar los tokens.'; + + @override + String get exportLockedTokenReason => 'Por favor, autentíquese para exportar tokens bloqueados.'; + + @override + String get fileSavedToDownloadsFolder => 'Archivo guardado en la carpeta de descargas'; + + @override + String get errorSavingFile => 'Error al guardar el archivo'; + + @override + String get toFile => 'A archivo'; + + @override + String get asQrCode => 'Como código QR'; + + @override + String get scanThisQrWithNewDevice => 'Escanee este código QR con su nuevo dispositivo para importar el token.'; + + @override + String get oneMore => 'Uno más'; + + @override + String get done => 'Hecho'; + + @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 1e69191cf..0b7c478da 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -142,6 +142,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get systemTheme => 'Utiliser le thème de l\'appareil'; + @override + String get someTokensDoNotSupportPolling => 'Certains jetons sont obsolètes et ne supportent pas l\'interrogation due serveur.'; + @override String get enablePolling => 'Activer l\'interrogation du serveur.'; @@ -606,6 +609,18 @@ class AppLocalizationsFr extends AppLocalizations { return 'Le lien saisi n\'est pas un jeton valide de $appName, ou il n\'est pas pris en charge'; } + @override + String importFailedToken(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Échec de l\'importation des jetons $count.', + one: 'Échec de l\'importation d\'un jeton.', + zero: 'Pas de jeton Échec de l\'importation.', + ); + return '$_temp0'; + } + @override String importExistingToken(num count) { String _temp0 = intl.Intl.pluralLogic( @@ -660,6 +675,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get importHintGoogleQrFile => 'Sélectionnez un fichier image avec le code QR que vous obtenez lorsque vous exportez vos comptes depuis Google Authenticator.\n!! Notez qu\'il n\'est pas sûr d\'enregistrer le code QR sur votre appareil, car les jetons ne sont pas cryptés !!'; + @override + String get importHintAuthenticatorProFile => 'Pour créer une sauvegarde de l\'application Authenticator Pro, accédez aux paramètres et appuyez sur \"Sauvegarde automatique\". Sélectionnez un emplacement de stockage et définissez un mot de passe. Puis appuyez sur \"Sauvegarder maintenant\" pour exporter les tokens.'; + + @override + String get importHintFreeOtpPlusQrScan => 'Scannez le code QR que vous recevez lorsque vous appuyez sur les trois points dans la tuile du jeton et sélectionnez \"Partager le code QR\".'; + + @override + String get importHintFreeOtpPlusFile => 'Pour créer une sauvegarde de l\'application FreeOTP+, appuyez sur les trois points dans le coin supérieur droit et sélectionnez \"Exporter\". Vous pouvez choisir entre les formats JSON et URI. Nous recommandons de supprimer la sauvegarde après l\'avoir importée, car elle n\'est pas cryptée.'; + @override String get qrFileDecodeError => 'Il n\'a pas été possible de décoder le code QR à partir de l\'image sélectionnée, veuillez utiliser le scanner de code QR à la place'; @@ -715,4 +739,140 @@ class AppLocalizationsFr extends AppLocalizations { String requestInfo(Object issuer, Object account) { return 'Envoyé par $issuer pour votre compte : \"$account\"'; } + + @override + String errorUnlinkingPushToken(Object label) { + return 'Echec du découplage du push token $label'; + } + + @override + String get pleaseSyncManuallyWhenNetworkIsAvailable => 'Veuillez synchroniser manuellement les jetons Push via les paramètres lorsqu\'une connexion réseau est disponible'; + + @override + String get pushTokens => 'Push Tokens'; + + @override + String get continueButton => 'Continue'; + + @override + String get addTokenManually => 'Add token manually'; + + @override + String get addFolder => 'Ajouter un dossier'; + + @override + String get searchTokens => 'Jetons de recherche'; + + @override + String get closeSearchTokens => 'Fermer la recherche'; + + @override + String get increaseCounter => 'Augmenter le compteur'; + + @override + String get copyOTPToClipboard => 'Copier l\'OTP dans le presse-papiers'; + + @override + String get licenses => 'Licences'; + + @override + String get optionalMessage => 'Message optionnel'; + + @override + String get confirmation => 'Confirmation'; + + @override + String get askLogSendedDescription => 'Avez-vous envoyé le journal et voulez-vous l\'effacer maintenant ?'; + + @override + String algorithmUnsupported(Object algorithm) { + return 'L\'algorithme $algorithm n\'est pas pris en charge'; + } + + @override + String get thisAppIsOpenSource => 'Cette application est open source\nRendez-nous visite sur GitHub'; + + @override + String get importExportTokens => 'Importer/Exporter les jetons'; + + @override + String get exportNonPrivacyIDEATokens => 'Exporter les jetons non privacyIDEA'; + + @override + String selectTokensToExport(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Sélectionner les jetons à exporter', + one: 'Sélectionner le jeton à exporter', + zero: '', + ); + return '$_temp0'; + } + + @override + String get noTokensToExport => 'Aucun jeton à exporter'; + + @override + String get exportAllTokens => 'Exporter tous les jetons'; + + @override + String get export => 'Exporter'; + + @override + String get exportingTokens => 'Exportation des jetons en cours...'; + + @override + String get exportTokens => 'Exporter les jetons'; + + @override + String get enterPasswordToEncrypt => 'Entrez un mot de passe pour chiffrer les jetons. Ce mot de passe sera requis pour importer les jetons.'; + + @override + String get exportLockedTokenReason => 'Veuillez vous authentifier pour exporter les jetons verrouillés.'; + + @override + String get fileSavedToDownloadsFolder => 'Fichier enregistré dans le dossier Téléchargements'; + + @override + String get errorSavingFile => 'Erreur lors de l\'enregistrement du fichier'; + + @override + String get toFile => 'Vers fichier'; + + @override + String get asQrCode => 'Sous forme de code QR'; + + @override + String get scanThisQrWithNewDevice => 'Scannez ce code QR avec votre nouvel appareil pour importer le jeton.'; + + @override + String get oneMore => 'Encore un'; + + @override + String get done => 'Terminé'; + + @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 b2bd2d0c0..372e68eb5 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -142,6 +142,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get systemTheme => 'Gebruik thema van het apparaat'; + @override + String get someTokensDoNotSupportPolling => 'Sommige tokens zijn verouderd en ondersteunen geen actief zoeken'; + @override String get enablePolling => 'Zoeken aanzetten'; @@ -606,6 +609,18 @@ class AppLocalizationsNl extends AppLocalizations { return 'De ingevoerde link is geen geldig token van $appName, of wordt niet ondersteund.'; } + @override + String importFailedToken(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Kan $count tokens niet importeren.', + one: 'Kan geen token importeren.', + zero: 'Geen token Niet geïmporteerd.', + ); + return '$_temp0'; + } + @override String importExistingToken(num count) { String _temp0 = intl.Intl.pluralLogic( @@ -660,6 +675,15 @@ class AppLocalizationsNl extends AppLocalizations { @override String get importHintGoogleQrFile => 'Selecteer een afbeeldingsbestand met de QR-code die u ontvangt wanneer u uw accounts exporteert vanuit Google Authenticator.\n!! Let op: het is niet veilig om de QR-code op je apparaat op te slaan, omdat de tokens niet versleuteld zijn !!'; + @override + String get importHintAuthenticatorProFile => 'Om een back-up te maken van de Authenticator Pro app, navigeer je naar de instellingen en tik je op \"Auto back-up\". Selecteer een opslaglocatie en stel een wachtwoord in. Druk vervolgens op \"Nu back-uppen\" om de tokens te exporteren.'; + + @override + String get importHintFreeOtpPlusQrScan => 'Scan de QR-code die u ontvangt wanneer u op de drie stippen in de tegel van de token drukt en selecteer \"QR-code delen\".'; + + @override + String get importHintFreeOtpPlusFile => 'Om een back-up van de FreeOTP+ app te maken, tikt u op de drie puntjes in de rechterbovenhoek en selecteert u \"Exporteren\". U kunt kiezen tussen JSON en URI formaat. We raden u aan de back-up te verwijderen na het importeren, omdat deze niet versleuteld is.'; + @override String get qrFileDecodeError => 'Het was niet mogelijk om de QR code te decoderen van de geselecteerde afbeelding, gebruik in plaats daarvan de QR code scanner.'; @@ -715,4 +739,140 @@ class AppLocalizationsNl extends AppLocalizations { String requestInfo(Object issuer, Object account) { return 'Verzonden door $issuer voor uw account: \"$account\"'; } + + @override + String errorUnlinkingPushToken(Object label) { + return 'Het is niet gelukt om het push token $label te ontkoppelen.'; + } + + @override + String get pleaseSyncManuallyWhenNetworkIsAvailable => 'Synchroniseer de push tokens handmatig via de instellingen als er een netwerkverbinding beschikbaar is.'; + + @override + String get pushTokens => 'Push Tokens'; + + @override + String get continueButton => 'Ga verder'; + + @override + String get addTokenManually => 'Voeg token handmatig toe'; + + @override + String get addFolder => 'Map toevoegen'; + + @override + String get searchTokens => 'Zoek tokens'; + + @override + String get closeSearchTokens => 'Zoekopdracht sluiten'; + + @override + String get increaseCounter => 'Verhoog teller'; + + @override + String get copyOTPToClipboard => 'Kopieer OTP naar klembord'; + + @override + String get licenses => 'Licenties'; + + @override + String get optionalMessage => 'Optioneel bericht'; + + @override + String get confirmation => 'Bevestiging'; + + @override + String get askLogSendedDescription => 'Heb je het logboek verzonden en wil je het nu wissen?'; + + @override + String algorithmUnsupported(Object algorithm) { + return 'Het algoritme $algorithm wordt niet ondersteund'; + } + + @override + String get thisAppIsOpenSource => 'Deze app is open source\nBezoek ons op GitHub'; + + @override + String get importExportTokens => 'Tokens importeren/exporteren'; + + @override + String get exportNonPrivacyIDEATokens => 'Niet-privacyIDEA tokens exporteren'; + + @override + String selectTokensToExport(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Selecteer tokens om te exporteren', + one: 'Selecteer token om te exporteren', + zero: '', + ); + return '$_temp0'; + } + + @override + String get noTokensToExport => 'Geen tokens om te exporteren'; + + @override + String get exportAllTokens => 'Alle tokens exporteren'; + + @override + String get export => 'Exporteren'; + + @override + String get exportingTokens => 'Tokens exporteren...'; + + @override + String get exportTokens => 'Tokens exporteren'; + + @override + String get enterPasswordToEncrypt => 'Voer een wachtwoord in om de tokens te versleutelen. Dit wachtwoord is vereist om de tokens te importeren.'; + + @override + String get exportLockedTokenReason => 'Authenticeer om vergrendelde tokens te exporteren.'; + + @override + String get fileSavedToDownloadsFolder => 'Bestand opgeslagen in de map Downloads'; + + @override + String get errorSavingFile => 'Fout bij het opslaan van het bestand'; + + @override + String get toFile => 'Naar bestand'; + + @override + String get asQrCode => 'Als QR-code'; + + @override + String get scanThisQrWithNewDevice => 'Scan deze QR-code met uw nieuwe apparaat om de token te importeren.'; + + @override + String get oneMore => 'Nog een'; + + @override + String get done => 'Klaar'; + + @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 1ec61e612..e9d999ede 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -142,6 +142,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get systemTheme => 'Motyw systemu'; + @override + String get someTokensDoNotSupportPolling => 'Część tokenów jest przestarzała i nie wspiera aktywnego zapytania dla autentykacji przez wiadomość push.'; + @override String get enablePolling => 'Włącz autentykację przez wiadomość push.'; @@ -606,6 +609,18 @@ class AppLocalizationsPl extends AppLocalizations { return 'Wprowadzony link nie jest prawidłowym tokenem $appName lub nie jest obsługiwany.'; } + @override + String importFailedToken(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Nie udało się zaimportować $count tokenów.', + one: 'Nie udało się zaimportować tokena.', + zero: 'Nie udało się zaimportować tokenu.', + ); + return '$_temp0'; + } + @override String importExistingToken(num count) { String _temp0 = intl.Intl.pluralLogic( @@ -660,6 +675,15 @@ class AppLocalizationsPl extends AppLocalizations { @override String get importHintGoogleQrFile => 'Wybierz plik obrazu z kodem QR otrzymanym podczas eksportowania kont z Google Authenticator.\n!! Należy pamiętać, że zapisywanie kodu QR na urządzeniu nie jest bezpieczne, ponieważ tokeny nie są szyfrowane !!'; + @override + String get importHintAuthenticatorProFile => 'Aby utworzyć kopię zapasową aplikacji Authenticator Pro, przejdź do ustawień i dotknij \"Automatyczna kopia zapasowa\". Wybierz lokalizację przechowywania i ustaw hasło. Następnie naciśnij \"Utwórz teraz kopię zapasową\", aby wyeksportować tokeny.'; + + @override + String get importHintFreeOtpPlusQrScan => 'Zeskanuj kod QR otrzymany po naciśnięciu trzech kropek na kafelku tokena i wybierz \"Udostępnij kod QR\".'; + + @override + String get importHintFreeOtpPlusFile => 'Aby utworzyć kopię zapasową aplikacji FreeOTP+, dotknij trzech kropek w prawym górnym rogu i wybierz \"Eksportuj\". Można wybrać format JSON lub URI. Zalecamy usunięcie kopii zapasowej po jej zaimportowaniu, ponieważ nie jest ona szyfrowana.'; + @override String get qrFileDecodeError => 'Nie można było zdekodować kodu QR z wybranego obrazu, zamiast tego użyj skanera kodów QR.'; @@ -715,4 +739,140 @@ class AppLocalizationsPl extends AppLocalizations { String requestInfo(Object issuer, Object account) { return 'Wysłane przez $issuer dla twojego konta: \"$account\"'; } + + @override + String errorUnlinkingPushToken(Object label) { + return 'Nie udało się odłączyć tokenu push $label.'; + } + + @override + String get pleaseSyncManuallyWhenNetworkIsAvailable => 'Zsynchronizuj tokeny push ręcznie za pomocą ustawień, gdy dostępne jest połączenie sieciowe'; + + @override + String get pushTokens => 'Tokeny push'; + + @override + String get continueButton => 'Kontynuuj'; + + @override + String get addTokenManually => 'Dodaj token ręcznie'; + + @override + String get addFolder => 'Dodaj folder'; + + @override + String get searchTokens => 'Wyszukiwanie tokenów'; + + @override + String get closeSearchTokens => 'Zamknij wyszukiwanie'; + + @override + String get increaseCounter => 'Zwiększ licznik'; + + @override + String get copyOTPToClipboard => 'Kopiowanie OTP do schowka'; + + @override + String get licenses => 'Licencja'; + + @override + String get optionalMessage => 'Opcjonalna wiadomość'; + + @override + String get confirmation => 'potwierdzenie'; + + @override + String get askLogSendedDescription => 'Czy wysłałeś dziennik i czy chcesz go teraz wyczyścić?'; + + @override + String algorithmUnsupported(Object algorithm) { + return 'Algorytm $algorithm nie jest obsługiwany'; + } + + @override + String get thisAppIsOpenSource => 'Ta aplikacja jest open source\nOdwiedź nas na GitHub'; + + @override + String get importExportTokens => 'Importuj/Eksportuj tokeny'; + + @override + String get exportNonPrivacyIDEATokens => 'Eksportuj tokeny niebędące privacyIDEA'; + + @override + String selectTokensToExport(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Wybierz tokeny do wyeksportowania', + one: 'Wybierz token do wyeksportowania', + zero: '', + ); + return '$_temp0'; + } + + @override + String get noTokensToExport => 'Brak tokenów do wyeksportowania'; + + @override + String get exportAllTokens => 'Eksportuj wszystkie tokeny'; + + @override + String get export => 'Eksportuj'; + + @override + String get exportingTokens => 'Eksportowanie tokenów...'; + + @override + String get exportTokens => 'Eksportuj tokeny'; + + @override + String get enterPasswordToEncrypt => 'Wprowadź hasło, aby zaszyfrować tokeny. To hasło będzie wymagane do importu tokenów.'; + + @override + String get exportLockedTokenReason => 'Proszę uwierzytelnić się, aby wyeksportować zablokowane tokeny.'; + + @override + String get fileSavedToDownloadsFolder => 'Plik zapisano w folderze Pobrane'; + + @override + String get errorSavingFile => 'Błąd podczas zapisywania pliku'; + + @override + String get toFile => 'Do pliku'; + + @override + String get asQrCode => 'Jako kod QR'; + + @override + String get scanThisQrWithNewDevice => 'Zeskanuj ten kod QR za pomocą nowego urządzenia, aby zaimportować token.'; + + @override + String get oneMore => 'Jeszcze jeden'; + + @override + String get done => 'Gotowe'; + + @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/l10n/app_nl.arb b/lib/l10n/app_nl.arb index edc35027b..f335e4b29 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -157,6 +157,12 @@ "@systemTheme": { "description": "The systems theme." }, + "someTokensDoNotSupportPolling": "Sommige tokens zijn verouderd en ondersteunen geen actief zoeken", + "@someTokensDoNotSupportPolling": { + "description": "Tells the user, that the following tokens do not support polling.", + "type": "text", + "placeholders": {} + }, "enablePolling": "Zoeken aanzetten", "@enablePolling": { "description": "Name of the setting switch that enables polling." @@ -533,6 +539,7 @@ "invalidQrScan": "De gescande QR code is geen geldige backup van {appName}.", "invalidQrFile": "Het geselecteerde bestand bevat geen geldige QR code van {appName}.", "invalidLink": "De ingevoerde link is geen geldig token van {appName}, of wordt niet ondersteund.", + "importFailedToken": "{count, plural, zero{Geen token Niet geïmporteerd.} one{Kan geen token importeren.} other{Kan {count} tokens niet importeren.}}", "importExistingToken": "{count, plural, zero{Er is geen token gevonden dat al in de toepassing aanwezig is.} one{Er is een token gevonden dat al bestaat in de applicatie.} other{{count} tokens gevonden die al in de applicatie staan.}}", "importConflictToken": "{count, plural, zero{Er is geen conflict met tokens die al bestaan.} one{Er is een conflict met tokens die al bestaan.Selecteer welke u wilt behouden.} other{Er is een conflict met tokens die al bestaan.Selecteer welke u wilt behouden.}}", "importNewToken": "{count, plural, zero{Er is geen nieuw token gevonden.} one{Er is een nieuw token gevonden dat zal worden geïmporteerd.} other{Er is een nieuw token {count} gevonden dat zal worden geïmporteerd.}}", @@ -542,6 +549,9 @@ "importHintAegisLink": "Voer de link in die u ontvangt wanneer u vermeldingen van Aegis overdraagt.", "importHintGoogleQrScan": "Scan de QR-code die u ontvangt wanneer u uw accounts exporteert vanuit Google Authenticator.", "importHintGoogleQrFile": "Selecteer een afbeeldingsbestand met de QR-code die u ontvangt wanneer u uw accounts exporteert vanuit Google Authenticator.\n!! Let op: het is niet veilig om de QR-code op je apparaat op te slaan, omdat de tokens niet versleuteld zijn !!", + "importHintAuthenticatorProFile": "Om een back-up te maken van de Authenticator Pro app, navigeer je naar de instellingen en tik je op \"Auto back-up\". Selecteer een opslaglocatie en stel een wachtwoord in. Druk vervolgens op \"Nu back-uppen\" om de tokens te exporteren.", + "importHintFreeOtpPlusQrScan": "Scan de QR-code die u ontvangt wanneer u op de drie stippen in de tegel van de token drukt en selecteer \"QR-code delen\".", + "importHintFreeOtpPlusFile": "Om een back-up van de FreeOTP+ app te maken, tikt u op de drie puntjes in de rechterbovenhoek en selecteert u \"Exporteren\". U kunt kiezen tussen JSON en URI formaat. We raden u aan de back-up te verwijderen na het importeren, omdat deze niet versleuteld is.", "qrFileDecodeError": "Het was niet mogelijk om de QR code te decoderen van de geselecteerde afbeelding, gebruik in plaats daarvan de QR code scanner.", "tokenLink": "tokenlink", "feedback": "Feedback", @@ -572,5 +582,55 @@ "example": "GitHub" } } - } + }, + "errorUnlinkingPushToken": "Het is niet gelukt om het push token {label} te ontkoppelen.", + "@errorUnlinkingPushToken": { + "description": "Error message when unlinking a push token failed.", + "placeholders": { + "label": { + "example": "PUSH1234A" + } + } + }, + "pleaseSyncManuallyWhenNetworkIsAvailable": "Synchroniseer de push tokens handmatig via de instellingen als er een netwerkverbinding beschikbaar is.", + "pushTokens": "Push Tokens", + "continueButton": "Ga verder", + "addTokenManually": "Voeg token handmatig toe", + "addFolder": "Map toevoegen", + "searchTokens": "Zoek tokens", + "closeSearchTokens": "Zoekopdracht sluiten", + "increaseCounter": "Verhoog teller", + "copyOTPToClipboard": "Kopieer OTP naar klembord", + "licenses": "Licenties", + "optionalMessage": "Optioneel bericht", + "confirmation": "Bevestiging", + "askLogSendedDescription": "Heb je het logboek verzonden en wil je het nu wissen?", + "algorithmUnsupported": "Het algoritme {algorithm} wordt niet ondersteund", + "@algorithmUnsupported": { + "placeholders": { + "algorithm": { + "example": "MD5" + } + } + }, + "thisAppIsOpenSource": "Deze app is open source\nBezoek ons op GitHub", + "invalidArgument": "{argument} is geen geldige waarde voor {type}", + "importExportTokens": "Tokens importeren/exporteren", + "exportNonPrivacyIDEATokens": "Niet-privacyIDEA tokens exporteren", + "selectTokensToExport": "{count, plural, zero{} one{Selecteer token om te exporteren} other{Selecteer tokens om te exporteren}}", + "noTokensToExport": "Geen tokens om te exporteren", + "exportAllTokens": "Alle tokens exporteren", + "export": "Exporteren", + "exportingTokens": "Tokens exporteren...", + "exportTokens": "Tokens exporteren", + "enterPasswordToEncrypt": "Voer een wachtwoord in om de tokens te versleutelen. Dit wachtwoord is vereist om de tokens te importeren.", + "exportLockedTokenReason": "Authenticeer om vergrendelde tokens te exporteren.", + "fileSavedToDownloadsFolder": "Bestand opgeslagen in de map Downloads", + "errorSavingFile": "Fout bij het opslaan van het bestand", + "toFile": "Naar bestand", + "asQrCode": "Als QR-code", + "scanThisQrWithNewDevice": "Scan deze QR-code met uw nieuwe apparaat om de token te importeren.", + "oneMore": "Nog een", + "done": "Klaar", + "confirmPassword": "Wachtwoord bevestigen" } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 8d5e663e5..04681aa25 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -159,6 +159,12 @@ "@systemTheme": { "description": "The systems theme." }, + "someTokensDoNotSupportPolling": "Część tokenów jest przestarzała i nie wspiera aktywnego zapytania dla autentykacji przez wiadomość push.", + "@someTokensDoNotSupportPolling": { + "description": "Tells the user, that the following tokens do not support polling.", + "type": "text", + "placeholders": {} + }, "enablePolling": "Włącz autentykację przez wiadomość push.", "@enablePolling": { "description": "Name of the setting switch that enables polling." @@ -530,6 +536,7 @@ "invalidQrScan": "Zeskanowany kod QR nie jest prawidłową kopią zapasową {appName}.", "invalidQrFile": "Wybrany plik nie zawiera prawidłowego kodu QR z {appName}.", "invalidLink": "Wprowadzony link nie jest prawidłowym tokenem {appName} lub nie jest obsługiwany.", + "importFailedToken": "{count, plural, zero{Nie udało się zaimportować tokenu.} one{Nie udało się zaimportować tokena.} other{Nie udało się zaimportować {count} tokenów.}}", "importExistingToken": "{count, plural, zero{Nie znaleziono tokena, który już znajduje się w aplikacji.} one{Znaleziono token, który już istnieje w aplikacji.} other{Znaleziono {count} tokenów, które już znajdują się w aplikacji.}}", "importConflictToken": "{count, plural, zero{Nie ma konfliktu z tokenami, które już istnieją.} one{Istnieje konflikt z tokenami, które już istnieją.} other{Istnieje konflikt z tokenami, które już istnieją.}}", "importNewToken": "{count, plural, zero{Nie znaleziono nowego tokena.} one{Znaleziono nowy token, który zostanie zaimportowany.} other{Znaleziono nowy token {count}, który zostanie zaimportowany.}}", @@ -539,6 +546,9 @@ "importHintAegisLink": "Wprowadź link otrzymany podczas przesyłania wpisów z Aegis.", "importHintGoogleQrScan": "Zeskanuj kod QR otrzymany podczas eksportowania kont z Google Authenticator", "importHintGoogleQrFile": "Wybierz plik obrazu z kodem QR otrzymanym podczas eksportowania kont z Google Authenticator.\n!! Należy pamiętać, że zapisywanie kodu QR na urządzeniu nie jest bezpieczne, ponieważ tokeny nie są szyfrowane !!", + "importHintAuthenticatorProFile": "Aby utworzyć kopię zapasową aplikacji Authenticator Pro, przejdź do ustawień i dotknij \"Automatyczna kopia zapasowa\". Wybierz lokalizację przechowywania i ustaw hasło. Następnie naciśnij \"Utwórz teraz kopię zapasową\", aby wyeksportować tokeny.", + "importHintFreeOtpPlusQrScan": "Zeskanuj kod QR otrzymany po naciśnięciu trzech kropek na kafelku tokena i wybierz \"Udostępnij kod QR\".", + "importHintFreeOtpPlusFile": "Aby utworzyć kopię zapasową aplikacji FreeOTP+, dotknij trzech kropek w prawym górnym rogu i wybierz \"Eksportuj\". Można wybrać format JSON lub URI. Zalecamy usunięcie kopii zapasowej po jej zaimportowaniu, ponieważ nie jest ona szyfrowana.", "qrFileDecodeError": "Nie można było zdekodować kodu QR z wybranego obrazu, zamiast tego użyj skanera kodów QR.", "tokenLink": "TokenLink", "feedback": "Informacje zwrotne", @@ -569,5 +579,55 @@ "example": "GitHub" } } - } + }, + "errorUnlinkingPushToken": "Nie udało się odłączyć tokenu push {label}.", + "@errorUnlinkingPushToken": { + "description": "Error message when unlinking a push token failed.", + "placeholders": { + "label": { + "example": "PUSH1234A" + } + } + }, + "pleaseSyncManuallyWhenNetworkIsAvailable": "Zsynchronizuj tokeny push ręcznie za pomocą ustawień, gdy dostępne jest połączenie sieciowe", + "pushTokens": "Tokeny push", + "continueButton": "Kontynuuj", + "addTokenManually": "Dodaj token ręcznie", + "addFolder": "Dodaj folder", + "searchTokens": "Wyszukiwanie tokenów", + "closeSearchTokens": "Zamknij wyszukiwanie", + "increaseCounter": "Zwiększ licznik", + "copyOTPToClipboard": "Kopiowanie OTP do schowka", + "licenses": "Licencja", + "optionalMessage": "Opcjonalna wiadomość", + "confirmation": "potwierdzenie", + "askLogSendedDescription": "Czy wysłałeś dziennik i czy chcesz go teraz wyczyścić?", + "algorithmUnsupported": "Algorytm {algorithm} nie jest obsługiwany", + "@algorithmUnsupported": { + "placeholders": { + "algorithm": { + "example": "MD5" + } + } + }, + "thisAppIsOpenSource": "Ta aplikacja jest open source\nOdwiedź nas na GitHub", + "invalidArgument": "{argument} nie jest prawidłową wartością dla {type}", + "importExportTokens": "Importuj/Eksportuj tokeny", + "exportNonPrivacyIDEATokens": "Eksportuj tokeny niebędące privacyIDEA", + "selectTokensToExport": "{count, plural, zero{} one{Wybierz token do wyeksportowania} other{Wybierz tokeny do wyeksportowania}}", + "noTokensToExport": "Brak tokenów do wyeksportowania", + "exportAllTokens": "Eksportuj wszystkie tokeny", + "export": "Eksportuj", + "exportingTokens": "Eksportowanie tokenów...", + "exportTokens": "Eksportuj tokeny", + "enterPasswordToEncrypt": "Wprowadź hasło, aby zaszyfrować tokeny. To hasło będzie wymagane do importu tokenów.", + "exportLockedTokenReason": "Proszę uwierzytelnić się, aby wyeksportować zablokowane tokeny.", + "fileSavedToDownloadsFolder": "Plik zapisano w folderze Pobrane", + "errorSavingFile": "Błąd podczas zapisywania pliku", + "toFile": "Do pliku", + "asQrCode": "Jako kod QR", + "scanThisQrWithNewDevice": "Zeskanuj ten kod QR za pomocą nowego urządzenia, aby zaimportować token.", + "oneMore": "Jeszcze jeden", + "done": "Gotowe", + "confirmPassword": "Potwierdź hasło" } \ No newline at end of file diff --git a/lib/mains/main_customizer.dart b/lib/mains/main_customizer.dart index 27d9e59cc..f11979325 100644 --- a/lib/mains/main_customizer.dart +++ b/lib/mains/main_customizer.dart @@ -19,8 +19,6 @@ limitations under the License. */ -import 'dart:developer'; - import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -31,7 +29,6 @@ import 'package:privacyidea_authenticator/utils/riverpod_providers.dart'; import 'package:privacyidea_authenticator/views/add_token_manually_view/add_token_manually_view.dart'; import 'package:privacyidea_authenticator/views/license_view/license_view.dart'; import 'package:privacyidea_authenticator/views/main_view/main_view.dart'; -import 'package:privacyidea_authenticator/views/onboarding_view/onboarding_view.dart'; import 'package:privacyidea_authenticator/views/qr_scanner_view/qr_scanner_view.dart'; import 'package:privacyidea_authenticator/views/settings_view/settings_view.dart'; import 'package:privacyidea_authenticator/views/splash_screen/splash_screen.dart'; @@ -60,8 +57,6 @@ class CustomizationAuthenticator extends ConsumerWidget { final state = ref.watch(settingsProvider); final locale = state.currentLocale; final applicationCustomizer = ref.watch(applicationCustomizerProvider); - log('applicationCustomizer primaryColor: ${applicationCustomizer.lightTheme.primaryColor}'); - log('applicationCustomizer primaryColor gen: ${applicationCustomizer.generateLightTheme().primaryColor}'); return LayoutBuilder( builder: (context, constraints) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -83,9 +78,6 @@ class CustomizationAuthenticator extends ConsumerWidget { SplashScreen.routeName: (context) => SplashScreen( customization: applicationCustomizer, ), - OnboardingView.routeName: (context) => OnboardingView( - appName: applicationCustomizer.appName, - ), MainView.routeName: (context) => MainView( appIcon: applicationCustomizer.appIcon, appName: applicationCustomizer.appName, diff --git a/lib/mains/main_netknights.dart b/lib/mains/main_netknights.dart index 2df0d3060..82661d394 100644 --- a/lib/mains/main_netknights.dart +++ b/lib/mains/main_netknights.dart @@ -18,11 +18,12 @@ See the License for the specific language governing permissions and limitations under the License. */ + import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; -import 'package:privacyidea_authenticator/utils/app_customizer.dart'; +import 'package:privacyidea_authenticator/utils/customization/application_customization.dart'; import 'package:privacyidea_authenticator/utils/globals.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; import 'package:privacyidea_authenticator/utils/riverpod_providers.dart'; @@ -30,7 +31,6 @@ import 'package:privacyidea_authenticator/views/add_token_manually_view/add_toke import 'package:privacyidea_authenticator/views/import_tokens_view/import_tokens_view.dart'; import 'package:privacyidea_authenticator/views/license_view/license_view.dart'; import 'package:privacyidea_authenticator/views/main_view/main_view.dart'; -import 'package:privacyidea_authenticator/views/onboarding_view/onboarding_view.dart'; import 'package:privacyidea_authenticator/views/push_token_view/push_tokens_view.dart'; import 'package:privacyidea_authenticator/views/qr_scanner_view/qr_scanner_view.dart'; import 'package:privacyidea_authenticator/views/settings_view/settings_view.dart'; @@ -48,17 +48,20 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await HomeWidgetUtils().registerInteractivityCallback(homeWidgetBackgroundCallback); await HomeWidgetUtils().setAppGroupId(appGroupId); - runApp(AppWrapper(child: PrivacyIDEAAuthenticator(customization: ApplicationCustomization.defaultCustomization))); + runApp(AppWrapper(child: PrivacyIDEAAuthenticator(ApplicationCustomization.defaultCustomization))); }); } class PrivacyIDEAAuthenticator extends ConsumerWidget { static ApplicationCustomization? currentCustomization; final ApplicationCustomization _customization; - PrivacyIDEAAuthenticator({required ApplicationCustomization customization, super.key}) : _customization = customization { - // ignore: prefer_initializing_formals + + factory PrivacyIDEAAuthenticator(ApplicationCustomization customization, {Key? key}) { PrivacyIDEAAuthenticator.currentCustomization = customization; + return PrivacyIDEAAuthenticator._(customization: customization, key: key); } + const PrivacyIDEAAuthenticator._({required ApplicationCustomization customization, super.key}) : _customization = customization; + @override Widget build(BuildContext context, WidgetRef ref) { globalRef = ref; @@ -97,14 +100,9 @@ class PrivacyIDEAAuthenticator extends ConsumerWidget { appName: _customization.appName, disablePatchNotes: _customization.disabledFeatures.contains(AppFeature.patchNotes), ), - OnboardingView.routeName: (context) => OnboardingView( - appName: _customization.appName, - ), PushTokensView.routeName: (context) => const PushTokensView(), SettingsView.routeName: (context) => const SettingsView(), - SplashScreen.routeName: (context) => SplashScreen( - customization: _customization, - ), + SplashScreen.routeName: (context) => SplashScreen(customization: _customization), QRScannerView.routeName: (context) => const QRScannerView(), }, ); diff --git a/lib/model/encryption/aes_encrypted.dart b/lib/model/encryption/aes_encrypted.dart index 87bf6c6d0..4e8ca823c 100644 --- a/lib/model/encryption/aes_encrypted.dart +++ b/lib/model/encryption/aes_encrypted.dart @@ -1,50 +1,280 @@ import 'dart:convert'; +import 'dart:math'; import 'dart:typed_data'; import 'package:cryptography/cryptography.dart'; -import 'package:encrypt/encrypt.dart'; +import 'package:cryptography/dart.dart'; +import 'package:flutter/foundation.dart'; -class AESEncrypted { - final Uint8List data; +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; + // Key derivation + final KdfAlgorithm kdf; final Uint8List salt; + + // Encryption + final Uint8List data; final Uint8List iv; - final String ivBase64; - final String padding; - final Hmac macAlgorithm; - final int iterations; - // final int keyBitLengh; - final AESMode aesMode; - - String? decryptedString; - - AESEncrypted( - {required this.data, required this.salt, required this.iv, String? padding, Hmac? macAlgorithm, int? iterations, int? keyBitLengh, AESMode? aesMode}) - : //dataBytes = base64Decode(data), - // saltBytes = base64Decode(salt), - // ivBytes = base64Decode(iv), - padding = padding ?? 'PKCS7', - ivBase64 = base64Encode(iv), - macAlgorithm = macAlgorithm ?? Hmac.sha256(), - iterations = iterations ?? 10000, - // keyBitLengh = keyBitLengh ?? 128,// - aesMode = aesMode ?? AESMode.gcm; - - Future decrypt(String password) async { - final keyGenerator = Pbkdf2(macAlgorithm: macAlgorithm, iterations: iterations, bits: salt.length * 8); - - final SecretKey secretKey = await keyGenerator.deriveKeyFromPassword(password: password, nonce: salt); - final Uint8List keyBytes = Uint8List.fromList(await secretKey.extractBytes()); - final Key key = Key(keyBytes); - - final encrypter = Encrypter(AES(key, mode: aesMode, padding: padding)); - final iv = IV.fromBase64(ivBase64); - final String decryptedString; - try { - final decrypted = encrypter.decryptBytes(Encrypted(data), iv: iv); - decryptedString = utf8.decode(decrypted); - } catch (e) { - throw Exception('Wrong password or corrupted data'); + final Mac? mac; + // final String? padding; + + final Cipher cypher; + + const AesEncrypted._({ + required this.kdf, + required this.salt, + required this.data, + required this.iv, + this.mac, + 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 decryptedString; + 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(); + final kdf = Pbkdf2( + macAlgorithm: defaultMacAlgorithm, + iterations: defaultIterations, + bits: defaultBits, + ); + 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); + return AesEncrypted._( + cypher: cypher, + kdf: kdf, + salt: salt, + iv: iv, + data: encryptedData, + mac: secretBox.mac, + ); + } + + Future decrypt(String password) async { + final SecretKey secretKey = await _deriveKey(password); + final SecretBox secretBox = SecretBox(data, nonce: iv, mac: mac ?? Mac.empty); + final decrypted = await cypher.decrypt(secretBox, secretKey: secretKey); + return Uint8List.fromList(decrypted); + } + + Future decryptToString(String password) async { + final decrypted = await decrypt(password); + return utf8.decode(decrypted); + } + + Future _deriveKey(String password) async { + return await kdf.deriveKeyFromPassword(password: password, nonce: salt); + } + + static AesEncrypted fromJson(Map json) { + return AesEncrypted( + data: base64Decode(json['data']), + salt: base64Decode(json['salt']), + iv: base64Decode(json['iv']), + mac: Mac(base64Decode(json['mac'])), + kdf: KdfAlgorithmX.fromJson(json['kdf']), + cypher: CipherX.fromJson(json['cypher']), + ); + } + + 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'), + }; + } + + 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 new file mode 100644 index 000000000..ffc585675 --- /dev/null +++ b/lib/model/encryption/token_encryption.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; + +import '../../processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart'; +import '../tokens/token.dart'; +import 'aes_encrypted.dart'; + +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)).toJson(); + return jsonEncode(encrypted); + } + + static Future> decrypt({required String encryptedTokens, required String password}) async { + 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}) { + final tokenJson = token.toJson(); + final encoded = json.encode(tokenJson); + final bytes = utf8.encode(encoded); + final base64 = base64Url.encode(bytes); + final uri = Uri.parse('${PrivacyIDEAAuthenticatorQrProcessor.scheme}://${PrivacyIDEAAuthenticatorQrProcessor.host}?data=$base64'); + return uri; + } + + static Token fromQrCodeUri(Uri uri) { + final base64String = uri.queryParameters['data']; + final bytes = base64Url.decode(base64String!); + final jsonString = utf8.decode(bytes); + final tokenJson = json.decode(jsonString) as Map; + return Token.fromJson(tokenJson); + } +} diff --git a/lib/model/encryption/uint_8_buffer.dart b/lib/model/encryption/uint_8_buffer.dart new file mode 100644 index 000000000..804717f31 --- /dev/null +++ b/lib/model/encryption/uint_8_buffer.dart @@ -0,0 +1,50 @@ +import 'dart:typed_data'; + +class Uint8Buffer { + int currentPos = 0; + Uint8List data; + Uint8Buffer({required this.data}); + factory Uint8Buffer.fromList(List list) { + return Uint8Buffer(data: Uint8List.fromList(list)); + } + + /// Writes [bytes] to the buffer + void writeBytes(Uint8List bytes) { + data = Uint8List.fromList([...data, ...bytes]); + } + + /// Reads [length] bytes from the current position + /// and moves the position forward + /// If [length] is out of bounds, it will return the rest of the buffer + Uint8List readBytes(int 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] + /// 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 41ffe4faf..fa09abb40 100644 --- a/lib/model/enums/algorithms.dart +++ b/lib/model/enums/algorithms.dart @@ -1,17 +1,7 @@ // ignore_for_file: constant_identifier_names - -import 'package:otp/otp.dart' as otp_library; - +// Do not rename or remove values, they are used for serialization. Only add new values. enum Algorithms { SHA1, SHA256, SHA512, } - -extension AlgorithmsExtension on Algorithms { - otp_library.Algorithm get otpLibraryAlgorithm => switch (this) { - Algorithms.SHA1 => otp_library.Algorithm.SHA1, - Algorithms.SHA256 => otp_library.Algorithm.SHA256, - Algorithms.SHA512 => otp_library.Algorithm.SHA512, - }; -} diff --git a/lib/model/enums/app_feature.dart b/lib/model/enums/app_feature.dart index 2e39e73a8..11c4e6f17 100644 --- a/lib/model/enums/app_feature.dart +++ b/lib/model/enums/app_feature.dart @@ -1,14 +1,4 @@ +// 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 ArgumentError('Invalid feature string: $featureString'), - }; -} 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/introduction.dart b/lib/model/enums/introduction.dart index 5672bfd63..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 IntroductionExtension on Introduction { - bool isConditionFulfilled(WidgetRef ref, IntroductionState state) => switch (this) { - Introduction.introductionScreen => state.isUncompleted(Introduction.introductionScreen), - Introduction.scanQrCode => state.isCompleted(Introduction.introductionScreen) && 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 b5fa16544..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 PatchNoteTypeExtension 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 a3598017b..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 PushTokenRollOutStateExtension 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 99cc7ca8d..c24d1a34a 100644 --- a/lib/model/enums/token_origin_source_type.dart +++ b/lib/model/enums/token_origin_source_type.dart @@ -1,19 +1,11 @@ -import '../../mains/main_netknights.dart'; -import '../token_origin.dart'; -import '../tokens/token.dart'; - +// Do not rename or remove values, they are used for serialization. Only add new values. enum TokenOriginSourceType { backupFile, qrScan, qrFile, + qrScanImport, link, + linkImport, manually, unknown, } - -extension TokenSourceTypeExtension on TokenOriginSourceType { - TokenOriginData toTokenOrigin({String data = '', String? appName}) => - TokenOriginData(source: this, data: data, appName: appName ?? PrivacyIDEAAuthenticator.currentCustomization?.appName); - - Token addOriginToToken({required Token token, required String data, String? appName}) => token.copyWith(origin: toTokenOrigin(data: data, appName: appName)); -} diff --git a/lib/model/enums/token_types.dart b/lib/model/enums/token_types.dart index 0b3ab49ed..a4756ce86 100644 --- a/lib/model/enums/token_types.dart +++ b/lib/model/enums/token_types.dart @@ -1,8 +1,9 @@ // 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, PIPUSH, DAYPASSWORD, + STEAM, } diff --git a/lib/model/extensions/color_extension.dart b/lib/model/extensions/color_extension.dart new file mode 100644 index 000000000..0c3d53011 --- /dev/null +++ b/lib/model/extensions/color_extension.dart @@ -0,0 +1,12 @@ +import 'dart:ui'; + +extension ColorExtension on Color { + 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 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 new file mode 100644 index 000000000..2dcace0cd --- /dev/null +++ b/lib/model/extensions/int_extension.dart @@ -0,0 +1,28 @@ +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); + + for (var index = byteArray.length - 1; index >= 0; index--) { + final byte = long & 0xff; + byteArray[index] = byte; + long = (long - byte) ~/ 256; + } + return byteArray; + } + + Iterable get digits sync* { + var number = this; + do { + yield number.remainder(10); + number ~/= 10; + } while (number != 0); + } + + num pow(num exponent) => math.pow(this, exponent); +} diff --git a/lib/model/extensions/sortable_list.dart b/lib/model/extensions/sortable_list.dart new file mode 100644 index 000000000..1c32c896b --- /dev/null +++ b/lib/model/extensions/sortable_list.dart @@ -0,0 +1,85 @@ +import '../mixins/sortable_mixin.dart'; + +extension SortableList on List { + List get sorted { + var list = List.from(this); + list.sort((a, b) => a.compareTo(b)); + print('-----------------------------------'); + list.forEach((element) { + print('sorted: ${element.runtimeType} Sortindex: ${element.sortIndex}'); + }); + print('-----------------------------------'); + return list; + } + + List fillNullIndices() { + var list = List.from(this); + var highestIndex = fold(0, (previousValue, element) { + if (element.sortIndex == null) return previousValue; + if (previousValue > element.sortIndex!) return previousValue; + return element.sortIndex!; + }); + for (var i = 0; i < list.length; i++) { + if (list[i].sortIndex == null) { + highestIndex++; + list[i] = list[i].copyWith(sortIndex: highestIndex) as T; + } + } + print('-----------------------------------'); + list.forEach((element) { + print('fillNullIndices: ${element.runtimeType} Sortindex: ${element.sortIndex}'); + }); + print('-----------------------------------'); + return list; + } + + /// Moves the [movedItem] to the position after [moveAfter] or before [moveBefore]. + /// If both [moveAfter] and [moveBefore] are null, the [movedItem] will be moved to the end of the list. + /// If both is set, [moveBefore] will be prioritized. + List moveBetween({T? moveAfter, required T movedItem, T? moveBefore}) { + var list = List.from(this).sorted.withCurrentSortIndexSet(); + final success = list.remove(movedItem); + if (!success) return list; + final newIndex = moveBefore != null + ? list.indexOf(moveBefore) + : moveAfter != null && list.contains(moveAfter) + ? list.indexOf(moveAfter) + 1 + : list.length; + list.insert(newIndex, movedItem); + list = list.withCurrentSortIndexSet(); + print('-----------------------------------'); + list.forEach((element) { + print('moveBetween: ${element.runtimeType} Sortindex: ${element.sortIndex}'); + }); + print('-----------------------------------'); + return list; + } + + List moveAllBetween({T? moveAfter, required List movedItems, T? moveBefore}) { + var list = List.from(this).sorted.withCurrentSortIndexSet(); + List removedItems = []; + for (final movedItem in movedItems) { + final success = list.remove(movedItem); + if (success) removedItems.add(movedItem); + } + if (removedItems.isEmpty) return list; + final newIndex = moveBefore != null + ? list.indexOf(moveBefore) + : moveAfter != null && list.contains(moveAfter) + ? list.indexOf(moveAfter) + 1 + : list.length; + list.insertAll(newIndex, removedItems); + list = list.withCurrentSortIndexSet(); + return list; + } + + List withCurrentSortIndexSet() { + final list = List.from(this); + for (var i = 0; i < list.length; i++) { + if (list[i].sortIndex != i) { + list[i] = list[i].copyWith(sortIndex: i) as T; + } + } + return list; + } +} diff --git a/lib/model/mixins/sortable_mixin.dart b/lib/model/mixins/sortable_mixin.dart index 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 new file mode 100644 index 000000000..ffe5ca295 --- /dev/null +++ b/lib/model/processor_result.dart @@ -0,0 +1,25 @@ +abstract class ProcessorResult { + const ProcessorResult(); + factory ProcessorResult.success(T data) => ProcessorResultSuccess(data); + factory ProcessorResult.failed(String errorMessage) => ProcessorResultFailed(errorMessage); + ProcessorResultSuccess? get asSuccess => this is ProcessorResultSuccess ? this as ProcessorResultSuccess : null; + ProcessorResultFailed? get asFailed => this is ProcessorResultFailed ? this as ProcessorResultFailed : null; +} + +class ProcessorResultSuccess extends ProcessorResult { + final T resultData; + const ProcessorResultSuccess(this.resultData); + + @override + String toString() { + return 'ProcessorResultSuccess(data: $resultData)'; + } +} + +class ProcessorResultFailed extends ProcessorResult { + final String message; + const ProcessorResultFailed(this.message); + + @override + String toString() => '$runtimeType(message: $message)'; +} diff --git a/lib/model/push_request.dart b/lib/model/push_request.dart index cf361e3e5..6c7fa0dee 100644 --- a/lib/model/push_request.dart +++ b/lib/model/push_request.dart @@ -1,7 +1,14 @@ +import 'dart:convert'; + +import 'package:base32/base32.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:pi_authenticator_legacy/pi_authenticator_legacy.dart'; import '../utils/identifiers.dart'; import '../utils/logger.dart'; +import '../utils/riverpod_providers.dart'; +import '../utils/rsa_utils.dart'; +import 'tokens/push_token.dart'; part 'push_request.g.dart'; @@ -26,11 +33,10 @@ class PushRequest { required this.sslVerify, required this.id, required this.expirationDate, - String? serial, - String? signature, + this.serial = '', + this.signature = '', this.accepted, - }) : serial = serial ?? '', - signature = signature ?? ''; + }); PushRequest copyWith({ String? title, @@ -59,10 +65,10 @@ 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 => id.hashCode; + int get hashCode => Object.hash(runtimeType, id); @override String toString() { @@ -122,4 +128,34 @@ class PushRequest { throw ArgumentError('Push request signature is ${data[PUSH_REQUEST_SIGNATURE].runtimeType}. Expected String.'); } } + + Future verifySignature(PushToken token, {LegacyUtils legacyUtils = const LegacyUtils(), RsaUtils rsaUtils = const RsaUtils()}) async { + Logger.info('Adding push request to token', name: 'push_request_notifier.dart#newRequest'); + String signedData = '$nonce|' + '$uri|' + '$serial|' + '$question|' + '$title|' + '${sslVerify ? '1' : '0'}'; + + // Re-add url and sslverify to android legacy tokens: + if (token.url == null) { + globalRef?.read(tokenProvider.notifier).updateToken(token, (p0) => p0.copyWith(url: uri, sslVerify: sslVerify)); + } + + bool isVerified = token.privateTokenKey == null + ? await legacyUtils.verify(token.serial, signedData, signature) + : rsaUtils.verifyRSASignature(token.rsaPublicServerKey!, utf8.encode(signedData), base32.decode(signature)); + + if (!isVerified) { + Logger.warning( + 'Validating incoming message failed.', + name: 'token_notifier.dart#addPushRequestToToken', + error: 'Signature does not match signed data.', + ); + return false; + } + Logger.info('Validating incoming message was successful.', name: 'token_notifier.dart#addPushRequestToToken'); + return true; + } } diff --git a/lib/model/push_request.g.dart b/lib/model/push_request.g.dart index 0ba01979b..a9ecce588 100644 --- a/lib/model/push_request.g.dart +++ b/lib/model/push_request.g.dart @@ -14,8 +14,8 @@ PushRequest _$PushRequestFromJson(Map json) => PushRequest( sslVerify: json['sslVerify'] as bool, id: json['id'] as int, expirationDate: DateTime.parse(json['expirationDate'] as String), - serial: json['serial'] as String?, - signature: json['signature'] as String?, + serial: json['serial'] as String? ?? '', + signature: json['signature'] as String? ?? '', accepted: json['accepted'] as bool?, ); diff --git a/lib/model/push_request_queue.dart b/lib/model/push_request_queue.dart deleted file mode 100644 index 95e92452f..000000000 --- a/lib/model/push_request_queue.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:json_annotation/json_annotation.dart'; - -import 'push_request.dart'; - -part 'push_request_queue.g.dart'; - -@JsonSerializable() -class PushRequestQueue { - PushRequestQueue(); - - List? _list; - - // The get and set methods are needed for serialization. - List get list { - _list ??= []; - return _list!; - } - - set list(List l) { - if (_list != null) { - throw ArgumentError('Initializing [list] in [PushRequestQueue] is only allowed once.'); - } - - _list = l; - } - - int get length => list.length; - - void forEach(void Function(PushRequest request) f) => list.forEach((f)); - - void removeWhere(bool Function(PushRequest request) f) => list.removeWhere(f); - - Iterable where(bool Function(PushRequest request) f) => list.where(f); - - bool any(bool Function(PushRequest element) f) => list.any(f); - - void remove(PushRequest request) { - final prToRemove = list.firstWhereOrNull((element) => element.id == request.id); - if (prToRemove == null) return; - list.remove(prToRemove); - } - - bool get isEmpty => list.isEmpty; - - bool get isNotEmpty => list.isNotEmpty; - - bool contains(PushRequest r) => list.contains(r); - - void add(PushRequest pushRequest) => list.add(pushRequest); - - PushRequest? peek() => list.isNotEmpty ? list.first : null; - - PushRequest? tryPop() => list.isNotEmpty ? list.removeAt(0) : null; - - @override - String toString() { - return 'PushRequestQueue{_list: $list}'; - } - - @override - bool operator ==(Object other) => identical(this, other) || other is PushRequestQueue && runtimeType == other.runtimeType && _listsAreEqual(list, other.list); - - 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; - } - - @override - int get hashCode => list.hashCode; - - factory PushRequestQueue.fromJson(Map json) => _$PushRequestQueueFromJson(json); - - Map toJson() => _$PushRequestQueueToJson(this); -} diff --git a/lib/model/push_request_queue.g.dart b/lib/model/push_request_queue.g.dart deleted file mode 100644 index 0285d802f..000000000 --- a/lib/model/push_request_queue.g.dart +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'push_request_queue.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -PushRequestQueue _$PushRequestQueueFromJson(Map json) => - PushRequestQueue() - ..list = (json['list'] as List) - .map((e) => PushRequest.fromJson(e as Map)) - .toList(); - -Map _$PushRequestQueueToJson(PushRequestQueue instance) => - { - 'list': instance.list, - }; 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..016254314 100644 --- a/lib/model/states/introduction_state.dart +++ b/lib/model/states/introduction_state.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:json_annotation/json_annotation.dart'; import '../enums/introduction.dart'; +import '../extensions/enums/introduction_extension.dart'; part 'introduction_state.g.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/push_request_state.dart b/lib/model/states/push_request_state.dart new file mode 100644 index 000000000..51fa8b8b4 --- /dev/null +++ b/lib/model/states/push_request_state.dart @@ -0,0 +1,86 @@ +import 'package:flutter/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../utils/custom_int_buffer.dart'; +import '../push_request.dart'; + +part 'push_request_state.g.dart'; + +@JsonSerializable() +class PushRequestState { + final List pushRequests; + final CustomIntBuffer knownPushRequests; + const PushRequestState({required this.pushRequests, required this.knownPushRequests}); + + bool knowsRequestId(int id) => knownPushRequests.contains(id); + bool knowsRequest(PushRequest pushRequest) => knowsRequestId(pushRequest.id); + + /// Adds the given push request to a new state and returns it. + PushRequestState withRequest(PushRequest pushRequest) => PushRequestState( + pushRequests: pushRequests.toList()..add(pushRequest), + knownPushRequests: knownPushRequests.put(pushRequest.id), + ); + + PushRequestState withoutRequest(PushRequest pushRequest) => PushRequestState( + pushRequests: pushRequests.toList()..remove(pushRequest), + knownPushRequests: knownPushRequests.copyWith(), + ); + + PushRequestState addOrReplace(PushRequest pushRequest) { + final requests = pushRequests.toList(); + final knowenIds = knownPushRequests.toList(); + if (requests.contains(pushRequest)) { + return PushRequestState( + pushRequests: requests + ..remove(pushRequest) + ..add(pushRequest), + knownPushRequests: knownPushRequests, + ); + } + knowenIds.add(pushRequest.id); + return PushRequestState( + pushRequests: requests..add(pushRequest), + knownPushRequests: CustomIntBuffer(list: knowenIds), + ); + } + + (PushRequestState, bool) replaceRequest(PushRequest pushRequest) { + final newRequests = pushRequests.toList(); + final index = newRequests.indexWhere((element) => element.id == pushRequest.id); + if (index == -1) { + return (this, false); + } + newRequests[index] = pushRequest; + return (PushRequestState(pushRequests: newRequests, knownPushRequests: knownPushRequests), true); + } + + PushRequest? currentOf(PushRequest pushRequest) => pushRequests.firstWhere((element) => element.id == pushRequest.id); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PushRequestState && listEquals(other.pushRequests, pushRequests) && other.knownPushRequests == knownPushRequests; + } + + @override + int get hashCode => pushRequests.hashCode * 31 + knownPushRequests.hashCode; + + @override + String toString() => 'PushRequestState(pushRequests: $pushRequests, knownPushRequests: $knownPushRequests)'; + + /* + ////////////////////////////////////////////////// + /////////////// Json Serialization /////////////// + ////////////////////////////////////////////////// + */ + Map toJson() => { + 'pushRequests': pushRequests.map((e) => e.toJson()).toList(), + 'knownPushRequests': knownPushRequests.toJson(), + }; + + factory PushRequestState.fromJson(Map json) => PushRequestState( + pushRequests: (json['pushRequests'] as List).map((e) => PushRequest.fromJson(e)).toList(), + knownPushRequests: CustomIntBuffer.fromJson(json['knownPushRequests']), + ); +} diff --git a/lib/model/states/push_request_state.g.dart b/lib/model/states/push_request_state.g.dart new file mode 100644 index 000000000..33064ac83 --- /dev/null +++ b/lib/model/states/push_request_state.g.dart @@ -0,0 +1,22 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'push_request_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PushRequestState _$PushRequestStateFromJson(Map json) => + PushRequestState( + pushRequests: (json['pushRequests'] as List) + .map((e) => PushRequest.fromJson(e as Map)) + .toList(), + knownPushRequests: CustomIntBuffer.fromJson( + json['knownPushRequests'] as Map), + ); + +Map _$PushRequestStateToJson(PushRequestState instance) => + { + 'pushRequests': instance.pushRequests, + 'knownPushRequests': instance.knownPushRequests, + }; diff --git a/lib/model/states/settings_state.dart b/lib/model/states/settings_state.dart index 5fadc4829..1d363e4a4 100644 --- a/lib/model/states/settings_state.dart +++ b/lib/model/states/settings_state.dart @@ -2,10 +2,10 @@ import 'dart:io'; import 'dart:ui'; import 'package:flutter/foundation.dart'; -import '../../utils/version.dart'; import '../../l10n/app_localizations.dart'; import '../../utils/identifiers.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 2295f7a74..9f3d2199f 100644 --- a/lib/model/states/token_state.dart +++ b/lib/model/states/token_state.dart @@ -23,35 +23,26 @@ class TokenState { List get pushTokensToRollOut => pushTokens.where((element) => !element.isRolledOut && element.rolloutState == PushTokenRollOutState.rolloutNotStarted).toList(); - TokenState({List tokens = const [], List? lastlyUpdatedTokens}) + TokenState({required List tokens, List? lastlyUpdatedTokens}) : tokens = List.from(tokens), - lastlyUpdatedTokens = lastlyUpdatedTokens ?? List.from(tokens) { - _sort(this.tokens); - } - TokenState repaceList({List? tokens}) => TokenState(tokens: tokens ?? this.tokens); + lastlyUpdatedTokens = List.from(lastlyUpdatedTokens ?? tokens); - Map tokensWithSameSectet(List tokens) { - final tokensWithSameSectet = {}; - final stateTokens = this.tokens; - 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]; - } + List get nonPiTokens => tokens.where((token) => token.isPrivacyIdeaToken == false).toList(); - return tokensWithSameSectet; - } + PushToken? getTokenBySerial(String serial) => pushTokens.firstWhereOrNull((element) => element.serial == serial); - static void _sort(List tokens) { - tokens.sort((a, b) => (a.sortIndex ?? double.infinity).compareTo(b.sortIndex ?? double.infinity)); + /// Maps the given tokens to the tokens that are already in the state + /// It ignores the id that is usually used to identify the token + /// Instead it uses the non-changeable values of the token to identify it + /// Like the secret and hash algorithm for OTP tokens, or the serial and public server key for push tokens + Map getSameTokens(List tokens) { + final sameTokensMap = {}; + final stateTokens = this.tokens; + + for (var token in tokens) { + sameTokensMap[token] = stateTokens.firstWhereOrNull((element) => element.isSameTokenAs(token)); + } + return sameTokensMap; } T? currentOf(T token) => tokens.firstWhereOrNull((element) => element.id == token.id) as T?; @@ -80,7 +71,7 @@ class TokenState { TokenState withoutTokens(List tokens) { final newTokens = List.from(this.tokens); newTokens.removeWhere((element) => tokens.any((token) => token.id == element.id)); - return TokenState(tokens: newTokens, lastlyUpdatedTokens: tokens); + return TokenState(tokens: newTokens, lastlyUpdatedTokens: const []); } // Add a token if it does not exist yet @@ -98,66 +89,71 @@ class TokenState { // Replace the token if it does exist // Do nothing if it does not exist - TokenState replaceToken(Token token) { - final newTokens = List.from(tokens); + (TokenState, bool) replaceToken(Token token) { + final newTokens = tokens.toList(); final index = newTokens.indexWhere((element) => element.id == token.id); if (index == -1) { Logger.warning('Tried to replace a token that does not exist.', name: 'token_state.dart#replaceToken'); - return this; + return (this, false); } newTokens[index] = token; - return TokenState(tokens: newTokens, lastlyUpdatedTokens: [token]); + return (TokenState(tokens: newTokens, lastlyUpdatedTokens: [token]), true); } // replace all tokens where the id is the same // if the id is none, add it to the list - TokenState addOrReplaceTokens(List tokens) { + TokenState addOrReplaceTokens(List tokens) { final newTokens = List.from(this.tokens); + final updatedTokens = []; for (var token in tokens) { final index = newTokens.indexWhere((element) => element.id == token.id); if (index == -1) { newTokens.add(token); + updatedTokens.add(token); continue; } newTokens[index] = token; + updatedTokens.add(token); } - return TokenState(tokens: newTokens, lastlyUpdatedTokens: tokens); + return TokenState(tokens: newTokens, lastlyUpdatedTokens: updatedTokens); } // Replace the tokens if it does exist // Do nothing if it does not exist - TokenState replaceTokens(List tokens) { + (TokenState, List) replaceTokens(List tokens) { final newTokens = List.from(this.tokens); - final lastlyUpdatedTokens = []; + final updatedTokens = []; + final failedToReplace = []; for (var token in tokens) { final index = newTokens.indexWhere((element) => element.id == token.id); if (index == -1) { Logger.warning('Tried to replace a token that does not exist.', name: 'token_state.dart#replaceToken'); + failedToReplace.add(token); continue; } newTokens[index] = token; - lastlyUpdatedTokens.add(token); } - return TokenState(tokens: newTokens, lastlyUpdatedTokens: lastlyUpdatedTokens); + 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(); - - PushToken? tokenWithPushRequest() => tokens.whereType().firstWhereOrNull((token) => token.pushRequests.isNotEmpty); } 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 new file mode 100644 index 000000000..78f931819 --- /dev/null +++ b/lib/model/token_import/token_import_origin.dart @@ -0,0 +1,13 @@ +import 'token_import_source.dart'; + +class TokenImportOrigin { + final String appName; + final String? iconPath; + final List importSources; + + const TokenImportOrigin({ + required this.appName, + required this.importSources, + this.iconPath, + }); +} 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..9e15f1b71 --- /dev/null +++ b/lib/model/token_import/token_import_source.dart @@ -0,0 +1,11 @@ +import '../../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 new file mode 100644 index 000000000..8f02a54fa --- /dev/null +++ b/lib/model/token_import/token_origin_data.dart @@ -0,0 +1,75 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../enums/token_origin_source_type.dart'; +import '../version.dart'; + +part 'token_origin_data.g.dart'; + +@JsonSerializable() +class TokenOriginData { + final TokenOriginSourceType source; + final String? appName; + final String data; + final bool? isPrivacyIdeaToken; + final DateTime? createdAt; + final Version? piServerVersion; + const TokenOriginData._({ + required this.source, + required this.data, + this.appName, + this.isPrivacyIdeaToken, + this.createdAt, + this.piServerVersion, + }); + + factory TokenOriginData({ + required TokenOriginSourceType source, + required String data, + String? appName, + bool? isPrivacyIdeaToken, + DateTime? createdAt, + Version? piServerVersion, + }) => + TokenOriginData._( + source: source, + appName: appName, + data: data, + isPrivacyIdeaToken: isPrivacyIdeaToken, + createdAt: createdAt ?? DateTime.now(), + piServerVersion: piServerVersion, + ); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is TokenOriginData && other.source == source && other.appName == appName && other.data == data; + } + + TokenOriginData copyWith({ + TokenOriginSourceType? source, + String? data, + String? appName, + bool? isPrivacyIdeaToken, + DateTime? createdAt, + Version? piServerVersion, + }) => + TokenOriginData( + source: source ?? this.source, + appName: appName ?? this.appName, + data: data ?? this.data, + isPrivacyIdeaToken: isPrivacyIdeaToken ?? this.isPrivacyIdeaToken, + createdAt: createdAt ?? this.createdAt, + piServerVersion: piServerVersion ?? this.piServerVersion, + ); + + @override + int get hashCode => Object.hashAll([source, appName, data]); + + // toString prints not data because it contains the secret + @override + String toString() => 'TokenOrigin{source: $source, app: $appName, isPrivacyIdeaToken: $isPrivacyIdeaToken, createdAt: $createdAt, data: $data'; + + factory TokenOriginData.fromJson(Map json) => _$TokenOriginDataFromJson(json); + + Map toJson() => _$TokenOriginDataToJson(this); +} diff --git a/lib/model/token_origin.g.dart b/lib/model/token_import/token_origin_data.g.dart similarity index 61% rename from lib/model/token_origin.g.dart rename to lib/model/token_import/token_origin_data.g.dart index cfc4bfa32..07d913023 100644 --- a/lib/model/token_origin.g.dart +++ b/lib/model/token_import/token_origin_data.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'token_origin.dart'; +part of 'token_origin_data.dart'; // ************************************************************************** // JsonSerializableGenerator @@ -11,6 +11,13 @@ TokenOriginData _$TokenOriginDataFromJson(Map json) => source: $enumDecode(_$TokenOriginSourceTypeEnumMap, json['source']), data: json['data'] as String, appName: json['appName'] as String?, + isPrivacyIdeaToken: json['isPrivacyIdeaToken'] as bool?, + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + piServerVersion: json['piServerVersion'] == null + ? null + : Version.fromJson(json['piServerVersion'] as Map), ); Map _$TokenOriginDataToJson(TokenOriginData instance) => @@ -18,13 +25,18 @@ Map _$TokenOriginDataToJson(TokenOriginData instance) => 'source': _$TokenOriginSourceTypeEnumMap[instance.source]!, 'appName': instance.appName, 'data': instance.data, + 'isPrivacyIdeaToken': instance.isPrivacyIdeaToken, + 'createdAt': instance.createdAt?.toIso8601String(), + 'piServerVersion': instance.piServerVersion, }; const _$TokenOriginSourceTypeEnumMap = { TokenOriginSourceType.backupFile: 'backupFile', TokenOriginSourceType.qrScan: 'qrScan', TokenOriginSourceType.qrFile: 'qrFile', + TokenOriginSourceType.qrScanImport: 'qrScanImport', TokenOriginSourceType.link: 'link', + TokenOriginSourceType.linkImport: 'linkImport', TokenOriginSourceType.manually: 'manually', TokenOriginSourceType.unknown: 'unknown', }; diff --git a/lib/model/token_import_origin.dart b/lib/model/token_import_origin.dart deleted file mode 100644 index 9ae032bb5..000000000 --- a/lib/model/token_import_origin.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../l10n/app_localizations.dart'; -import '../processors/mixins/token_import_processor.dart'; -import '../processors/scheme_processors/token_import_scheme_processors/otp_auth_migration_processor.dart'; -import '../processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart'; -import '../processors/token_import_file_processor/aegis_import_file_processor.dart'; -import '../processors/token_import_file_processor/two_fas_import_file_processor.dart'; -import 'enums/token_import_type.dart'; - -class TokenImportOrigin { - final String appName; - final String? iconPath; - final List importEntitys; - - const TokenImportOrigin({ - required this.appName, - required this.importEntitys, - this.iconPath, - }); -} - -class TokenImportEntity { - final TokenImportType type; - final TokenImportProcessor processor; - final String Function(BuildContext context) importHint; - - const TokenImportEntity({required this.processor, required this.type, required this.importHint}); -} - -class TokenImportSourceList { - static const _importSourceIconFolder = 'assets/images/import_sources/'; - static List appList = [ - TokenImportOrigin( - appName: 'Google Authenticator', - iconPath: '${_importSourceIconFolder}google_authenticator.png', - importEntitys: [ - TokenImportEntity( - processor: const OtpAuthMigrationProcessor(), - type: TokenImportType.qrScan, - importHint: (context) => AppLocalizations.of(context)!.importHintGoogleQrScan, - ), - TokenImportEntity( - processor: const OtpAuthMigrationProcessor(), - type: TokenImportType.qrFile, - importHint: (context) => AppLocalizations.of(context)!.importHintGoogleQrFile, - ), - ], - ), - TokenImportOrigin( - appName: 'Aegis Authenticator', - iconPath: '${_importSourceIconFolder}aegis_authenticator.png', - importEntitys: [ - TokenImportEntity( - processor: const AegisImportFileProcessor(), - type: TokenImportType.backupFile, - importHint: (context) => AppLocalizations.of(context)!.importHintAegisBackupFile, - ), - TokenImportEntity( - processor: const OtpAuthProcessor(), - type: TokenImportType.qrScan, - importHint: (context) => AppLocalizations.of(context)!.importHintAegisQrScan, - ), - TokenImportEntity( - processor: const OtpAuthProcessor(), - type: TokenImportType.link, - importHint: (context) => AppLocalizations.of(context)!.importHintAegisLink, - ), - ], - ), - TokenImportOrigin( - appName: '2FAS Authenticator', - iconPath: '${_importSourceIconFolder}2fas.png', - importEntitys: [ - TokenImportEntity( - processor: const TwoFasFileImportProcessor(), - type: TokenImportType.backupFile, - importHint: (context) => AppLocalizations.of(context)!.importHint2FAS), - ], - ), - ]; -} diff --git a/lib/model/token_origin.dart b/lib/model/token_origin.dart deleted file mode 100644 index 3d3038cdc..000000000 --- a/lib/model/token_origin.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -import 'enums/token_origin_source_type.dart'; - -part 'token_origin.g.dart'; - -@JsonSerializable() -class TokenOriginData { - TokenOriginSourceType source; - String? appName; - String data; - TokenOriginData({required this.source, required this.data, this.appName}); - - // toString prints not data because it contains the secret - @override - String toString() => 'TokenOrigin{source: $source, app: $appName}'; - - factory TokenOriginData.fromJson(Map json) => _$TokenOriginDataFromJson(json); - - Map toJson() => _$TokenOriginDataToJson(this); -} diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart index 219878dce..df5c5165a 100644 --- a/lib/model/tokens/day_password_token.dart +++ b/lib/model/tokens/day_password_token.dart @@ -1,17 +1,16 @@ import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:otp/otp.dart' as otp_library; import 'package:uuid/uuid.dart'; -import '../../utils/crypto_utils.dart'; +import '../../utils/errors.dart'; import '../../utils/identifiers.dart'; -import '../../utils/utils.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_origin.dart'; +import '../extensions/enums/algorithms_extension.dart'; +import '../extensions/enums/encodings_extension.dart'; +import '../token_import/token_origin_data.dart'; import 'otp_token.dart'; import 'token.dart'; @@ -20,17 +19,12 @@ 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; - @override - Duration get showDuration => const Duration(seconds: 30); - DayPasswordToken({ required Duration period, - required super.label, - required super.issuer, required super.id, required super.algorithm, required super.digits, @@ -44,14 +38,31 @@ class DayPasswordToken extends OTPToken { super.isLocked, super.isHidden, super.origin, + 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 bool sameValuesAs(Token other) { - return super.sameValuesAs(other) && other is DayPasswordToken && other.period == period; + return super.sameValuesAs(other) && other is DayPasswordToken && other.viewMode == viewMode; } + @override + // It is the same token the the period as to be the same + bool isSameTokenAs(Token other) { + return super.isSameTokenAs(other) && other is DayPasswordToken && other.period == period; + } + + @override + bool operator ==(Object other) { + return super == other && other is DayPasswordToken && other.period == period && other.viewMode == viewMode; + } + + @override + int get hashCode => Object.hashAll([super.hashCode, period, viewMode]); + @override DayPasswordToken copyWith({ Duration? period, @@ -63,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, }) => @@ -76,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, @@ -85,17 +96,16 @@ 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, ); @override - String get otpValue => otp_library.OTP.generateTOTPCodeString( - secret, - DateTime.now().millisecondsSinceEpoch, + String get otpValue => algorithm.generateTOTPCodeString( + secret: secret, + time: DateTime.now(), length: digits, - algorithm: algorithm.otpLibraryAlgorithm, - interval: period.inSeconds, + interval: period, isGoogle: true, ); @@ -111,35 +121,56 @@ class DayPasswordToken extends OTPToken { return DateTime.now().add(durationUntilNextOTP + const Duration(milliseconds: 1)); } - 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'); - DayPasswordToken dayPasswordToken; - try { - dayPasswordToken = DayPasswordToken( - label: uriMap[URI_LABEL] ?? '', - issuer: uriMap[URI_ISSUER] ?? '', - id: const Uuid().v4(), - algorithm: mapStringToAlgorithm(uriMap[URI_ALGORITHM] ?? 'SHA1'), - digits: uriMap[URI_DIGITS] ?? 6, - secret: encodeSecretAs(uriMap[URI_SECRET], Encodings.base32), - period: Duration(seconds: uriMap[URI_PERIOD]), - tokenImage: uriMap[URI_IMAGE], - pin: uriMap[URI_PIN], - isLocked: uriMap[URI_PIN], + /// 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] != null && 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] != null && 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, ); - } catch (e) { - throw ArgumentError('Invalid URI: $e'); } - return dayPasswordToken; } - factory DayPasswordToken.fromJson(Map json) => _$DayPasswordTokenFromJson(json); + factory DayPasswordToken.fromUriMap(Map uriMap) { + validateUriMap(uriMap); + + return DayPasswordToken( + label: uriMap[URI_LABEL] ?? '', + issuer: uriMap[URI_ISSUER] ?? '', + id: const Uuid().v4(), + 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] ?? 86400), // default 24 hours + tokenImage: uriMap[URI_IMAGE], + pin: uriMap[URI_PIN], + isLocked: uriMap[URI_PIN], + origin: uriMap[URI_ORIGIN], + ); + } + + @override Map toJson() => _$DayPasswordTokenToJson(this); + factory DayPasswordToken.fromJson(Map json) => _$DayPasswordTokenFromJson(json); @override - String toString() { - return 'DayPassword${super.toString()}period: $period'; - } + String toString() => 'DayPassword${super.toString()}period: $period'; } diff --git a/lib/model/tokens/day_password_token.g.dart b/lib/model/tokens/day_password_token.g.dart index 3d0663046..ebbb0e7d0 100644 --- a/lib/model/tokens/day_password_token.g.dart +++ b/lib/model/tokens/day_password_token.g.dart @@ -9,8 +9,6 @@ part of 'day_password_token.dart'; DayPasswordToken _$DayPasswordTokenFromJson(Map json) => DayPasswordToken( period: Duration(microseconds: json['period'] as int), - label: json['label'] as String, - issuer: json['issuer'] as String, id: json['id'] as String, algorithm: $enumDecode(_$AlgorithmsEnumMap, json['algorithm']), digits: json['digits'] as int, @@ -28,6 +26,8 @@ DayPasswordToken _$DayPasswordTokenFromJson(Map json) => origin: json['origin'] == null ? null : TokenOriginData.fromJson(json['origin'] as Map), + label: json['label'] as String? ?? '', + issuer: json['issuer'] as String? ?? '', ); Map _$DayPasswordTokenToJson(DayPasswordToken instance) => diff --git a/lib/model/tokens/hotp_token.dart b/lib/model/tokens/hotp_token.dart index 2a261f568..75f092f02 100644 --- a/lib/model/tokens/hotp_token.dart +++ b/lib/model/tokens/hotp_token.dart @@ -1,15 +1,13 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:otp/otp.dart' as otp_library; import 'package:uuid/uuid.dart'; -import '../../utils/crypto_utils.dart'; import '../../utils/identifiers.dart'; -import '../../utils/utils.dart'; import '../enums/algorithms.dart'; import '../enums/encodings.dart'; import '../enums/token_types.dart'; -import '../extensions/enum_extension.dart'; -import '../token_origin.dart'; +import '../extensions/enums/algorithms_extension.dart'; +import '../extensions/enums/encodings_extension.dart'; +import '../token_import/token_origin_data.dart'; import 'otp_token.dart'; import 'token.dart'; @@ -17,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 @@ -25,33 +23,34 @@ class HOTPToken extends OTPToken { HOTPToken({ this.counter = 0, - required super.label, - required super.issuer, required super.id, required super.algorithm, required super.digits, 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(type: TokenTypes.HOTP.asString); + super.label = '', + super.issuer = '', + }) : super(type: TokenTypes.HOTP.name); @override - bool sameValuesAs(Token other) { - return super.sameValuesAs(other) && other is HOTPToken && other.counter == counter; - } + bool sameValuesAs(Token other) => super.sameValuesAs(other) && other is HOTPToken && other.counter == counter; @override - String get otpValue => otp_library.OTP.generateHOTPCodeString( - secret, - counter, + // Counter can be changed even if its the same token + bool isSameTokenAs(Token other) => super.isSameTokenAs(other) && other is HOTPToken; + + @override + String get otpValue => algorithm.generateHOTPCodeString( + secret: secret, + counter: counter, length: digits, - algorithm: algorithm.otpLibraryAlgorithm, isGoogle: true, ); @@ -67,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, }) => @@ -83,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, ); @@ -99,27 +98,22 @@ class HOTPToken extends OTPToken { factory HOTPToken.fromUriMap(Map uriMap) { if (uriMap[URI_SECRET] == null) throw ArgumentError('Secret is required'); if (uriMap[URI_DIGITS] < 1) throw ArgumentError('Digits must be greater than 0'); - HOTPToken hotpToken; - try { - hotpToken = HOTPToken( - label: uriMap[URI_LABEL] ?? '', - issuer: uriMap[URI_ISSUER] ?? '', - id: const Uuid().v4(), - algorithm: mapStringToAlgorithm(uriMap[URI_ALGORITHM] ?? 'SHA1'), - digits: uriMap[URI_DIGITS] ?? 6, - secret: encodeSecretAs(uriMap[URI_SECRET], Encodings.base32), - counter: uriMap[URI_COUNTER] ?? 0, - tokenImage: uriMap[URI_IMAGE], - pin: uriMap[URI_PIN], - isLocked: uriMap[URI_PIN], - ); - } catch (e) { - throw ArgumentError('Invalid URI: $e'); - } - return hotpToken; + return HOTPToken( + label: uriMap[URI_LABEL] ?? '', + issuer: uriMap[URI_ISSUER] ?? '', + id: const Uuid().v4(), + 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, + tokenImage: uriMap[URI_IMAGE], + pin: uriMap[URI_PIN], + isLocked: uriMap[URI_PIN], + origin: uriMap[URI_ORIGIN], + ); } - factory HOTPToken.fromJson(Map json) => _$HOTPTokenFromJson(json); - + @override Map toJson() => _$HOTPTokenToJson(this); + factory HOTPToken.fromJson(Map json) => _$HOTPTokenFromJson(json); } diff --git a/lib/model/tokens/hotp_token.g.dart b/lib/model/tokens/hotp_token.g.dart index 513808d66..b8ef1d22d 100644 --- a/lib/model/tokens/hotp_token.g.dart +++ b/lib/model/tokens/hotp_token.g.dart @@ -8,22 +8,22 @@ part of 'hotp_token.dart'; HOTPToken _$HOTPTokenFromJson(Map json) => HOTPToken( counter: json['counter'] as int? ?? 0, - label: json['label'] as String, - issuer: json['issuer'] as String, id: json['id'] as String, algorithm: $enumDecode(_$AlgorithmsEnumMap, json['algorithm']), digits: json['digits'] as int, 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 : TokenOriginData.fromJson(json['origin'] as Map), + label: json['label'] as String? ?? '', + issuer: json['issuer'] as String? ?? '', ); Map _$HOTPTokenToJson(HOTPToken instance) => { diff --git a/lib/model/tokens/otp_token.dart b/lib/model/tokens/otp_token.dart index 7b6032a1a..36711210e 100644 --- a/lib/model/tokens/otp_token.dart +++ b/lib/model/tokens/otp_token.dart @@ -1,5 +1,6 @@ +import '../../utils/logger.dart'; import '../enums/algorithms.dart'; -import '../token_origin.dart'; +import '../token_import/token_origin_data.dart'; import 'token.dart'; abstract class OTPToken extends Token { @@ -7,27 +8,36 @@ abstract class OTPToken extends Token { final int digits; // the number of digits the otp value will have final String secret; // the secret based on which the otp value is calculated in base32 String get otpValue; // the current otp value + Duration get showDuration { + const Duration duration = Duration(seconds: 30); + Logger.info('$runtimeType showDuration: ${duration.inSeconds} seconds'); + return duration; + } // the duration the otp value is shown const OTPToken({ required this.algorithm, required this.digits, required this.secret, - required super.label, - required super.issuer, required super.id, required super.type, super.pin, super.tokenImage, - super.sortIndex, super.isLocked, super.isHidden, + super.sortIndex, super.folderId, super.origin, + super.label = '', + super.issuer = '', }); + // @override + // No changeable value in OTPToken + // bool sameValuesAs(Token other) => super.sameValuesAs(other); + @override - bool sameValuesAs(Token other) { - return super.sameValuesAs(other) && other is OTPToken && other.algorithm == algorithm && other.digits == digits && other.secret == secret; + bool isSameTokenAs(Token other) { + return super.isSameTokenAs(other) && other is OTPToken && other.algorithm == algorithm && other.digits == digits && other.secret == secret; } @override diff --git a/lib/model/tokens/push_token.dart b/lib/model/tokens/push_token.dart index e39b0fb66..3239f2191 100644 --- a/lib/model/tokens/push_token.dart +++ b/lib/model/tokens/push_token.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:pointycastle/asymmetric/api.dart'; import 'package:uuid/uuid.dart'; @@ -8,10 +7,7 @@ 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 '../push_request.dart'; -import '../push_request_queue.dart'; -import '../token_origin.dart'; +import '../token_import/token_origin_data.dart'; import 'token.dart'; part 'push_token.g.dart'; @@ -21,9 +17,10 @@ class PushToken extends Token { static RsaUtils rsaParser = const RsaUtils(); final DateTime? expirationDate; final String serial; + final String? fbToken; @override - Duration get showDuration => Duration.zero; + bool? get isPrivacyIdeaToken => true; // Roll out final bool sslVerify; @@ -45,32 +42,12 @@ class PushToken extends Token { RSAPrivateKey? get rsaPrivateTokenKey => privateTokenKey == null ? null : rsaParser.deserializeRSAPrivateKeyPKCS1(privateTokenKey!); PushToken withPrivateTokenKey(RSAPrivateKey key) => copyWith(privateTokenKey: rsaParser.serializeRSAPrivateKeyPKCS1(key)); - PushToken withPushRequest(PushRequest pr) { - pushRequests.add(pr); - knownPushRequests.put(pr.id); - return copyWith(pushRequests: pushRequests, knownPushRequests: knownPushRequests); - } - - PushToken withoutPushRequest(PushRequest pr) { - if (pushRequests.list.firstWhereOrNull((element) => element.id == pr.id) != null) { - pushRequests.remove(pr); - } - return copyWith(pushRequests: pushRequests); - } - - late final PushRequestQueue pushRequests; - final CustomIntBuffer knownPushRequests; - - bool knowsRequestWithId(int id) { - bool exists = pushRequests.any((element) => element.id == id); - return exists || knownPushRequests.contains(id); - } - PushToken({ required this.serial, - required super.label, - required super.issuer, + super.label, + super.issuer, required super.id, + this.fbToken, this.url, this.expirationDate, this.enrollmentCredentials, @@ -80,11 +57,9 @@ class PushToken extends Token { bool? isRolledOut, bool? sslVerify, PushTokenRollOutState? rolloutState, - PushRequestQueue? pushRequests, - CustomIntBuffer? knownPushRequests, String? type, // just for @JsonSerializable(): type of PushToken is always TokenTypes.PIPUSH - super.sortIndex, super.tokenImage, + super.sortIndex, super.folderId, super.pin, super.isLocked, @@ -93,23 +68,28 @@ class PushToken extends Token { }) : isRolledOut = isRolledOut ?? false, sslVerify = sslVerify ?? false, rolloutState = rolloutState ?? PushTokenRollOutState.rolloutNotStarted, - knownPushRequests = knownPushRequests ?? CustomIntBuffer(), - pushRequests = pushRequests ?? PushRequestQueue(), - super(type: TokenTypes.PIPUSH.asString); + super(type: TokenTypes.PIPUSH.name); @override bool sameValuesAs(Token other) { return super.sameValuesAs(other) && other is PushToken && - other.serial == serial && + other.fbToken == fbToken && other.expirationDate == expirationDate && other.sslVerify == sslVerify && other.enrollmentCredentials == enrollmentCredentials && other.url == url && - other.isRolledOut == isRolledOut && - other.publicServerKey == publicServerKey && + other.isRolledOut == isRolledOut; + } + + @override + bool isSameTokenAs(Token other) { + return super.isSameTokenAs(other) && + other is PushToken && + other.serial == serial && + other.privateTokenKey == privateTokenKey && other.publicTokenKey == publicTokenKey && - other.privateTokenKey == privateTokenKey; + other.publicServerKey == publicServerKey; } @override @@ -119,14 +99,13 @@ class PushToken extends Token { String? issuer, String? id, String? tokenImage, - PushRequestQueue? pushRequests, + String? fbToken, bool? pin, bool? isLocked, bool? isHidden, bool? sslVerify, String? enrollmentCredentials, Uri? url, - int? sortIndex, String? publicServerKey, String? publicTokenKey, String? privateTokenKey, @@ -134,6 +113,7 @@ class PushToken extends Token { bool? isRolledOut, PushTokenRollOutState? rolloutState, CustomIntBuffer? knownPushRequests, + int? sortIndex, int? Function()? folderId, TokenOriginData? origin, }) { @@ -142,22 +122,21 @@ class PushToken extends Token { serial: serial ?? this.serial, issuer: issuer ?? this.issuer, tokenImage: tokenImage ?? this.tokenImage, + fbToken: fbToken ?? this.fbToken, id: id ?? this.id, - pushRequests: pushRequests ?? this.pushRequests, pin: pin ?? this.pin, isLocked: isLocked ?? this.isLocked, isHidden: isHidden ?? this.isHidden, 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, - knownPushRequests: knownPushRequests ?? this.knownPushRequests, + sortIndex: sortIndex ?? this.sortIndex, folderId: folderId != null ? folderId() : this.folderId, origin: origin ?? this.origin, ); @@ -180,15 +159,11 @@ class PushToken extends Token { 'isRolledOut: $isRolledOut, ' 'rolloutState: $rolloutState, ' 'publicServerKey: $publicServerKey, ' - 'publicTokenKey: $publicTokenKey, ' - 'pushRequests: $pushRequests, ' - 'knownPushRequests: $knownPushRequests}'; + 'privateTokenKey: $privateTokenKey, ' + 'publicTokenKey: $publicTokenKey}'; } - factory PushToken.fromUriMap(Map uriMap) { - PushToken pushToken; - try { - pushToken = PushToken( + factory PushToken.fromUriMap(Map uriMap) => PushToken( serial: uriMap[URI_SERIAL] ?? '', label: uriMap[URI_LABEL] ?? '', issuer: uriMap[URI_ISSUER] ?? '', @@ -200,16 +175,11 @@ class PushToken extends Token { tokenImage: uriMap[URI_IMAGE], pin: uriMap[URI_PIN], isLocked: uriMap[URI_PIN], + origin: uriMap[URI_ORIGIN], ); - } catch (e) { - throw ArgumentError('Invalid URI: $e'); - } - return pushToken; - } factory PushToken.fromJson(Map json) { final newToken = _$PushTokenFromJson(json); - newToken.pushRequests.removeWhere((request) => request.expirationDate.isBefore(DateTime.now())); final currentRolloutState = switch (newToken.rolloutState) { PushTokenRollOutState.rolloutNotStarted => PushTokenRollOutState.rolloutNotStarted, PushTokenRollOutState.generatingRSAKeyPair || PushTokenRollOutState.generatingRSAKeyPairFailed => PushTokenRollOutState.generatingRSAKeyPairFailed, @@ -220,5 +190,6 @@ class PushToken extends Token { return newToken.copyWith(rolloutState: currentRolloutState); } + @override Map toJson() => _$PushTokenToJson(this); } diff --git a/lib/model/tokens/push_token.g.dart b/lib/model/tokens/push_token.g.dart index a38b45102..2f3acccfa 100644 --- a/lib/model/tokens/push_token.g.dart +++ b/lib/model/tokens/push_token.g.dart @@ -8,9 +8,10 @@ part of 'push_token.dart'; PushToken _$PushTokenFromJson(Map json) => PushToken( serial: json['serial'] as String, - label: json['label'] as String, - issuer: json['issuer'] as String, + label: json['label'] as String? ?? '', + issuer: json['issuer'] as String? ?? '', id: json['id'] as String, + fbToken: json['fbToken'] as String?, url: json['url'] == null ? null : Uri.parse(json['url'] as String), expirationDate: json['expirationDate'] == null ? null @@ -23,17 +24,9 @@ PushToken _$PushTokenFromJson(Map json) => PushToken( sslVerify: json['sslVerify'] as bool?, rolloutState: $enumDecodeNullable( _$PushTokenRollOutStateEnumMap, json['rolloutState']), - pushRequests: json['pushRequests'] == null - ? null - : PushRequestQueue.fromJson( - json['pushRequests'] as Map), - knownPushRequests: json['knownPushRequests'] == null - ? null - : CustomIntBuffer.fromJson( - json['knownPushRequests'] as Map), 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?, @@ -57,6 +50,7 @@ Map _$PushTokenToJson(PushToken instance) => { 'type': instance.type, 'expirationDate': instance.expirationDate?.toIso8601String(), 'serial': instance.serial, + 'fbToken': instance.fbToken, 'sslVerify': instance.sslVerify, 'enrollmentCredentials': instance.enrollmentCredentials, 'url': instance.url?.toString(), @@ -65,8 +59,6 @@ Map _$PushTokenToJson(PushToken instance) => { 'publicServerKey': instance.publicServerKey, 'privateTokenKey': instance.privateTokenKey, 'publicTokenKey': instance.publicTokenKey, - 'pushRequests': instance.pushRequests, - 'knownPushRequests': instance.knownPushRequests, }; const _$PushTokenRollOutStateEnumMap = { diff --git a/lib/model/tokens/steam_token.dart b/lib/model/tokens/steam_token.dart new file mode 100644 index 000000000..4800e9b84 --- /dev/null +++ b/lib/model/tokens/steam_token.dart @@ -0,0 +1,129 @@ +import 'package:base32/base32.dart'; +import 'package:crypto/crypto.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:uuid/uuid.dart'; + +import '../../utils/errors.dart'; +import '../../utils/identifiers.dart'; +import '../enums/algorithms.dart'; +import '../enums/encodings.dart'; +import '../enums/token_types.dart'; +import '../extensions/enums/encodings_extension.dart'; +import '../extensions/int_extension.dart'; +import '../token_import/token_origin_data.dart'; +import 'token.dart'; +import 'totp_token.dart'; + +part 'steam_token.g.dart'; + +@JsonSerializable() +class SteamToken extends TOTPToken { + @override + bool get isPrivacyIdeaToken => false; + static String get tokenType => TokenTypes.STEAM.name; + static const String steamAlphabet = "23456789BCDFGHJKMNPQRTVWXY"; + + SteamToken({ + required super.id, + required super.secret, + String? type, + super.tokenImage, + super.pin, + super.isLocked, + super.isHidden, + super.sortIndex, + super.folderId, + super.origin, + super.label = '', + super.issuer = '', + }) : super( + type: type ?? tokenType, + period: 30, + digits: 5, + algorithm: Algorithms.SHA1, + ); + + @override + SteamToken copyWith({ + String? label, + String? issuer, + String? id, + bool? isLocked, + bool? isHidden, + bool? pin, + String? tokenImage, + int? sortIndex, + int? Function()? folderId, + TokenOriginData? origin, + int? period, // unused steam tokens always have 30 seconds period + int? digits, // unused steam tokens always have 5 digits + Algorithms? algorithm, // unused steam tokens always have SHA1 algorithm + String? secret, + }) { + return SteamToken( + label: label ?? this.label, + issuer: issuer ?? this.issuer, + id: id ?? this.id, + secret: secret ?? this.secret, + tokenImage: tokenImage ?? this.tokenImage, + 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, + ); + } + + // @override + /// No changeable value in SteamToken + // bool sameValuesAs(Token other) => super.sameValuesAs(other); + + @override + bool isSameTokenAs(Token other) => super.isSameTokenAs(other) && other is SteamToken; + + 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; + final offset = digest[digest.length - 1] & 0x0f; + + var code = ((digest[offset] & 0x7f) << 24) | ((digest[offset + 1] & 0xff) << 16) | ((digest[offset + 2] & 0xff) << 8) | (digest[offset + 3] & 0xff); + + final stringBuffer = StringBuffer(); + for (int i = 0; i < digits; i++) { + stringBuffer.write(steamAlphabet[code % steamAlphabet.length]); + code ~/= steamAlphabet.length; + } + return stringBuffer.toString(); + } + + @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 new file mode 100644 index 000000000..68a5d14ab --- /dev/null +++ b/lib/model/tokens/steam_token.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'steam_token.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SteamToken _$SteamTokenFromJson(Map json) => SteamToken( + id: json['id'] as String, + secret: json['secret'] as String, + type: json['type'] as String?, + tokenImage: json['tokenImage'] as String?, + 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 + : TokenOriginData.fromJson(json['origin'] as Map), + label: json['label'] as String? ?? '', + issuer: json['issuer'] as String? ?? '', + ); + +Map _$SteamTokenToJson(SteamToken instance) => + { + 'label': instance.label, + 'issuer': instance.issuer, + 'id': instance.id, + 'pin': instance.pin, + 'isLocked': instance.isLocked, + 'isHidden': instance.isHidden, + 'tokenImage': instance.tokenImage, + 'folderId': instance.folderId, + 'sortIndex': instance.sortIndex, + 'origin': instance.origin, + 'type': instance.type, + 'secret': instance.secret, + }; diff --git a/lib/model/tokens/token.dart b/lib/model/tokens/token.dart index 63a987135..6d55e8cea 100644 --- a/lib/model/tokens/token.dart +++ b/lib/model/tokens/token.dart @@ -4,14 +4,16 @@ import '../../utils/identifiers.dart'; import '../enums/token_types.dart'; import '../extensions/enum_extension.dart'; import '../mixins/sortable_mixin.dart'; -import '../token_origin.dart'; +import '../token_import/token_origin_data.dart'; import 'day_password_token.dart'; import 'hotp_token.dart'; import 'push_token.dart'; +import 'steam_token.dart'; import 'totp_token.dart'; @immutable abstract class Token with SortableMixin { + bool? get isPrivacyIdeaToken => origin?.isPrivacyIdeaToken; final String tokenVersion = 'v1.0.0'; // The version of this token, this is used for serialization. final String label; // the name of the token, it cannot be uses as an identifier final String issuer; // The issuer of this token, currently unused. @@ -19,12 +21,10 @@ abstract class Token with SortableMixin { final bool pin; final bool isLocked; final bool isHidden; - Duration get showDuration; final String? tokenImage; final int? folderId; @override final int? sortIndex; - final TokenOriginData? origin; // Must be string representation of TokenType enum. @@ -32,26 +32,28 @@ 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); - throw ArgumentError.value(json, 'json', 'Building the token type [$type] is not a supported right now.'); + 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); - throw ArgumentError.value(uriMap, 'uri', 'Building the token type [$type] is not a supported right now.'); + 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'); } const Token({ - required this.label, - required this.issuer, + this.label = '', + this.issuer = '', required this.id, required this.type, this.tokenImage, @@ -66,16 +68,13 @@ abstract class Token with SortableMixin { isHidden = (pin != null && pin ? true : isLocked ?? false) == false ? false : isHidden ?? false, pin = pin ?? false; - /// If the type and the id are the same the tokens it is the same token (== operator). - /// But [sameValuesAs] is used to check if a different token has the same values as this token. - /// Id is here ignored because it is only used to identify the same the token. - bool sameValuesAs(Token other) { - return other.label == label && - other.issuer == issuer && - other.pin == pin && - other.isLocked == isLocked && - other.tokenImage == tokenImage && - other.type == type; + /// This is used to compare the changeable values of the token. + bool sameValuesAs(Token other) => + other.label == label && other.issuer == issuer && other.pin == pin && other.isLocked == isLocked && other.tokenImage == tokenImage; + + /// This is used to identify the same token even if the id is different. + bool isSameTokenAs(Token other) { + return other.type == type; // && other.origin?.appName == origin?.appName && other.origin?.data == origin?.data; } @override @@ -114,4 +113,6 @@ abstract class Token with SortableMixin { 'folderId: $folderId, ' 'origin: $origin, '; } + + Map toJson(); } diff --git a/lib/model/tokens/totp_token.dart b/lib/model/tokens/totp_token.dart index 882747402..75f0db286 100644 --- a/lib/model/tokens/totp_token.dart +++ b/lib/model/tokens/totp_token.dart @@ -1,16 +1,14 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:otp/otp.dart' as otp_library; import 'package:uuid/uuid.dart'; -import '../../utils/crypto_utils.dart'; import '../../utils/identifiers.dart'; import '../../utils/logger.dart'; -import '../../utils/utils.dart'; import '../enums/algorithms.dart'; import '../enums/encodings.dart'; import '../enums/token_types.dart'; -import '../extensions/enum_extension.dart'; -import '../token_origin.dart'; +import '../extensions/enums/algorithms_extension.dart'; +import '../extensions/enums/encodings_extension.dart'; +import '../token_import/token_origin_data.dart'; import 'otp_token.dart'; import 'token.dart'; @@ -18,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 @@ -26,44 +24,47 @@ class TOTPToken extends OTPToken { @override Duration get showDuration { final Duration duration = Duration(milliseconds: (period * 1000 + (secondsUntilNextOTP * 1000).toInt())); - Logger.warning('TOTPToken.showDuration: $duration'); + Logger.info('$runtimeType showDuration: ${duration.inSeconds} seconds'); return duration; } final int period; - @override - String get otpValue => otp_library.OTP.generateTOTPCodeString( - secret, - DateTime.now().millisecondsSinceEpoch, + String otpFromTime(DateTime time) => algorithm.generateTOTPCodeString( + secret: secret, + time: time, length: digits, - algorithm: algorithm.otpLibraryAlgorithm, - interval: period, + interval: Duration(seconds: period), isGoogle: true, ); + @override + String get otpValue => otpFromTime(DateTime.now()); + TOTPToken({ required int period, - required super.label, - required super.issuer, required super.id, required super.algorithm, required super.digits, required super.secret, - String? type, // just for @JsonSerializable(): type of TOTPToken is always TokenTypes.TOTP + String? type, super.tokenImage, - super.sortIndex, super.pin, super.isLocked, super.isHidden, + super.sortIndex, super.folderId, super.origin, + super.label = '', + super.issuer = '', }) : period = period < 1 ? 30 : period, // period must be greater than 0 otherwise IntegerDivisionByZeroException is thrown in OTP.generateTOTPCodeString - super(type: TokenTypes.TOTP.asString); + super(type: type ?? tokenType); + + // @override + // No changeable value in TOTPToken + // bool sameValuesAs(Token other) => super.sameValuesAs(other); @override - bool sameValuesAs(Token other) { - return super.sameValuesAs(other) && other is TOTPToken && other.period == period; - } + bool isSameTokenAs(Token other) => super.isSameTokenAs(other) && other is TOTPToken && other.period == period; @override TOTPToken copyWith({ @@ -75,10 +76,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, }) { @@ -91,10 +92,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, ); @@ -109,24 +110,19 @@ class TOTPToken extends OTPToken { if (uriMap[URI_SECRET] == null) throw ArgumentError('Secret is required'); if (uriMap[URI_DIGITS] != null && uriMap[URI_DIGITS] < 1) throw ArgumentError('Digits must be greater than 0'); if (uriMap[URI_PERIOD] != null && uriMap[URI_PERIOD] < 1) throw ArgumentError('Period must be greater than 0'); - TOTPToken totpToken; - try { - totpToken = TOTPToken( - label: uriMap[URI_LABEL] ?? '', - issuer: uriMap[URI_ISSUER] ?? '', - id: const Uuid().v4(), - algorithm: mapStringToAlgorithm(uriMap[URI_ALGORITHM] ?? 'SHA1'), - digits: uriMap[URI_DIGITS] ?? 6, - tokenImage: uriMap[URI_IMAGE], - secret: encodeSecretAs(uriMap[URI_SECRET], Encodings.base32), - period: uriMap[URI_PERIOD] ?? 30, - pin: uriMap[URI_PIN], - isLocked: uriMap[URI_PIN], - ); - } catch (e) { - throw ArgumentError('Invalid URI: $e'); - } - return totpToken; + return TOTPToken( + label: uriMap[URI_LABEL] ?? '', + issuer: uriMap[URI_ISSUER] ?? '', + id: const Uuid().v4(), + algorithm: Algorithms.values.byName((uriMap[URI_ALGORITHM] ?? 'SHA1')), + digits: uriMap[URI_DIGITS] ?? 6, + tokenImage: uriMap[URI_IMAGE], + secret: Encodings.base32.encode(uriMap[URI_SECRET]), + period: uriMap[URI_PERIOD] ?? 30, + pin: uriMap[URI_PIN], + isLocked: uriMap[URI_PIN], + origin: uriMap[URI_ORIGIN], + ); } double get currentProgress { @@ -139,6 +135,7 @@ class TOTPToken extends OTPToken { return period - (secondsSinceEpoch % (period)); } + @override Map toJson() => _$TOTPTokenToJson(this); factory TOTPToken.fromJson(Map json) => _$TOTPTokenFromJson(json); } diff --git a/lib/model/tokens/totp_token.g.dart b/lib/model/tokens/totp_token.g.dart index 5e7be7112..70e4b229b 100644 --- a/lib/model/tokens/totp_token.g.dart +++ b/lib/model/tokens/totp_token.g.dart @@ -8,22 +8,22 @@ part of 'totp_token.dart'; TOTPToken _$TOTPTokenFromJson(Map json) => TOTPToken( period: json['period'] as int, - label: json['label'] as String, - issuer: json['issuer'] as String, id: json['id'] as String, algorithm: $enumDecode(_$AlgorithmsEnumMap, json['algorithm']), digits: json['digits'] as int, 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 : TokenOriginData.fromJson(json['origin'] as Map), + label: json['label'] as String? ?? '', + issuer: json['issuer'] as String? ?? '', ); Map _$TOTPTokenToJson(TOTPToken instance) => { diff --git a/lib/utils/version.dart b/lib/model/version.dart similarity index 88% rename from lib/utils/version.dart rename to lib/model/version.dart index 440c12916..b5084027d 100644 --- a/lib/utils/version.dart +++ b/lib/model/version.dart @@ -1,3 +1,8 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'version.g.dart'; + +@JsonSerializable() class Version implements Comparable { final int major; final int minor; @@ -84,12 +89,10 @@ class Version implements Comparable { @override String toString() => '$major.$minor.$patch'; + static Version fromJson(Map json) => _$VersionFromJson(json); + + Map toJson() => _$VersionToJson(this); + @override - int get hashCode { - int hash = 17; - hash = hash * 31 + major.hashCode; - hash = hash * 31 + minor.hashCode; - hash = hash * 31 + patch.hashCode; - return hash; - } + int get hashCode => Object.hash(major, minor, patch); } diff --git a/lib/model/version.g.dart b/lib/model/version.g.dart new file mode 100644 index 000000000..8e28376b3 --- /dev/null +++ b/lib/model/version.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'version.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Version _$VersionFromJson(Map json) => Version( + json['major'] as int, + json['minor'] as int, + json['patch'] as int, + ); + +Map _$VersionToJson(Version instance) => { + 'major': instance.major, + 'minor': instance.minor, + 'patch': instance.patch, + }; diff --git a/lib/processors/mixins/token_import_processor.dart b/lib/processors/mixins/token_import_processor.dart index c2956414e..d2c22255d 100644 --- a/lib/processors/mixins/token_import_processor.dart +++ b/lib/processors/mixins/token_import_processor.dart @@ -1,12 +1,13 @@ +import '../../model/processor_result.dart'; import '../../model/tokens/token.dart'; -import '../scheme_processors/token_import_scheme_processors/otp_auth_migration_processor.dart'; +import '../scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart'; import '../token_import_file_processor/token_import_file_processor_interface.dart'; mixin TokenImportProcessor { static Set implementations = { - const OtpAuthMigrationProcessor(), + const GoogleAuthenticatorQrProcessor(), ...TokenImportFileProcessor.implementations, }; - Future> processTokenMigrate(T data, {V args}); + Future>> processTokenMigrate(T data, {V args}); } diff --git a/lib/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor.dart b/lib/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor.dart index 2d71409db..591034229 100644 --- a/lib/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor.dart +++ b/lib/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor.dart @@ -77,9 +77,9 @@ class HomeWidgetNavigateProcessor implements NavigationSchemeProcessor { Logger.warning('Could not find globalRef', name: 'home_widget_processor.dart#_showLockedHomeWidgetProcessor'); return; } - final authenticated = await globalRef!.read(tokenProvider.notifier).showTokenById(tokenId); + final showedToken = await globalRef!.read(tokenProvider.notifier).showTokenById(tokenId); - if (authenticated) { + if (showedToken?.isHidden == false) { final folderId = globalRef!.read(tokenProvider).currentOfId(tokenId)?.folderId; if (folderId != null) { globalRef!.read(tokenFolderProvider.notifier).expandFolderById(folderId); 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 new file mode 100644 index 000000000..6183f4ccb --- /dev/null +++ b/lib/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart @@ -0,0 +1,35 @@ +import '../../../model/enums/token_origin_source_type.dart'; +import '../../../model/extensions/enums/token_origin_source_type.dart'; +import '../../../model/processor_result.dart'; +import '../../../model/tokens/token.dart'; +import '../../../utils/token_import_origins.dart'; +import 'otp_auth_processor.dart'; + +class FreeOtpPlusQrProcessor extends OtpAuthProcessor { + const FreeOtpPlusQrProcessor(); + + @override + Future>> processUri(Uri uri, {bool fromInit = false}) => _processOtpAuth(uri); + + Future>> _processOtpAuth(Uri uri) async { + final results = >[]; + + final result = await super.processUri(uri); + results.addAll(result); + + return results.map((t) { + if (t is! ProcessorResultSuccess) return t; + return ProcessorResultSuccess( + TokenOriginSourceType.qrScanImport.addOriginToToken( + appName: TokenImportOrigins.freeOtpPlus.appName, + token: t.resultData, + isPrivacyIdeaToken: false, + data: uri.toString(), + ), + ); + }).toList(); + } + + @override + Set get supportedSchemes => const OtpAuthProcessor().supportedSchemes; +} diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_migration_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart similarity index 72% rename from lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_migration_processor.dart rename to lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart index 8f190a713..b0ee10ba6 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_migration_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart @@ -5,26 +5,30 @@ 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'; +import '../../../model/processor_result.dart'; import '../../../model/tokens/token.dart'; import '../../../proto/generated/GoogleAuthenticatorImport.pb.dart'; +import '../../../utils/token_import_origins.dart'; import 'otp_auth_processor.dart'; import 'token_import_scheme_processor_interface.dart'; -class OtpAuthMigrationProcessor extends TokenImportSchemeProcessor { - const OtpAuthMigrationProcessor(); +class GoogleAuthenticatorQrProcessor extends TokenImportSchemeProcessor { + const GoogleAuthenticatorQrProcessor(); static const OtpAuthProcessor otpAuthProcessor = OtpAuthProcessor(); @override Set get supportedSchemes => {'otpauth-migration'}; @override - Future> processUri(Uri uri, {bool fromInit = false}) async { + Future>> processUri(Uri uri, {bool fromInit = false}) async { if (!supportedSchemes.contains(uri.scheme)) return []; final value = uri.toString(); // check prefix "otpauth-migration://offline?data=" // extract suffix - Base64 encode - List results = []; + List> results = []; RegExp exp = RegExp(r"otpauth-migration\:\/\/offline\?data=(.*)$"); final match = exp.firstMatch(value); @@ -37,7 +41,6 @@ class OtpAuthMigrationProcessor extends TokenImportSchemeProcessor { final gai = GoogleAuthenticatorImport.fromBuffer(decoded); Logger.warning("${gai.otpParameters.length} tokens found"); for (var param in gai.otpParameters) { - List tokens; try { var base32string = base32.encode(Uint8List.fromList(param.secret)); final name = Uri.encodeFull(param.name); @@ -91,16 +94,31 @@ class OtpAuthMigrationProcessor extends TokenImportSchemeProcessor { type = ""; break; } - Logger.warning("Processing $type token ${param.name}"); + Logger.info("Processing $type token ${param.name}"); final uri = Uri.parse("otpauth://$type/$name?secret=$base32string&issuer=$issuer$algorithm$digits$period$counter"); - tokens = await otpAuthProcessor.processUri(uri); + results.addAll(await otpAuthProcessor.processUri(uri)); } catch (e) { - Logger.warning("Skipping token ${param.name} due to error: $e"); + Logger.error( + "Skipping token ${param.name} due to error: $e", + name: "GoogleAuthenticatorQrProcessor#processUri", + error: e, + stackTrace: StackTrace.current, + ); + results.add(ProcessorResultFailed(e.toString())); continue; } - results.addAll(tokens); } - return results; + return results.map((t) { + if (t is! ProcessorResultSuccess) return t; + return ProcessorResultSuccess( + TokenOriginSourceType.qrScanImport.addOriginToToken( + appName: TokenImportOrigins.googleAuthenticator.appName, + token: t.resultData, + isPrivacyIdeaToken: false, + data: base64.encode(decoded), + ), + ); + }).toList(); } } 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 8c527a894..84895051f 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,14 +1,21 @@ import 'dart:typed_data'; +import 'package:collection/collection.dart'; +import '../../../model/enums/token_origin_source_type.dart'; +import '../../../model/token_import/token_origin_data.dart'; + +import '../../../l10n/app_localizations.dart'; import '../../../model/enums/algorithms.dart'; import '../../../model/enums/encodings.dart'; import '../../../model/enums/token_types.dart'; import '../../../model/extensions/enum_extension.dart'; +import '../../../model/extensions/enums/encodings_extension.dart'; +import '../../../model/processor_result.dart'; import '../../../model/tokens/token.dart'; -import '../../../utils/crypto_utils.dart'; +import '../../../utils/errors.dart'; +import '../../../utils/globals.dart'; import '../../../utils/identifiers.dart'; import '../../../utils/logger.dart'; -import '../../../utils/supported_versions.dart'; import '../../../utils/view_utils.dart'; import '../../../widgets/dialog_widgets/two_step_dialog.dart'; import 'token_import_scheme_processor_interface.dart'; @@ -19,47 +26,58 @@ class OtpAuthProcessor extends TokenImportSchemeProcessor { Set get supportedSchemes => {'otpauth'}; @override - Future> processUri(Uri uri, {bool fromInit = false}) async { - if (!supportedSchemes.contains(uri.scheme)) return []; + Future>> processUri(Uri uri, {bool fromInit = false}) async { + 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 ArgumentError catch (e, s) { - // Error while parsing qr code. - Logger.warning('Malformed QR code:', name: 'token_notifier.dart#_handleOtpAuth', error: e, stackTrace: s); - //showMessage(message: '${e.message}\n Please inform the creator of this qr code about the problem.', duration: const Duration(seconds: 8)); - - return []; + } 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 = globalContextSync != null ? e.localizedMessage(AppLocalizations.of(globalContextSync!)!) : e.unlocalizedMessage; + 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; } Token newToken; try { - newToken = Token.fromUriMap(uriMap); + newToken = + Token.fromUriMap(uriMap).copyWith(origin: TokenOriginData(source: TokenOriginSourceType.link, data: uri.toString(), createdAt: DateTime.now())); } on FormatException catch (e) { Logger.warning('Error while parsing otpAuth.', name: 'token_notifier.dart#addTokenFromOtpAuth', error: e); - showMessage(message: e.message, duration: const Duration(seconds: 3)); - return []; + 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 ProcessorResultFailed('An error occurred while parsing the QR code.')]; } - return [newToken]; + return [ProcessorResultSuccess(newToken)]; } } @@ -67,11 +85,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.HOTP.isString(type) || TokenTypes.TOTP.isString(type) || TokenTypes.DAYPASSWORD.isString(type)) { + if (TokenTypes.values.firstWhereOrNull((element) => element.isName(type, caseSensitive: false)) != null) { return _parseOtpAuth(uri); } throw ArgumentError.value( @@ -81,6 +99,7 @@ Map _parseOtpToken(Uri uri) { ); } +const String _steamTokenIssuer = "Steam"; Map _parseOtpAuth(Uri uri) { // otpauth://TYPE/LABEL?PARAMETERS Map uriMap = {}; @@ -104,6 +123,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') { @@ -114,15 +136,8 @@ Map _parseOtpAuth(Uri uri) { uriMap[URI_IMAGE] = uri.queryParameters['image']; } - String algorithm = uri.queryParameters['algorithm'] ?? Algorithms.SHA1.asString; // Optional parameter - - if (!Algorithms.SHA1.isString(algorithm) && !Algorithms.SHA256.isString(algorithm) && !Algorithms.SHA512.isString(algorithm)) { - throw ArgumentError.value( - uri, - 'uri', - 'The algorithm [$algorithm] is 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; @@ -130,10 +145,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', ); } @@ -143,7 +159,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. // @@ -153,89 +169,93 @@ Map _parseOtpAuth(Uri uri) { secretAsString += '='; } secretAsString = secretAsString.toUpperCase(); - if (!isValidEncoding(secretAsString, Encodings.base32)) { + final secret = Encodings.base32.tryDecode(secretAsString); + if (secret == null) { 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].', ); } - Uint8List secret = decodeSecretToUint8(secretAsString, Encodings.base32); - uriMap[URI_SECRET] = secret; - if (uriMap[URI_TYPE] == 'hotp') { - // Parse counter. - String? counterAsString = uri.queryParameters['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].', + // Parse counter. + String? counterString = uri.queryParameters['counter']; + if (counterString != null) { + uriMap[URI_COUNTER] = int.tryParse(counterString); + if (uriMap[URI_COUNTER] == null) { + throw LocalizedArgumentError( + localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter), + unlocalizedMessage: '[$counterString] is not a valid value for uri parameter [counter].', + invalidValue: counterString, + name: 'counter', ); } } - if (uriMap[URI_TYPE] == 'totp' || uriMap[URI_TYPE] == 'daypassword') { - // Parse period. - String periodAsString = uri.queryParameters['period'] ?? '30'; - - int? period = int.tryParse(periodAsString); - if (period == null) { - throw ArgumentError('Value [$periodAsString] for parameter [period] is invalid.'); + // Parse period. + String? periodString = uri.queryParameters['period']; + if (periodString != null) { + uriMap[URI_PERIOD] = int.tryParse(periodString); + if (uriMap[URI_PERIOD] == null) { + throw LocalizedArgumentError( + localizedMessage: (localizations, value, parameter) => localizations.invalidValueForParameter(value, parameter), + unlocalizedMessage: 'Value [$periodString] for parameter [period] is invalid.', + invalidValue: periodString, + name: 'period', + ); } - uriMap[URI_PERIOD] = period; } if (_is2StepURI(uri)) { - // Parse for 2 step roll out. - String saltLengthAsString = uri.queryParameters['2step_salt'] ?? '10'; - String outputLengthInByteAsString = uri.queryParameters['2step_output'] ?? '20'; - String iterationsAsString = uri.queryParameters['2step_difficulty'] ?? '10000'; - - // Parse parameters - 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].', - ); - } - 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].', - ); - } - try { - uriMap[URI_ITERATIONS] = int.parse(iterationsAsString); - } on FormatException { - throw ArgumentError.value( - uri, - 'uri', - '[$iterationsAsString] is not a valid value for parameter [2step_difficulty].', - ); - } + uriMap.addAll(_parse2StepURI(uri)); } return uriMap; } +Map _parse2StepURI(Uri uri) { + Map uriMap2Step = {}; + // Parse for 2 step roll out. + String saltLengthAsString = uri.queryParameters['2step_salt'] ?? '10'; + String outputLengthInByteAsString = uri.queryParameters['2step_output'] ?? '20'; + String iterationsAsString = uri.queryParameters['2step_difficulty'] ?? '10000'; + + // Parse parameters + try { + uriMap2Step[URI_SALT_LENGTH] = int.parse(saltLengthAsString); + } on FormatException { + 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 { + uriMap2Step[URI_OUTPUT_LENGTH_IN_BYTES] = int.parse(outputLengthInByteAsString); + } on FormatException { + 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 { + uriMap2Step[URI_ITERATIONS] = int.parse(iterationsAsString); + } on FormatException { + 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', + ); + } + return uriMap2Step; +} + Map _parsePiPushToken(Uri uri) { // otpauth://pipush/LABELTEXT? // url=https://privacyidea.org/enroll/this/token @@ -254,7 +274,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 { @@ -263,17 +288,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', ); } @@ -287,19 +314,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']; @@ -328,8 +365,8 @@ Map _parsePiPushToken(Uri uri) { issuer = split[0]; label = split[1]; } else { - label = param; issuer = _parseIssuer(uri); + label = param; } } on Error { label = param; diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/pia_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/pia_processor.dart deleted file mode 100644 index 19431b986..000000000 --- a/lib/processors/scheme_processors/token_import_scheme_processors/pia_processor.dart +++ /dev/null @@ -1,11 +0,0 @@ -import '../../../model/tokens/token.dart'; -import 'token_import_scheme_processor_interface.dart'; - -class PiaProcessor extends TokenImportSchemeProcessor { - const PiaProcessor(); - @override - Set get supportedSchemes => {'pia'}; - - @override - Future> processUri(Uri uri, {bool fromInit = false}) async => []; -} 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 new file mode 100644 index 000000000..73c5f980c --- /dev/null +++ b/lib/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart @@ -0,0 +1,31 @@ +import '../../../model/encryption/token_encryption.dart'; +import '../../../model/processor_result.dart'; +import '../../../model/tokens/token.dart'; +import '../../../utils/logger.dart'; +import 'token_import_scheme_processor_interface.dart'; + +class PrivacyIDEAAuthenticatorQrProcessor extends TokenImportSchemeProcessor { + const PrivacyIDEAAuthenticatorQrProcessor(); + static const scheme = 'pia'; + static const host = 'qrbackup'; + + @override + Set get supportedSchemes => {scheme}; + + @override + Future>> processUri(Uri uri, {bool fromInit = false}) async { + Logger.warning('Processing URI: $uri'); + if (!supportedSchemes.contains(uri.scheme) || uri.host != host) { + Logger.warning('Unsupported scheme or host'); + return []; + } + + try { + final token = TokenEncryption.fromQrCodeUri(uri); + + return [ProcessorResult.success(token)]; + } catch (e) { + return [ProcessorResult.failed('Invalid URI')]; + } + } +} diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface.dart b/lib/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface.dart index 1d25a2196..a67ff36c3 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface.dart @@ -1,28 +1,29 @@ +import '../../../model/processor_result.dart'; import '../../../model/tokens/token.dart'; import '../../mixins/token_import_processor.dart'; import '../scheme_processor_interface.dart'; -import 'otp_auth_migration_processor.dart'; +import 'google_authenticator_qr_processor.dart'; import 'otp_auth_processor.dart'; -import 'pia_processor.dart'; +import 'privacyidea_authenticator_qr_processor.dart'; abstract class TokenImportSchemeProcessor with TokenImportProcessor implements SchemeProcessor { const TokenImportSchemeProcessor(); static const Set implementations = { OtpAuthProcessor(), - OtpAuthMigrationProcessor(), - PiaProcessor(), + GoogleAuthenticatorQrProcessor(), + PrivacyIDEAAuthenticatorQrProcessor(), }; @override /// data: [Uri] uri /// args: [bool] fromInit - Future> processTokenMigrate(Uri data, {bool args = false}) => processUri(data, fromInit: args); + Future>> processTokenMigrate(Uri data, {bool args = false}) => processUri(data, fromInit: args); @override - Future> processUri(Uri uri, {bool fromInit = false}); + Future>> processUri(Uri uri, {bool fromInit = false}); - static Future?> processUriByAny(Uri uri) async { + static Future>?> processUriByAny(Uri uri) async { for (TokenImportSchemeProcessor processor in implementations) { if (processor.supportedSchemes.contains(uri.scheme)) { return await processor.processUri(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 a87748fc6..f8a093ba5 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 @@ -9,11 +9,20 @@ import 'package:encrypt/encrypt.dart'; 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 '../../utils/crypto_utils.dart'; +import '../../l10n/app_localizations.dart'; +import '../../model/processor_result.dart'; +import '../../utils/errors.dart'; +import '../../utils/globals.dart'; +import '../../utils/utils.dart'; import 'token_import_file_processor_interface.dart'; import 'two_fas_import_file_processor.dart'; @@ -44,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 { @@ -66,7 +76,7 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { } @override - Future fileIsValid({required XFile file}) async { + Future fileIsValid(XFile file) async { final Map json; try { final String fileContent = await file.readAsString(); @@ -78,7 +88,7 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { } @override - Future fileNeedsPassword({required XFile file}) async { + Future fileNeedsPassword(XFile file) async { Map json; try { final String fileContent = await file.readAsString(); @@ -90,7 +100,7 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { } @override - Future> processFile({required XFile file, String? password}) async { + Future>> processFile(XFile file, {String? password}) async { final String fileContent = await file.readAsString(); final Map json; try { @@ -107,16 +117,34 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { } } - List _processPlain(Map json) { - final List tokens = []; - 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) { + final results = >[]; + final localization = globalContextSync != null ? AppLocalizations.of(globalContextSync!)! : null; 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']) ?? 'Unsupported token type: ${entry['type']}')); continue; } Map info = entry['info']; @@ -124,19 +152,69 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { URI_TYPE: entry[AEGIS_TYPE], URI_LABEL: entry[AEGIS_LABEL], URI_ISSUER: entry[AEGIS_ISSUER], - URI_SECRET: decodeSecretToUint8(info[AEGIS_SECRET] as String, Encodings.none), + URI_SECRET: Encodings.none.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), + ), }; - tokens.add(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(localization != null ? e.localizedMessage(localization) : e.unlocalizedMessage)); } catch (e) { - Logger.warning('Failed to parse token.', name: '_processPlain#OtpAuthImportFileProcessor'); + Logger.error('Failed to parse token.', name: 'AegisImportFileProcessor#_processPlain', error: e, stackTrace: StackTrace.current); + results.add(ProcessorResult.failed(e.toString())); } } - return tokens; + return Future.value(results); + } + + Future>> _processPlainV3(Map json) { + final results = >[]; + final localization = globalContextSync != null ? AppLocalizations.of(globalContextSync!)! : null; + 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']) ?? 'Unsupported token type: ${entry['type']}')); + continue; + } + Map info = entry['info']; + final entryUriMap = { + URI_TYPE: entry[AEGIS_TYPE], + URI_LABEL: entry[AEGIS_LABEL], + URI_ISSUER: entry[AEGIS_ISSUER], + URI_SECRET: Encodings.base32.decode(info[AEGIS_SECRET]), + URI_ALGORITHM: info[AEGIS_ALGORITHM], + URI_DIGITS: info[AEGIS_DIGITS], + URI_PERIOD: info[AEGIS_PERIOD], + URI_COUNTER: info[AEGIS_COUNTER], + URI_PIN: info[AEGIS_PIN], + URI_ORIGIN: TokenOriginSourceType.backupFile.toTokenOrigin( + appName: TokenImportOrigins.aegisAuthenticator.appName, + isPrivacyIdeaToken: false, + data: jsonEncode(entry), + ), + }; + results.add(ProcessorResult.success(Token.fromUriMap(entryUriMap))); + } on LocalizedException catch (e) { + results.add(ProcessorResultFailed(localization != null ? e.localizedMessage(localization) : e.unlocalizedMessage)); + } catch (e) { + Logger.error('Failed to parse token.', name: 'AegisImportFileProcessor#_processPlain', error: e, stackTrace: StackTrace.current); + results.add(ProcessorResultFailed(e.toString())); + } + } + + return Future.value(results); } Future runIsolatedKdf(ScryptParameters scryptParameters, String password) async { @@ -150,7 +228,7 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { return keyBytes; } - Future> _processEncrypted(Map json, String? password) async { + Future>> _processEncrypted(Map json, String? password) async { final String dbEncrypted = json['db']; final Map header = json['header']; final Map dbParams = header['params']; 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 new file mode 100644 index 000000000..5ddbe17c8 --- /dev/null +++ b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart @@ -0,0 +1,315 @@ +// ignore_for_file: constant_identifier_names, empty_catches + +import 'dart:convert'; + +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/encodings.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/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'; +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/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'; + +class AuthenticatorProImportFileProcessor extends TokenImportFileProcessor { + static const String header = "AUTHENTICATORPRO"; + static const String headerLegacy = "AuthenticatorPro"; + + static const String _AUTHENTICATOR_PRO_TYPE = 'Type'; + static const String _AUTHENTICATOR_PRO_ISSUER = 'Issuer'; + static const String _AUTHENTICATOR_PRO_LABEL = 'Username'; + static const String _AUTHENTICATOR_PRO_SECRET = "Secret"; + static const String _AUTHENTICATOR_PRO_DIGITS = "Digits"; + static const String _AUTHENTICATOR_PRO_PERIOD = "Period"; + static const String _AUTHENTICATOR_PRO_COUNTER = "Counter"; + static const String _AUTHENTICATOR_PRO_ALGORITHM = "Algorithm"; + + static final typeMap = { + 1: TokenTypes.HOTP.name, + 2: TokenTypes.TOTP.name, + // 3: 'mOTP', // Not supported + 4: TokenTypes.STEAM.name, + // 5: 'Yandex', // Not supported + }; + + static final algorithmMap = { + 0: Algorithms.SHA1.name, + 1: Algorithms.SHA256.name, + 2: Algorithms.SHA512.name, + }; + + const AuthenticatorProImportFileProcessor(); + + @override + Future fileIsValid(XFile file) async { + final contentBytes = await file.readAsBytes(); + try { + final contentString = utf8.decode(contentBytes); + try { + // Check if it's a JSON with plain tokens + if (json.decode(contentString)['Authenticators'] != null) return true; + } catch (e) {} + try { + // Check if it's the HTML export + if (contentString.startsWith('')) return true; + } catch (e) {} + try { + // Check if it's a list of URIs + List lines = contentString.split('\n')..removeWhere((element) => element.isEmpty); + if (lines.every((line) => line.isEmpty || line.startsWith('otpauth://'))) return true; + } catch (e) {} + // Its utf8 encoded, but not a JSON, HTML or URI list, so it's not valid -> return false + Logger.warning( + 'File is not a valid Authenticator Pro backup file', + error: 'Invalid content: $contentString', + name: 'authenticator_pro_import_file_processor#fileIsValid', + ); + return false; + } catch (e) { + // When utf8 decoding fails, it's may be encrypted + try { + final headerByteLength = utf8.encode(header).length; + final importedHeader = contentBytes.sublist(0, headerByteLength); + final fileHeader = utf8.decode(importedHeader); + if (fileHeader == header || fileHeader == headerLegacy) { + return true; + } + } catch (e) {} + Logger.warning( + 'File is not a valid Authenticator Pro backup file', + error: 'Content Bytes: $contentBytes', + name: 'authenticator_pro_import_file_processor#fileIsValid', + ); + } + // It's not utf8 encoded and not encrypted, so it's not valid -> return false + return false; + } + + @override + Future fileNeedsPassword(XFile file) async { + final contentBytes = await file.readAsBytes(); + + try { + utf8.decode(contentBytes); + + return false; + } catch (e) { + final headerByteLength = utf8.encode(header).length; + final fileHeaderBytes = contentBytes.sublist(0, headerByteLength); + final fileHeader = utf8.decode(fileHeaderBytes); + + if (fileHeader == header || fileHeader == headerLegacy) { + return true; + } + } + return false; + } + + @override + Future>> processFile(XFile file, {String? password}) async { + var results = >[]; + final bytes = await file.readAsBytes(); + Uint8Buffer uint8buffer = Uint8Buffer(data: bytes); + final headerByteLength = utf8.encode(header).length; + final fileHeader = utf8.decode(uint8buffer.readBytes(headerByteLength)); + Logger.info('File header: $fileHeader', name: 'authenticator_pro_import_file_processor#processFile'); + final plainText = switch (fileHeader) { + header => await _processEncrypted(uint8buffer: uint8buffer, password: password ?? ''), + headerLegacy => await _processEncryptedLegacy(uint8buffer: uint8buffer, password: password ?? ''), + _ => utf8.decode(bytes), + }; + results = await _processPlain(fileContent: plainText); + + return results.map((t) { + if (t is! ProcessorResultSuccess) return t; + return ProcessorResultSuccess(TokenOriginSourceType.backupFile.addOriginToToken( + appName: TokenImportOrigins.authenticatorPro.appName, + token: t.resultData, + isPrivacyIdeaToken: false, + data: t.resultData.origin!.data, + )); + }).toList(); + } + + Future _processEncrypted({required Uint8Buffer uint8buffer, required String password}) async { + // Modern version uses Argon2id + const int keySize = 32; // 32 bytes = 256 bits + const int memoryCost = 65536; // 2^16 KiB = 64 MiB + const int parallelism = 4; + const int iterations = 3; + const int saltSize = 16; + const int ivSize = 12; + + final Argon2id kdf = Argon2id( + iterations: iterations, + parallelism: parallelism, + memory: memoryCost, // number in KiB + hashLength: keySize, + ); + + final aesEncrypted = AesEncrypted( + cypher: AesGcm.with256bits(), // AES/GCM/NoPadding + salt: uint8buffer.readBytes(saltSize), + iv: uint8buffer.readBytes(ivSize), + data: uint8buffer.readBytesToEnd(), + kdf: kdf, + ); + + try { + return await aesEncrypted.decryptToString(password); + } catch (e) { + throw BadDecryptionPasswordException('Invalid password'); + } + } + + Future _processEncryptedLegacy({required Uint8Buffer uint8buffer, required String password}) async { + // Legacy uses PBKDF2 + const int iterations = 64000; + // keySize = 256 bits + const int saltSizeByte = 20; // 20 Bytes = 160 bits + + // // AES/CBC/PKCS5Padding + + final cypher = AesCbc.with256bits(macAlgorithm: Hmac.sha1()); + final ivLength = cypher.nonceLength; + // final salt = uint8buffer.readBytes(saltSize); + // final iv = uint8buffer.readBytes(cypher.blockLength); + final AesEncrypted aesEncrypted = AesEncrypted( + cypher: cypher, + kdf: Pbkdf2( + macAlgorithm: Hmac.sha256(), // KeySize = 256 bits + iterations: iterations, + bits: saltSizeByte * 8, + ), + salt: uint8buffer.readBytes(saltSizeByte), + iv: uint8buffer.readBytes(ivLength), + data: uint8buffer.readBytesToEnd(), + ); + + try { + return await aesEncrypted.decryptToString(password); + } catch (e) { + throw BadDecryptionPasswordException('Invalid password'); + } + } + + Future>> _processPlain({required String fileContent}) async { + try { + final tokensMap = (json.decode(fileContent)['Authenticators'].cast>()) as List>; + return _processJson(tokensMap: tokensMap); + } catch (e) { + try { + final lines = fileContent.split('\n').where((e) => e.isNotEmpty).map((e) => Uri.parse(e)).toList(); + return _processUriList(lines: lines); + } catch (e) { + if (fileContent.startsWith('')) { + return _processHtml(fileContent: fileContent); + } + } + } + return []; + } + + Future>> _processUriList({required List lines}) async { + final results = >[]; + for (var uri in lines) { + try { + final newResults = await const OtpAuthProcessor().processUri(uri); + results.addAll(newResults); + } on LocalizedException catch (e) { + 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(ProcessorResultFailed(e.toString())); + } + } + return results; + } + + Future>> _processHtml({required String fileContent}) async { + RegExp exp = RegExp(r'(?<=(\)).*(?=(\<\/code\>))'); + final results = >[]; + try { + final matches = exp.allMatches(fileContent); + for (var match in matches) { + final uri = Uri.parse(match.group(0)!); + final newResults = await const OtpAuthProcessor().processUri(uri); + for (final newResult in newResults) { + if (newResult is! ProcessorResultSuccess) { + results.add(newResult); + continue; + } + results.add( + ProcessorResultSuccess( + TokenOriginSourceType.backupFile.addOriginToToken( + appName: TokenImportOrigins.authenticatorPro.appName, + isPrivacyIdeaToken: false, + token: newResult.resultData, + data: uri.toString(), + ), + ), + ); + } + } + } on LocalizedException catch (e) { + 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(ProcessorResultFailed(e.toString())); + } + return results; + } + + Future>> _processJson({required List> tokensMap}) async { + Logger.info('Processing plain file', name: 'authenticator_pro_import_file_processor#_processAuthPro'); + final result = >[]; + for (var tokenMap in tokensMap) { + try { + final typeInt = tokenMap[_AUTHENTICATOR_PRO_TYPE] as int; + final tokenType = typeMap[typeInt]; + if (tokenType == null) { + Logger.warning('Unsupported token type: $typeInt'); + continue; + } + final uriMap = { + URI_TYPE: tokenType, + URI_ISSUER: tokenMap[_AUTHENTICATOR_PRO_ISSUER] as String, + URI_LABEL: tokenMap[_AUTHENTICATOR_PRO_LABEL] as String, + URI_SECRET: Encodings.base32.decode(tokenMap[_AUTHENTICATOR_PRO_SECRET] as String), + URI_DIGITS: tokenMap[_AUTHENTICATOR_PRO_DIGITS] as int, + URI_PERIOD: tokenMap[_AUTHENTICATOR_PRO_PERIOD] as int, + URI_ALGORITHM: algorithmMap[tokenMap[_AUTHENTICATOR_PRO_ALGORITHM] as int], + URI_COUNTER: tokenMap[_AUTHENTICATOR_PRO_COUNTER] as int, + URI_ORIGIN: TokenOriginSourceType.backupFile.toTokenOrigin( + appName: TokenImportOrigins.authenticatorPro.appName, + isPrivacyIdeaToken: false, + data: jsonEncode(tokenMap), + ), + }; + + final token = Token.fromUriMap(uriMap); + result.add(ProcessorResultSuccess(token)); + } on LocalizedException catch (e) { + 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(ProcessorResultFailed(e.toString())); + } + } + return result; + } +} diff --git a/lib/processors/token_import_file_processor/free_otp_plus_import_file_processor.dart b/lib/processors/token_import_file_processor/free_otp_plus_import_file_processor.dart new file mode 100644 index 000000000..ef3c9a6ef --- /dev/null +++ b/lib/processors/token_import_file_processor/free_otp_plus_import_file_processor.dart @@ -0,0 +1,127 @@ +// ignore_for_file: constant_identifier_names + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:file_selector/file_selector.dart'; +import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/utils/globals.dart'; +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'; +import 'token_import_file_processor_interface.dart'; + +class FreeOtpPlusImportFileProcessor extends TokenImportFileProcessor { + static const String _FREE_OTP_PLUS_ALGORITHM = 'algo'; // String: "MD5", "SHA1", "SHA256", "SHA512" + static const String _FREE_OTP_PLUS_COUNTER = 'counter'; + static const String _FREE_OTP_PLUS_DIGITS = 'digits'; + static const String _FREE_OTP_PLUS_ISSUER = 'issuerExt'; + static const String _FREE_OTP_PLUS_LABEL = 'label'; + static const String _FREE_OTP_PLUS_PERIOD = 'period'; + static const String _FREE_OTP_PLUS_SECRET = 'secret'; // Base32 encoded + static const String _FREE_OTP_PLUS_TYPE = 'type'; // String: "TOTP", "HOTP" + static const String _steamTokenIssuer = "Steam"; + static const String _steamTokenType = "steam"; + + const FreeOtpPlusImportFileProcessor(); + + @override + Future fileIsValid(XFile file) async { + String contentString; + try { + contentString = await file.readAsString(); + } catch (e) { + return false; + } + try { + final json = jsonDecode(contentString) as Map; + return json['tokens'] != null; + // ignore: empty_catches + } catch (e) {} + List lines = contentString.split('\n')..removeWhere((element) => element.isEmpty); + if (lines.every((line) => line.isEmpty || line.startsWith('otpauth://'))) return true; + return false; + } + + @override + Future fileNeedsPassword(XFile file) async => false; + + @override + Future>> processFile(XFile file, {String? password}) async { + String content = await file.readAsString(); + try { + final json = jsonDecode(content) as Map; + return _processJson(json); + // ignore: empty_catches + } catch (e) {} + List lines = content.split('\n')..removeWhere((element) => element.isEmpty); + final results = >[]; + for (final line in lines) { + try { + final uri = Uri.parse(line); + 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(ProcessorResultFailed(e.toString())); + } + } + return results.map((t) { + if (t is! ProcessorResultSuccess) return t; + return ProcessorResultSuccess(TokenOriginSourceType.backupFile.addOriginToToken( + appName: TokenImportOrigins.freeOtpPlus.appName, + token: t.resultData, + isPrivacyIdeaToken: false, + data: t.resultData.origin!.data, + )); + }).toList(); + } + + Future>> _processJson(Map json) async { + final tokensJson = (json['tokens'] as List?)?.cast>(); + final tokens = >[]; + if (tokensJson == null) { + return []; + } + for (var tokenJson in tokensJson) { + tokens.add(await _processJsonToken(tokenJson)); + } + return tokens; + } + + Future> _processJsonToken(Map tokenJson) async { + try { + return ProcessorResultSuccess(Token.fromUriMap(_jsonToUriMap(tokenJson))); + } on LocalizedException catch (e) { + 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 ProcessorResultFailed(e.toString()); + } + } + + Map _jsonToUriMap(Map tokenJson) { + return { + /// Steam is a special case, its hardcoded in the original app. + URI_TYPE: tokenJson[_FREE_OTP_PLUS_ISSUER] == _steamTokenIssuer ? _steamTokenType : tokenJson[_FREE_OTP_PLUS_TYPE].toLowerCase(), + URI_LABEL: tokenJson[_FREE_OTP_PLUS_LABEL], + URI_SECRET: Uint8List.fromList((tokenJson[_FREE_OTP_PLUS_SECRET] as List).cast()), + URI_ISSUER: tokenJson[_FREE_OTP_PLUS_ISSUER], + URI_ALGORITHM: tokenJson[_FREE_OTP_PLUS_ALGORITHM], + URI_DIGITS: tokenJson[_FREE_OTP_PLUS_DIGITS], + URI_COUNTER: tokenJson[_FREE_OTP_PLUS_COUNTER] + 1, // FreeOTP+ saves only in JSON as 0-based counter + URI_PERIOD: tokenJson[_FREE_OTP_PLUS_PERIOD], + URI_ORIGIN: TokenOriginSourceType.backupFile.toTokenOrigin( + appName: TokenImportOrigins.freeOtpPlus.appName, + isPrivacyIdeaToken: false, + data: jsonEncode(tokenJson), + ), + }; + } +} 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 new file mode 100644 index 000000000..9e85c6bd7 --- /dev/null +++ b/lib/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor.dart @@ -0,0 +1,50 @@ +import 'dart:convert'; + +import 'package:file_selector/file_selector.dart'; + +import '../../model/encryption/token_encryption.dart'; +import '../../model/processor_result.dart'; +import '../../model/tokens/token.dart'; +import '../../utils/logger.dart'; +import 'token_import_file_processor_interface.dart'; +import 'two_fas_import_file_processor.dart'; + +class PrivacyIDEAAuthenticatorImportFileProcessor extends TokenImportFileProcessor { + const PrivacyIDEAAuthenticatorImportFileProcessor(); + @override + Future fileIsValid(XFile file) async { + try { + final content = await file.readAsString(); + final json = jsonDecode(content); + if (json['data'] != null && json['salt'] != null && json['iv'] != null && json['mac'] != null) { + return true; + } + return false; + } catch (e) { + return false; + } + } + + @override + Future fileNeedsPassword(XFile file) => Future.value(true); + + @override + Future>> processFile(XFile file, {String? password}) async { + assert(password != null); + + try { + final content = await file.readAsString(); + List tokens; + try { + tokens = await TokenEncryption.decrypt(encryptedTokens: content, password: password!); + } catch (e) { + throw BadDecryptionPasswordException('Invalid password'); + } + final results = tokens.map((token) => ProcessorResultSuccess(token)).toList(); + return results; + } catch (e) { + Logger.error('Failed to process file', name: 'PrivacyIDEAAuthenticatorImportFileProcessor#processFile', error: e, stackTrace: StackTrace.current); + return [ProcessorResultFailed(e.toString())]; + } + } +} diff --git a/lib/processors/token_import_file_processor/token_import_file_processor_interface.dart b/lib/processors/token_import_file_processor/token_import_file_processor_interface.dart index 5859edece..75a0bdc85 100644 --- a/lib/processors/token_import_file_processor/token_import_file_processor_interface.dart +++ b/lib/processors/token_import_file_processor/token_import_file_processor_interface.dart @@ -1,5 +1,6 @@ import 'package:file_selector/file_selector.dart'; +import '../../model/processor_result.dart'; import '../../model/tokens/token.dart'; import '../../utils/logger.dart'; import '../mixins/token_import_processor.dart'; @@ -10,22 +11,22 @@ abstract class TokenImportFileProcessor with TokenImportProcessor> processTokenMigrate(XFile data, {String? args}) async { - return processFile(file: data, password: args); + Future>> processTokenMigrate(XFile data, {String? args}) async { + return processFile(data, password: args); } - Future> processFile({required XFile file, String? password}); + Future>> processFile(XFile file, {String? password}); static final List implementations = [ const AegisImportFileProcessor(), - const TwoFasFileImportProcessor(), + const TwoFasAuthenticatorImportFileProcessor(), ]; - static Future> processFileByAny({required XFile file, String? password}) async { - List tokens = []; + static Future>> processFileByAny({required XFile file, String? password}) async { + final tokens = >[]; for (TokenImportFileProcessor processor in implementations) { try { - tokens.addAll(await processor.processFile(file: file, password: password)); + tokens.addAll(await processor.processFile(file, password: password)); return tokens; } catch (e) { Logger.warning('Failed to process file with processor ${processor.runtimeType}', @@ -36,8 +37,8 @@ abstract class TokenImportFileProcessor with TokenImportProcessor fileIsValid({required XFile file}); + Future fileIsValid(XFile file); /// Returns true if a password is required to decrypt the Tokens - Future fileNeedsPassword({required XFile file}); + Future fileNeedsPassword(XFile file); } 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 ce8270182..2b09f42f4 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 @@ -2,27 +2,37 @@ import 'dart:convert'; +import 'package:cryptography/cryptography.dart'; import 'package:file_selector/file_selector.dart'; -import 'package:privacyidea_authenticator/model/enums/encodings.dart'; -import 'package:privacyidea_authenticator/utils/logger.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/encodings.dart'; +import '../../model/enums/token_origin_source_type.dart'; +import '../../model/processor_result.dart'; import '../../model/tokens/token.dart'; -import '../../utils/crypto_utils.dart'; +import '../../utils/errors.dart'; +import '../../utils/globals.dart'; import '../../utils/identifiers.dart'; +import '../../utils/logger.dart'; import 'token_import_file_processor_interface.dart'; -class TwoFasFileImportProcessor extends TokenImportFileProcessor { - const TwoFasFileImportProcessor(); +class TwoFasAuthenticatorImportFileProcessor extends TokenImportFileProcessor { + const TwoFasAuthenticatorImportFileProcessor(); static const String TWOFAS_TYPE = 'tokenType'; static const String TWOFAS_ISSUER = 'name'; static const String TWOFAS_SECRET = 'secret'; + static const String TWOFAS_ALGORITHM = 'algorithm'; static const String TWOFAS_LABEL = 'label'; static const String TWOFAS_DIGITS = 'digits'; + static const String TWOFAS_PERIOD = 'period'; static const String TWOFAS_COUNTER = 'counter'; @override - Future> processFile({required XFile file, String? password}) async { + Future>> processFile(XFile file, {String? password}) async { final String fileContent = await file.readAsString(); final Map json; try { @@ -30,37 +40,37 @@ class TwoFasFileImportProcessor extends TokenImportFileProcessor { } catch (e) { throw InvalidFileContentException('No valid 2FAS import file'); } - if (password == null) return processPlainFile(jsonString: fileContent, json: json); + if (password == null) return _processPlainFile(jsonString: fileContent, json: json); return processEncryptedFile(jsonString: fileContent, json: json, password: password); } @override - Future fileNeedsPassword({required XFile file}) async { + Future fileIsValid(XFile file) async { try { final Map json = jsonDecode(await file.readAsString()) as Map; - return json['servicesEncrypted'] != null; + return json['servicesEncrypted'] != null || json['services'] != null; } catch (e) { return false; } } @override - Future fileIsValid({required XFile file}) async { + Future fileNeedsPassword(XFile file) async { try { final Map json = jsonDecode(await file.readAsString()) as Map; - return json['servicesEncrypted'] != null || json['services'] != null; + return json['servicesEncrypted'] != null; } catch (e) { return false; } } - Future> processEncryptedFile({String? jsonString, Map? json, required String password}) async { + Future>> processEncryptedFile({String? jsonString, Map? json, required String password}) async { json ??= jsonDecode(jsonString!) as Map; if (json['servicesEncrypted'] == null) { if (json['services'] == null) { throw InvalidFileContentException('No valid 2FAS import file'); } else { - return processPlainFile(json: json); + return _processPlainFile(json: json); } } final String decryptedTokens; @@ -68,11 +78,19 @@ class TwoFasFileImportProcessor extends TokenImportFileProcessor { try { final servicesEncrypted = json['servicesEncrypted'] as String; final splitted = servicesEncrypted.split(':'); - decryptedTokens = await AESEncrypted( - data: base64Decode(splitted[0]), - salt: base64Decode(splitted[1]), - iv: base64Decode(splitted[2]), - ).decrypt(password); + final dataWithMac = base64Decode(splitted[0]); + + final cypther = AesGcm.with256bits(); + final salt = base64Decode(splitted[1]); + final iv = base64Decode(splitted[2]); + + decryptedTokens = await AesEncrypted( + cypher: cypther, + kdf: Pbkdf2(macAlgorithm: Hmac.sha256(), iterations: 10000, bits: salt.length), + data: dataWithMac, + salt: salt, + iv: iv, + ).decryptToString(password); } catch (e) { Logger.warning('Failed to decrypt 2FAS import file', error: e, name: 'two_fas_import_file_processor.dart#processEncryptedFile'); throw BadDecryptionPasswordException('Wrong decryption password'); @@ -82,11 +100,10 @@ class TwoFasFileImportProcessor extends TokenImportFileProcessor { } catch (e) { throw InvalidFileContentException('No valid 2FAS import file'); } - final List tokens = await processPlainTokens(decryptedTokensJsonList.cast>()); - return tokens; + return await _processPlainTokens(decryptedTokensJsonList.cast>()); } - Future> processPlainFile({String? jsonString, Map? json}) async { + Future>> _processPlainFile({String? jsonString, Map? json}) async { try { json ??= jsonDecode(jsonString!) as Map; } catch (e) { @@ -102,16 +119,23 @@ class TwoFasFileImportProcessor extends TokenImportFileProcessor { } } Logger.info('2FAS import file contains ${tokensJsonList.length} tokens', name: 'two_fas_import_file_processor.dart#processPlainFile'); - return processPlainTokens(tokensJsonList.cast>()); + return _processPlainTokens(tokensJsonList.cast>()); } - Future> processPlainTokens(List> tokensJsonList) async { - final List tokens = []; + Future>> _processPlainTokens(List> tokensJsonList) async { + final results = >[]; for (Map twoFasToken in tokensJsonList) { - tokens.add(Token.fromUriMap(_twoFasToUriMap(twoFasToken))); + try { + results.add(ProcessorResultSuccess(Token.fromUriMap(_twoFasToUriMap(twoFasToken)))); + } on LocalizedException catch (e) { + 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(ProcessorResultFailed(e.toString())); + } } - Logger.info('successfully imported ${tokens.length} tokens', name: 'two_fas_import_file_processor.dart#processPlainTokens'); - return tokens; + Logger.info('successfully imported ${results.length} tokens', name: 'two_fas_import_file_processor.dart#processPlainTokens'); + return results; } Map _twoFasToUriMap(Map twoFasToken) { @@ -119,10 +143,17 @@ class TwoFasFileImportProcessor extends TokenImportFileProcessor { return { URI_TYPE: twoFasOTP[TWOFAS_TYPE], URI_ISSUER: twoFasToken[TWOFAS_ISSUER], - URI_SECRET: decodeSecretToUint8(twoFasToken[TWOFAS_SECRET], Encodings.none), + URI_SECRET: Encodings.base32.decode(twoFasToken[TWOFAS_SECRET]), + URI_ALGORITHM: twoFasOTP[TWOFAS_ALGORITHM], URI_LABEL: twoFasOTP[TWOFAS_LABEL], URI_DIGITS: twoFasOTP[TWOFAS_DIGITS], + URI_PERIOD: twoFasOTP[TWOFAS_PERIOD], URI_COUNTER: twoFasOTP[TWOFAS_COUNTER], + URI_ORIGIN: TokenOriginSourceType.backupFile.toTokenOrigin( + appName: TokenImportOrigins.twoFasAuthenticator.appName, + isPrivacyIdeaToken: false, + data: jsonEncode(twoFasToken), + ), }; } } diff --git a/lib/repo/preference_settings_repository.dart b/lib/repo/preference_settings_repository.dart index 06904ef1c..16043b027 100644 --- a/lib/repo/preference_settings_repository.dart +++ b/lib/repo/preference_settings_repository.dart @@ -1,8 +1,8 @@ -import '../utils/version.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../interfaces/repo/settings_repository.dart'; import '../model/states/settings_state.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/repo/secure_push_request_repository.dart b/lib/repo/secure_push_request_repository.dart new file mode 100644 index 000000000..631a0ca98 --- /dev/null +++ b/lib/repo/secure_push_request_repository.dart @@ -0,0 +1,94 @@ +// ignore_for_file: constant_identifier_names + +import 'dart:convert'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mutex/mutex.dart'; + +import '../interfaces/repo/push_request_repository.dart'; +import '../model/push_request.dart'; +import '../model/states/push_request_state.dart'; +import '../utils/custom_int_buffer.dart'; + +class SecurePushRequestRepository implements PushRequestRepository { + const SecurePushRequestRepository(); + + // Use this to lock critical sections of code. + static final Mutex _m = Mutex(); + + /// Function [f] is executed, protected by Mutex [_m]. + /// That means, that calls of this method will always be executed serial. + static Future protect(Future Function() f) => _m.protect(f); + Future> readAllPushRequests() async { + final value = await _storage.readAll(); + final result = {}; + for (var key in value.keys) { + if (key.startsWith(_securePushRequestKey)) { + result[key] = value[key]!; + } + } + return result; + } + + static const FlutterSecureStorage _storage = FlutterSecureStorage(); + static const String _securePushRequestKey = 'app_v3_pr_state'; + + @override + + /// Save the state to the secure storage. + /// This is a critical section, so it is protected by Mutex. + Future saveState(PushRequestState pushRequestState) => protect(() => _saveState(pushRequestState)); + Future _saveState(PushRequestState pushRequestState) async { + final stateJson = jsonEncode(pushRequestState.toJson()); + await _storage.write(key: _securePushRequestKey, value: stateJson); + } + + @override + + /// Load the state from the secure storage. + /// If no state is found, an empty state is returned. + /// This is a critical section, so it is protected by Mutex. + Future loadState() => protect(_loadState); + Future _loadState() async { + final String? stateJson = await _storage.read(key: _securePushRequestKey); + if (stateJson == null) { + return const PushRequestState(pushRequests: [], knownPushRequests: CustomIntBuffer(list: [])); + } + return PushRequestState.fromJson(jsonDecode(stateJson)); + } + + @override + + /// Adds a push request in the given state if it is not already known. + /// If no state is given, the current state is loaded from the secure storage. + /// This is a critical section, so it is protected by Mutex. + Future add(PushRequest pushRequest, {PushRequestState? state}) => protect(() async { + state ??= await _loadState(); + if (state!.knowsRequest(pushRequest)) { + return state!; + } + final newState = state!.withRequest(pushRequest); + await _saveState(newState); + return newState; + }); + + @override + + /// Remove a push request from the state. + /// If no state is given, the current state is loaded from the secure storage. + /// This is a critical section, so it is protected by Mutex. + Future remove(PushRequest pushRequest, {PushRequestState? state}) => protect(() async { + state ??= await _loadState(); + final newState = state!.withoutRequest(pushRequest); + await _saveState(newState); + return newState; + }); + + @override + + /// Removes all push requests from the repository. + /// If no state is saved, nothing will happen. + /// This is a critical section, so it is protected by Mutex. + Future clearState() => protect(() => _clearState()); + Future _clearState() => _storage.delete(key: _securePushRequestKey); +} diff --git a/lib/repo/secure_token_repository.dart b/lib/repo/secure_token_repository.dart index e97da02b0..eb878bfe7 100644 --- a/lib/repo/secure_token_repository.dart +++ b/lib/repo/secure_token_repository.dart @@ -20,6 +20,7 @@ limitations under the License. */ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; @@ -33,6 +34,7 @@ import 'package:privacyidea_authenticator/utils/logger.dart'; import 'package:privacyidea_authenticator/utils/riverpod_providers.dart'; import 'package:privacyidea_authenticator/utils/view_utils.dart'; +import '../utils/identifiers.dart'; import '../views/settings_view/settings_view_widgets/send_error_dialog.dart'; import '../widgets/dialog_widgets/default_dialog.dart'; import '../widgets/dialog_widgets/default_dialog_button.dart'; @@ -46,128 +48,136 @@ class SecureTokenRepository implements TokenRepository { /// Function [f] is executed, protected by Mutex [_m]. /// That means, that calls of this method will always be executed serial. - static protect(Future Function() f) => _m.protect(f); - + static Future _protect(Future Function() f) => _m.protect(f); static const FlutterSecureStorage _storage = FlutterSecureStorage(); - - static const String _GLOBAL_PREFIX = 'app_v3_'; + static const String _TOKEN_PREFIX = GLOBAL_SECURE_REPO_PREFIX; // ########################################################################### // TOKENS // ########################################################################### - /// Saves [token]s securely on the device, if [token] already exists - /// in the storage the existing value is overwritten. - /// Returns all tokens that could not be saved. @override - Future> saveOrReplaceTokens(List tokens) async { - final failedTokens = []; - for (var element in tokens) { - if (!await _saveOrReplaceToken(element)) { - failedTokens.add(element); - } - } - if (failedTokens.isNotEmpty) { - Logger.warning('Could not save all tokens to secure storage', name: 'storage_utils.dart#saveOrReplaceTokens', stackTrace: StackTrace.current); - } else { - Logger.info('Saved all (${tokens.length}) tokens to secure storage'); - } - return failedTokens; - } - - Future _saveOrReplaceToken(Token token) async { - try { - await _storage.write(key: _GLOBAL_PREFIX + token.id, value: jsonEncode(token)); - } catch (_) { - return false; - } - return true; - } + Future loadToken(String id) => _protect(() async { + final token = await _storage.read(key: _TOKEN_PREFIX + id); + if (token == null) { + Logger.warning('Token not found in secure storage', name: 'secure_token_repository.dart#loadToken'); + return null; + } + return Token.fromJson(jsonDecode(token)); + }); /// Returns a list of all tokens that are saved in the secure storage of /// this device. /// If [loadLegacy] is set to true, will attempt to load old android and ios tokens. @override - Future> loadTokens() async { - late Map keyValueMap; - try { - keyValueMap = await _storage.readAll(); - } on PlatformException catch (e, s) { - Logger.warning("Token found, but could not be decrypted.", name: 'storage_utils.dart#loadTokens', error: e, stackTrace: s, verbose: true); - _decryptErrorDialog(); - return []; - } - - List tokenList = []; - - for (var i = 0; i < keyValueMap.length; i++) { - final value = keyValueMap.values.elementAt(i); - final key = keyValueMap.keys.elementAt(i); - Map? serializedToken; + Future> loadTokens() => _protect>(() async { + late Map keyValueMap; + try { + keyValueMap = await _storage.readAll(); + } on PlatformException catch (e, s) { + Logger.warning("Token found, but could not be decrypted.", name: 'secure_token_repository.dart#loadTokens', error: e, stackTrace: s, verbose: true); + _decryptErrorDialog(); + return []; + } - try { - serializedToken = jsonDecode(value); - } on FormatException catch (e, s) { - if (key == _CURRENT_APP_TOKEN_KEY || key == _NEW_APP_TOKEN_KEY) { - continue; + List tokenList = []; + + for (var i = 0; i < keyValueMap.length; i++) { + final value = keyValueMap.values.elementAt(i); + final key = keyValueMap.keys.elementAt(i); + Map? valueJson; + if (!key.startsWith(_TOKEN_PREFIX)) { + // Every token should start with the global prefix. + // But not everything that starts with the global prefix is a token. + continue; + } + + try { + valueJson = jsonDecode(value); + } on FormatException catch (_) { + // Value should be a json. Skip everything that is not a json. + continue; + } + + if (valueJson == null || !valueJson.containsKey('type')) { + Logger.warning( + 'Could not deserialize token from secure storage. Value: $value\nserializedToken = $valueJson\ncontainsKey(type) = ${valueJson?.containsKey('type')} ', + name: 'secure_token_repository.dart#loadAllTokens'); + // If valueJson is null or does not contain a type, it can't be a token. Skip it. + continue; + } + + // TODO token.version might be deprecated, is there a reason to use it? + // TODO when the token version (token.version) changed handle this here. + Logger.info('Loading token from secure storage: ${valueJson['id']}', name: 'secure_token_repository.dart#loadTokens', stackTrace: StackTrace.current); + try { + tokenList.add(Token.fromJson(valueJson)); + } catch (e, s) { + Logger.error('Could not load token from secure storage', name: 'secure_token_repository.dart#loadTokens', error: e, stackTrace: s); + } } - _storage.delete(key: key); - Logger.warning( - 'Could not deserialize token from secure storage. Value: $value, key: $key', - name: 'storage_utils.dart#loadAllTokens', - error: e, - stackTrace: s, - verbose: true, - ); - // Skip everything that does not fit a serialized token - continue; - } - if (serializedToken == null || !serializedToken.containsKey('type')) { - Logger.warning( - 'Could not deserialize token from secure storage. Value: $value\nserializedToken = $serializedToken\ncontainsKey(type) = ${serializedToken?.containsKey('type')} ', - name: 'storage_utils.dart#loadAllTokens'); - // Skip everything that fits for deserialization but is not a token - continue; - } + //Logger.info('Loaded ${tokenList.length} tokens from secure storage'); + return tokenList; + }); - // TODO token.version might be deprecated, is there a reason to use it? - // TODO when the token version (token.version) changed handle this here. + /// Saves [token]s securely on the device, if [token] already exists + /// in the storage the existing value is overwritten. + /// Returns all tokens that could not be saved. + @override + Future> saveOrReplaceTokens(List tokens) => _protect>(() async { + final failedTokens = []; + for (var element in tokens) { + if (!await _saveOrReplaceToken(element)) { + failedTokens.add(element); + } + } + if (failedTokens.isNotEmpty) { + Logger.warning('Could not save all tokens to secure storage', + name: 'secure_token_repository.dart#saveOrReplaceTokens', stackTrace: StackTrace.current); + } else { + Logger.info('Saved all (${tokens.length}) tokens to secure storage', name: 'secure_token_repository.dart#saveOrReplaceTokens'); + } + return failedTokens; + }); - // TODO Is this still needed? Can a json annotation be used instead to - // define default values? - // Handle new fields here - serializedToken['issuer'] ??= ''; - serializedToken['label'] ??= ''; + @override + Future saveOrReplaceToken(Token token) => _protect(() => _saveOrReplaceToken(token)); - tokenList.add(Token.fromJson(serializedToken)); + Future _saveOrReplaceToken(Token token) async { + try { + await _storage.write(key: _TOKEN_PREFIX + token.id, value: jsonEncode(token)); + } catch (_) { + return false; } - - //Logger.info('Loaded ${tokenList.length} tokens from secure storage'); - return tokenList; + return true; } + /// Deletes the saved jsons of [tokens] from the secure storage. @override - Future> deleteTokens(List tokens) async { - final failedTokens = []; - for (var element in tokens) { - if (!await _deleteToken(element)) { - failedTokens.add(element); - } - } - if (failedTokens.isNotEmpty) { - Logger.warning('Could not delete all tokens from secure storage', - name: 'storage_utils.dart#deleteTokens', error: 'Failed tokens: $failedTokens', stackTrace: StackTrace.current); - } - return failedTokens; - } + Future> deleteTokens(List tokens) => _protect>(() async { + final failedTokens = []; + for (var element in tokens) { + if (!await _deleteToken(element)) { + failedTokens.add(element); + } + } + if (failedTokens.isNotEmpty) { + Logger.warning('Could not delete all tokens from secure storage', + name: 'secure_token_repository.dart#deleteTokens', error: 'Failed tokens: $failedTokens', stackTrace: StackTrace.current); + } + return failedTokens; + }); /// Deletes the saved json of [token] from the secure storage. + @override + Future deleteToken(Token token) => _protect(() => _deleteToken(token)); + Future _deleteToken(Token token) async { try { - _storage.delete(key: _GLOBAL_PREFIX + token.id); + _storage.delete(key: _TOKEN_PREFIX + token.id); } catch (e, s) { - Logger.warning('Could not delete token from secure storage', name: 'storage_utils.dart#deleteToken', error: e, stackTrace: s); + Logger.warning('Could not delete token from secure storage', name: 'secure_token_repository.dart#deleteToken', error: e, stackTrace: s); return false; } Logger.info('Token deleted from secure storage'); @@ -175,105 +185,92 @@ class SecureTokenRepository implements TokenRepository { } // ########################################################################### - // FIREBASE CONFIG + // ERROR HANDLING // ########################################################################### - static const _CURRENT_APP_TOKEN_KEY = '${_GLOBAL_PREFIX}CURRENT_APP_TOKEN'; - - static Future setCurrentFirebaseToken(String str) async => _storage.write(key: _CURRENT_APP_TOKEN_KEY, value: str); - - static Future getCurrentFirebaseToken() async => _storage.read(key: _CURRENT_APP_TOKEN_KEY); - - static const _NEW_APP_TOKEN_KEY = '${_GLOBAL_PREFIX}NEW_APP_TOKEN'; - - // This is used for checking if the token was updated. - static Future setNewFirebaseToken(String str) async => _storage.write(key: _NEW_APP_TOKEN_KEY, value: str); - - static Future getNewFirebaseToken() async => _storage.read(key: _NEW_APP_TOKEN_KEY); -} - -Future _decryptErrorDialog() => showAsyncDialog( - barrierDismissible: false, - builder: (context) => DefaultDialog( - title: Text(AppLocalizations.of(context)!.decryptErrorTitle), - content: Text(AppLocalizations.of(context)!.decryptErrorContent), - actions: [ - DefaultDialogButton( - onPressed: () async { - final isDataDeleted = await _decryptErrorDeleteTokenConfirmationDialog(); - if (isDataDeleted == true) { + Future _decryptErrorDialog() => showAsyncDialog( + barrierDismissible: false, + builder: (context) => DefaultDialog( + title: Text(AppLocalizations.of(context)!.decryptErrorTitle), + content: Text(AppLocalizations.of(context)!.decryptErrorContent), + actions: [ + DefaultDialogButton( + onPressed: () async { + final isDataDeleted = await _decryptErrorDeleteTokenConfirmationDialog(); + if (isDataDeleted == true) { + // ignore: use_build_context_synchronously + Navigator.pop(context); + globalRef?.read(tokenProvider.notifier).loadStateFromRepo(); + } + }, + child: Text( + AppLocalizations.of(context)!.decryptErrorButtonDelete, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ), + DefaultDialogButton( + child: Text(AppLocalizations.of(context)!.decryptErrorButtonSendError), + onPressed: () async { + Logger.info('Sending error report', name: 'secure_token_repository.dart#_decryptErrorDialog'); + await showDialog( + context: context, + builder: (context) => const SendErrorDialog(), + useRootNavigator: false, + ); + }), + DefaultDialogButton( + onPressed: () async { + showDialog( + barrierDismissible: false, + context: context, + builder: (context) => const Center( + child: SizedBox( + height: 50, + width: 50, + child: CircularProgressIndicator(), + ), + ), + ); + await Future.delayed( + const Duration(milliseconds: 500), + ); + // ignore: use_build_context_synchronously + Navigator.pop(context); // ignore: use_build_context_synchronously Navigator.pop(context); globalRef?.read(tokenProvider.notifier).loadStateFromRepo(); - } - }, - child: Text( - AppLocalizations.of(context)!.decryptErrorButtonDelete, - style: TextStyle(color: Theme.of(context).colorScheme.error), + }, + child: Text(AppLocalizations.of(context)!.decryptErrorButtonRetry), ), - ), - DefaultDialogButton( - child: Text(AppLocalizations.of(context)!.decryptErrorButtonSendError), + ], + ), + ); + + Future _decryptErrorDeleteTokenConfirmationDialog() => showAsyncDialog( + builder: (context) => DefaultDialog( + title: Text(AppLocalizations.of(context)!.decryptErrorTitle), + content: Text(AppLocalizations.of(context)!.decryptErrorDeleteConfirmationContent), + actions: [ + DefaultDialogButton( + onPressed: () => Navigator.pop(context, false), + child: Text(AppLocalizations.of(context)!.cancel), + ), + DefaultDialogButton( onPressed: () async { - Logger.info('Sending error report', name: 'storage_utils.dart#_decryptErrorDialog'); - await showDialog( - context: context, - builder: (context) => const SendErrorDialog(), - useRootNavigator: false, + Logger.info( + 'Deleting all tokens from secure storage', + name: 'secure_token_repository.dart#_decryptErrorDeleteTokenConfirmationDialog', + verbose: true, ); - }), - DefaultDialogButton( - onPressed: () async { - showDialog( - barrierDismissible: false, - context: context, - builder: (context) => const Center( - child: SizedBox( - height: 50, - width: 50, - child: CircularProgressIndicator(), - ), - ), - ); - await Future.delayed( - const Duration(milliseconds: 500), - ); - // ignore: use_build_context_synchronously - Navigator.pop(context); - // ignore: use_build_context_synchronously - Navigator.pop(context); - globalRef?.read(tokenProvider.notifier).loadStateFromRepo(); - }, - child: Text(AppLocalizations.of(context)!.decryptErrorButtonRetry), - ), - ], - ), - ); - -Future _decryptErrorDeleteTokenConfirmationDialog() => showAsyncDialog( - builder: (context) => DefaultDialog( - title: Text(AppLocalizations.of(context)!.decryptErrorTitle), - content: Text(AppLocalizations.of(context)!.decryptErrorDeleteConfirmationContent), - actions: [ - DefaultDialogButton( - onPressed: () => Navigator.pop(context, false), - child: Text(AppLocalizations.of(context)!.cancel), - ), - DefaultDialogButton( - onPressed: () async { - Logger.info( - 'Deleting all tokens from secure storage', - name: 'storage_utils.dart#_decryptErrorDeleteTokenConfirmationDialog', - verbose: true, - ); - Navigator.pop(context, true); - await SecureTokenRepository._storage.deleteAll(); - }, - child: Text( - AppLocalizations.of(context)!.decryptErrorButtonDelete, - style: TextStyle(color: Theme.of(context).colorScheme.error), + Navigator.pop(context, true); + await SecureTokenRepository._storage.deleteAll(); + }, + child: Text( + AppLocalizations.of(context)!.decryptErrorButtonDelete, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), ), - ), - ], - ), - ); + ], + ), + ); +} diff --git a/lib/state_notifiers/deeplink_notifier.dart b/lib/state_notifiers/deeplink_notifier.dart index 43e8f509b..8929ddb21 100644 --- a/lib/state_notifiers/deeplink_notifier.dart +++ b/lib/state_notifiers/deeplink_notifier.dart @@ -54,7 +54,7 @@ class DeeplinkNotifier extends StateNotifier { if (!mounted) return; state = DeepLink(initialUri, fromInit: true); Logger.info('Got initial uri from ${source.name}'); - return; // There can only be one initial uri + return; // There should be only one initial uri } } } 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/push_request_notifier.dart b/lib/state_notifiers/push_request_notifier.dart index ce8643997..67eb6e5f1 100644 --- a/lib/state_notifiers/push_request_notifier.dart +++ b/lib/state_notifiers/push_request_notifier.dart @@ -22,71 +22,302 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; +import 'package:mutex/mutex.dart'; +import 'package:privacyidea_authenticator/interfaces/repo/push_request_repository.dart'; +import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; import 'package:privacyidea_authenticator/model/push_request.dart'; import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; -import 'package:privacyidea_authenticator/utils/firebase_utils.dart'; +import 'package:privacyidea_authenticator/utils/globals.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; import 'package:privacyidea_authenticator/utils/network_utils.dart'; import 'package:privacyidea_authenticator/utils/push_provider.dart'; +import 'package:privacyidea_authenticator/utils/riverpod_providers.dart'; import 'package:privacyidea_authenticator/utils/rsa_utils.dart'; -/// Interface between the [PushProvider] and the UI. -class PushRequestNotifier extends StateNotifier { - // Used for periodically polling for push challenges +import '../model/states/push_request_state.dart'; +import '../repo/secure_push_request_repository.dart'; +import '../utils/custom_int_buffer.dart'; - final PushProvider _pushProvider; +class PushRequestNotifier extends StateNotifier { + late final Future initState; + final loadingRepoMutex = Mutex(); + final updatingRequestMutex = Mutex(); + final PushRequestRepository _pushRepo; + + PushProvider _pushProvider; + PushProvider get pushProvider => _pushProvider; final PrivacyIdeaIOClient _ioClient; final RsaUtils _rsaUtils; + final Map _expirationTimers = {}; + PushRequestNotifier({ - PushRequest? initState, - PushProvider? pushProvider, + PushRequestState? initState, PrivacyIdeaIOClient? ioClient, + required PushProvider pushProvider, RsaUtils? rsaUtils, - FirebaseUtils? firebaseUtils, + PushRequestRepository? pushRepo, }) : _ioClient = ioClient ?? const PrivacyIdeaIOClient(), - _pushProvider = pushProvider ?? PushProvider(), + _pushProvider = pushProvider, _rsaUtils = rsaUtils ?? const RsaUtils(), - super(initState) { - _pushProvider.initialize(pushSubscriber: this, firebaseUtils: firebaseUtils ?? FirebaseUtils()); - } - - // ACTIONS - Future acceptPop(PushToken pushToken) async { - final pushRequest = pushToken.pushRequests.tryPop(); - if (pushRequest == null) return false; - Logger.info('Approving push request.', name: 'push_request_notifier.dart#approve'); - final updatedPushRequest = pushRequest.copyWith(accepted: true); - final successfullyApproved = await _handleReaction(pushRequest: updatedPushRequest, token: pushToken); - if (!successfullyApproved) { - pushToken.pushRequests.add(pushRequest); + _pushRepo = pushRepo ?? const SecurePushRequestRepository(), + super( + initState ?? const PushRequestState(pushRequests: [], knownPushRequests: CustomIntBuffer(list: [])), + ) { + _init(initState); + } + + void swapPushProvider(PushProvider newProvider) { + _pushProvider.unsubscribe(add); + _pushProvider = newProvider; + _pushProvider.subscribe(add); + } + + Future _init(PushRequestState? initialState) async { + initState = initialState != null ? Future.value(initialState) : _loadFromRepo(); + _pushProvider.subscribe(add); + await initState; + Logger.info('PushRequestNotifier initialized', name: 'push_request_notifier.dart#_init'); + } + + @override + void dispose() { + _pushProvider.unsubscribe(add); + _cancalAllTimers(); + super.dispose(); + } + + /* + ///////////////////////////////////////////////////////////////////////////// + //////////////////// Repository and PushRequest Handling //////////////////// + ///////////////////////////////////////////////////////////////////////////// + /// Repository layer is always use loadingRepoMutex for the latest state + */ + + Future _loadFromRepo() async { + await loadingRepoMutex.acquire(); + final PushRequestState loadedState; + try { + loadedState = await _pushRepo.loadState(); + } catch (e) { + Logger.error('Failed to load push request state from repo.', name: 'push_request_notifier.dart#_loadFromRepo', error: e); + loadingRepoMutex.release(); + return state; + } + _renewTimers(loadedState.pushRequests); + state = loadedState; + loadingRepoMutex.release(); + return loadedState; + } + + /// Adds a PushRequest to repo and state. Returns true if successful, false if not. + /// If the request already exists, it will be replaced. + Future _addOrReplacePushRequest(PushRequest pushRequest) async { + await loadingRepoMutex.acquire(); + final oldState = state; + final newState = oldState.addOrReplace(pushRequest); + try { + await _pushRepo.saveState(newState); + } catch (e) { + Logger.warning( + 'Failed to save push request: $pushRequest', + name: 'push_request_notifier.dart#_addOrReplacePushRequest', + error: e, + ); + loadingRepoMutex.release(); + return false; + } + state = newState; + loadingRepoMutex.release(); + return true; + } + + /// Replaces a PushRequest in repo and state. Returns true if successful, false if not. + Future _replacePushRequest(PushRequest pushRequest) async { + await loadingRepoMutex.acquire(); + final oldState = state; + final (newState, replaced) = oldState.replaceRequest(pushRequest); + if (!replaced) { + Logger.warning( + 'Tried to replace a push request that does not exist.', + name: 'push_request_notifier.dart#_replacePushRequest', + ); + loadingRepoMutex.release(); + return false; + } + try { + await _pushRepo.saveState(newState); + } catch (e) { + Logger.warning( + 'Failed to save push request: $pushRequest', + name: 'push_request_notifier.dart#_replacePushRequest', + error: e, + ); + loadingRepoMutex.release(); + return false; + } + state = newState; + loadingRepoMutex.release(); + return true; + } + + /// Removes a PushRequest from repo and state. Returns true if successful, false if not. + Future _remove(PushRequest pushRequest) async { + await loadingRepoMutex.acquire(); + final newState = state.withoutRequest(pushRequest); + try { + await _pushRepo.saveState(newState); + } catch (e) { + Logger.error( + 'Failed to save push request state after removing push request.', + name: 'push_request_notifier.dart#_remove', + error: e, + ); + loadingRepoMutex.release(); + return false; + } + state = newState; + _cancelTimer(pushRequest); + loadingRepoMutex.release(); + return true; + } + /* + ////////////////////////////////////////////////////////////////////////////// + ////////////////////// Update PushRequest Methods //////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + /// Updating layer is always use updatingRequestMutex for the latest state + */ + + /// Updates a PushRequest of the current state. The updated PushRequest is saved to the repo and the state. Returns the updated PushRequest if successful, null if not. + Future _updatePushRequest(PushRequest pushRequest, Future Function(PushRequest) updater) async { + await updatingRequestMutex.acquire(); + final current = state.currentOf(pushRequest); + if (current == null) { + Logger.warning('Tried to update a push request that does not exist.', name: 'push_request_notifier.dart#updatePushRequest'); + updatingRequestMutex.release(); + return null; + } + final updated = await updater(current); + final replaced = await _replacePushRequest(updated); + updatingRequestMutex.release(); + return replaced ? updated : current; + } + + /* + ////////////////////////////////////////////////////////////////////////////// + //////////////////////////// Public Methods ////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + /// There is no need to use mutexes because the updating functions are always using the latest version of the updating tokens. + */ + + Future pollForChallenges({required bool isManually}) => pushProvider.pollForChallenges(isManually: isManually); + + Future loadStateFromRepo() => _loadFromRepo(); + + /// Accepts a push request and returns true if successful, false if not. + /// An accepted push request is removed from the state. + /// It should be still in the CustomIntBuffer of the state. + Future accept(PushToken pushToken, PushRequest pushRequest) async { + if (pushRequest.accepted != null) { + Logger.warning('The push request is already accepted or declined.', name: 'push_request_notifier.dart#decline'); + return false; } - state = updatedPushRequest; + Logger.info('Decline push request.', name: 'push_request_notifier.dart#decline'); + final updated = await _updatePushRequest(pushRequest, (p0) async { + final updated = p0.copyWith(accepted: true); + final success = await _handleReaction(pushRequest: updated, token: pushToken); + if (!success) { + return p0; + } + return updated; + }); + if (updated == null || updated.accepted != true) return false; + await _remove(updated); return true; } - Future declinePop(PushToken pushToken) async { - final pushRequest = pushToken.pushRequests.tryPop(); - if (pushRequest == null) return false; + Future decline(PushToken pushToken, PushRequest pushRequest) async { + if (pushRequest.accepted != null) { + Logger.warning('The push request is already accepted or declined.', name: 'push_request_notifier.dart#decline'); + return false; + } Logger.info('Decline push request.', name: 'push_request_notifier.dart#decline'); - final updatedPushRequest = pushRequest.copyWith(accepted: false); - final successfullyDeclined = await _handleReaction(pushRequest: updatedPushRequest, token: pushToken); - if (!successfullyDeclined) { - pushToken.pushRequests.add(pushRequest); + final updated = await _updatePushRequest(pushRequest, (p0) async { + final updated = p0.copyWith(accepted: false); + final success = await _handleReaction(pushRequest: updated, token: pushToken); + if (!success) { + return p0; + } + return updated; + }); + if (updated == null || updated.accepted != false) return false; + await _remove(updated); + return true; + } + + Future add(PushRequest pr) async { + if (state.knowsRequestId(pr.id)) { + Logger.info( + 'The push request already exists.', + name: 'token_notifier.dart#addPushRequestToToken', + ); return false; } - state = updatedPushRequest; - return successfullyDeclined; + // Save the pending request. + await _addOrReplacePushRequest(pr); + // Remove the request after it expires. + _setupTimer(pr); + Logger.info('Added push request ${pr.id} to state', name: 'token_notifier.dart#addPushRequestToToken'); + return true; + } + + Future remove(PushRequest pushRequest) => _remove(pushRequest); + + ////////////////////////////////////////////////////////////////////////////// + ///////////////////////// Helper Methods ///////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + void _renewTimers(List pushRequests) { + _cancalAllTimers(); + _setupAllTimers(pushRequests); + } + + void _cancelTimer(PushRequest pr) => _expirationTimers.remove(pr.id.toString())?.cancel(); + + void _cancalAllTimers() { + for (var i = 0; i < _expirationTimers.length; i++) { + _expirationTimers.remove(i.toString())?.cancel(); + } + } + + /// Sets up a timer to remove the push request after it expires. + /// If the request is already expired, it will be removed immediately. + /// When the timer is set up, the old timer is canceled. + void _setupTimer(PushRequest pr) { + _expirationTimers[pr.id.toString()]?.cancel(); + int time = pr.expirationDate.difference(DateTime.now()).inMilliseconds; + if (time < 1) { + _remove(pr); + return; + } + _expirationTimers[pr.id.toString()] = Timer(Duration(milliseconds: time), () async => _remove(pr)); } - void newRequest(PushRequest pushRequest) => state = pushRequest; + void _setupAllTimers(List pushRequests) { + _cancalAllTimers(); + for (var pr in pushRequests) { + int time = pr.expirationDate.difference(DateTime.now()).inMilliseconds; + if (time < 1) { + _remove(pr); + } + _expirationTimers[pr.id.toString()] = Timer(Duration(milliseconds: time), () async => _remove(pr)); + } + } Future _handleReaction({required PushRequest pushRequest, required PushToken token}) async { if (pushRequest.accepted == null) return false; - Logger.info('Push auth request accepted=${pushRequest.accepted}, sending response to privacyidea', name: 'token_widgets.dart#handleReaction'); - // signature ::= {nonce}|{serial}[|decline] String msg = '${pushRequest.nonce}|${token.serial}'; if (pushRequest.accepted! == false) { @@ -96,7 +327,6 @@ class PushRequestNotifier extends StateNotifier { if (signature == null) { return false; } - // POST https://privacyideaserver/validate/check // nonce= // serial= @@ -110,13 +340,23 @@ class PushRequestNotifier extends StateNotifier { if (pushRequest.accepted! == false) { body["decline"] = "1"; } - - Response response = await _ioClient.doPost(sslVerify: pushRequest.sslVerify, url: pushRequest.uri, body: body); + Response response; + try { + response = await _ioClient.doPost(sslVerify: pushRequest.sslVerify, url: pushRequest.uri, body: body); + } catch (e) { + Logger.warning('Sending push request response failed. Retrying.', name: 'token_widgets.dart#handleReaction'); + try { + response = await _ioClient.doPost(sslVerify: pushRequest.sslVerify, url: pushRequest.uri, body: body); + } catch (e) { + Logger.warning('Sending push request response failed consistently.', name: 'token_widgets.dart#handleReaction', error: e); + globalRef?.read(statusMessageProvider.notifier).state = (AppLocalizations.of(await globalContext)!.connectionFailed, null); + return false; + } + } if (response.statusCode != 200) { Logger.warning('Sending push request response failed.', name: 'token_widgets.dart#handleReaction'); return false; } - return true; } } diff --git a/lib/state_notifiers/settings_notifier.dart b/lib/state_notifiers/settings_notifier.dart index 0850bbe9f..6f960429b 100644 --- a/lib/state_notifiers/settings_notifier.dart +++ b/lib/state_notifiers/settings_notifier.dart @@ -1,10 +1,10 @@ import 'dart:ui'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../utils/version.dart'; import '../interfaces/repo/settings_repository.dart'; import '../model/states/settings_state.dart'; +import '../model/version.dart'; import '../utils/logger.dart'; import '../utils/push_provider.dart'; @@ -25,9 +25,8 @@ class SettingsNotifier extends StateNotifier { void loadFromRepo() async { loadingRepo = Future(() async { final newState = await _repo.loadSettings(); - PushProvider(pollingEnabled: state.enablePolling); + PushProvider.instance?.setPollingEnabled(state.enablePolling); state = newState; - PushProvider(pollingEnabled: state.enablePolling); Logger.info('Loading settings from repo: $newState', name: 'settings_notifier.dart#_loadFromRepo'); return newState; }); 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 2bc8caf4c..d48ff3387 100644 --- a/lib/state_notifiers/token_notifier.dart +++ b/lib/state_notifiers/token_notifier.dart @@ -2,23 +2,26 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:base32/base32.dart'; import 'package:collection/collection.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; 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 '../model/enums/token_origin_source_type.dart'; import '../interfaces/repo/token_repository.dart'; import '../l10n/app_localizations.dart'; import '../model/enums/push_token_rollout_state.dart'; -import '../model/push_request.dart'; +import '../model/enums/token_origin_source_type.dart'; +import '../model/extensions/enums/push_token_rollout_state_extension.dart'; +import '../model/extensions/enums/token_origin_source_type.dart'; +import '../model/processor_result.dart'; import '../model/states/token_state.dart'; import '../model/tokens/hotp_token.dart'; +import '../model/tokens/otp_token.dart'; import '../model/tokens/push_token.dart'; import '../model/tokens/token.dart'; import '../processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface.dart'; @@ -36,12 +39,12 @@ import '../utils/utils.dart'; import '../utils/view_utils.dart'; class TokenNotifier extends StateNotifier { - static final Map _timers = {}; - late Future loadingRepo; - late Future?> updatingTokens = Future(() => null); + static final Map _hidingTimers = {}; + late final Future initState; + final _loadingRepoMutex = Mutex(); + final _updatingTokensMutex = Mutex(); final TokenRepository _repo; final RsaUtils _rsaUtils; - final LegacyUtils _legacy; final PrivacyIdeaIOClient _ioClient; final FirebaseUtils _firebaseUtils; @@ -55,213 +58,274 @@ class TokenNotifier extends StateNotifier { HomeWidgetUtils? homeWidgetUtils, }) : _rsaUtils = rsaUtils ?? const RsaUtils(), _repo = repository ?? const SecureTokenRepository(), - _legacy = legacy ?? const LegacyUtils(), _ioClient = ioClient ?? const PrivacyIdeaIOClient(), _firebaseUtils = firebaseUtils ?? FirebaseUtils(), super( - initialState ?? TokenState(), + initialState ?? TokenState(tokens: const [], lastlyUpdatedTokens: const []), ) { - _init(); + _init(initialState); } - Future _init() async { - await _loadFromRepo(); - await loadingRepo; + Future _init(TokenState? initialState) async { + initState = initialState != null ? Future.value(initialState) : _loadFromRepo(); + await initState; await hideLockedTokens(); Logger.info('TokenNotifier initialized.', name: 'token_notifier.dart#_init'); } + /* ///////////////////////////////////////////////////////////////////////////// /////////////////////// Repository and Token Handling /////////////////////// ///////////////////////////////////////////////////////////////////////////// - /// Always waits for other repo methods + /// Repository layer is always use loadingRepoMutex for the latest state + */ + + /// Adds a token and returns true if successful, false if not. + Future _addOrReplaceToken(Token token) async { + await _loadingRepoMutex.acquire(); + final success = await _repo.saveOrReplaceToken(token); + if (!success) { + Logger.warning( + 'Saving token failed. Token: ${token.id}', + name: 'token_notifier.dart#_addOrReplaceToken', + ); + _loadingRepoMutex.release(); + return false; + } + state = state.addOrReplaceToken(token); + _loadingRepoMutex.release(); + return true; + } + /// Adds a list of tokens and returns the tokens that could not be added or replaced. Future> _addOrReplaceTokens(List tokens) async { + await _loadingRepoMutex.acquire(); + final failedTokens = await _repo.saveOrReplaceTokens(tokens); + if (failedTokens.isNotEmpty) { + Logger.warning( + 'Saving tokens failed. Failed Tokens: ${failedTokens.length}', + name: 'token_notifier.dart#_saveOrReplaceTokens', + ); + // Every token that is saved should not be in the failedTokens list + final savedTokens = tokens.where((element) => !failedTokens.contains(element)).toList(); + state = state.addOrReplaceTokens(savedTokens); + return failedTokens; + } + // [failedTokens] is empty, so every token was saved successfully and we dont need to filter the tokens state = state.addOrReplaceTokens(tokens); - await loadingRepo; - loadingRepo = Future(() async { - final failedTokens = await _repo.saveOrReplaceTokens(state.lastlyUpdatedTokens); - if (failedTokens.isNotEmpty) { - Logger.warning( - 'Saving tokens failed. Failed Tokens: ${failedTokens.length}', - name: 'token_notifier.dart#_saveOrReplaceTokens', - ); - final newState = state.addOrReplaceTokens(failedTokens); - state = newState; - return newState; - } - return state; - }); - return (await loadingRepo).lastlyUpdatedTokens; + _loadingRepoMutex.release(); + return []; } - Future> _replaceTokens(List tokens) async { - state = state.replaceTokens(tokens); - await loadingRepo; - loadingRepo = Future(() async { - final failedTokens = await _repo.saveOrReplaceTokens(state.lastlyUpdatedTokens); - if (failedTokens.isNotEmpty) { - Logger.warning( - 'Saving tokens failed. Failed Tokens: ${failedTokens.length}', - name: 'token_notifier.dart#_saveOrReplaceTokens', - ); - final newState = state.addOrReplaceTokens(failedTokens); - state = newState; - return newState; - } - return state; - }); - return (await loadingRepo).lastlyUpdatedTokens; + /// Replaces a token if it exists and returns true if successful, false if not. + Future _replaceToken(Token token) async { + await _loadingRepoMutex.acquire(); + final (newState, replaced) = state.replaceToken(token); + if (!replaced) { + Logger.warning('Tried to replace a token that does not exist.', name: 'token_notifier.dart#_replaceToken'); + _loadingRepoMutex.release(); + return false; + } + final saved = await _repo.saveOrReplaceToken(token); + if (!saved) { + Logger.warning( + 'Saving token failed. Token: ${token.id}', + name: 'token_notifier.dart#_replaceToken', + ); + _loadingRepoMutex.release(); + return false; + } + state = newState; + _loadingRepoMutex.release(); + return true; } + /// Returns a list of tokens that could not be replaced + Future> _replaceTokens(List tokens) async { + await _loadingRepoMutex.acquire(); + final oldState = state; + final (newState, failedToReplace) = state.replaceTokens(tokens); + state = newState; + for (var e in failedToReplace) { + tokens.remove(e); + } + final failedToSave = await _repo.saveOrReplaceTokens(tokens); + if (failedToSave.isNotEmpty) { + Logger.warning( + 'Saving tokens failed. Failed Tokens: ${failedToSave.length}', + name: 'token_notifier.dart#_saveOrReplaceTokens', + ); + final recovered = oldState.tokens.whereType().where((oldToken) => failedToSave.contains(oldToken)).toList(); + state = state.addOrReplaceTokens(recovered); + _loadingRepoMutex.release(); + return failedToSave; + } + _loadingRepoMutex.release(); + return []; + } + + /// Removes a token and returns true if successful, false if not. Future _removeToken(Token token) async { - await loadingRepo; + await _loadingRepoMutex.acquire(); state = state.withoutToken(token); - loadingRepo = Future(() async { - final failedTokens = await _repo.deleteTokens([token]); - if (failedTokens.isNotEmpty) { - Logger.warning( - 'Deleting tokens failed. Failed Tokens: ${failedTokens.length}', - name: 'token_notifier.dart#_deleteTokensRepo', - ); - final newState = state.addOrReplaceTokens(failedTokens); - state = newState; - return newState; - } - return state; - }); - final failedTokens = (await loadingRepo).lastlyUpdatedTokens; - await _handlePushTokensIfExist(); - return failedTokens.isEmpty; + + final success = await _repo.deleteToken(token); + if (!success) { + Logger.warning( + 'Deleting token failed. Token: ${token.id}', + name: 'token_notifier.dart#_deleteTokensRepo', + ); + state = state.addOrReplaceToken(token); + _loadingRepoMutex.release(); + return false; + } + _loadingRepoMutex.release(); + _handlePushTokensIfExist(); + return true; } + /// Loads the tokens from the repository sets it as the new state and returns the new state. Future _loadFromRepo() async { - List tokens; - loadingRepo = Future( - () async { - try { - tokens = await _repo.loadTokens(); - TokenState newState = TokenState(tokens: tokens); - state = newState; - return newState; - } catch (_) { - return Future(() => state); - } - }, - ); - final newState = await loadingRepo; - await _handlePushTokensIfExist(); + await _loadingRepoMutex.acquire(); + TokenState newState; + try { + List tokens; + tokens = await _repo.loadTokens(); + newState = TokenState(tokens: tokens, lastlyUpdatedTokens: tokens); + state = newState; + } catch (e) { + Logger.error( + 'Loading tokens from storage failed.', + name: 'token_notifier.dart#_loadFromRepo', + error: e, + ); + _loadingRepoMutex.release(); + return state; + } + _loadingRepoMutex.release(); + _handlePushTokensIfExist(); return newState; } + Future _saveStateToRepo(TokenState state) async { + await _loadingRepoMutex.acquire(); + try { + await _repo.saveOrReplaceTokens(state.tokens); + } catch (e) { + Logger.error( + 'Saving tokens to storage failed.', + name: 'token_notifier.dart#_saveStateToRepo', + error: e, + ); + _loadingRepoMutex.release(); + return false; + } + _loadingRepoMutex.release(); + return true; + } + + /* ////////////////////////////////////////////////////////////////////////////// ///////////////////////// Update Token Methods /////////////////////////////// ////////////////////////////////////////////////////////////////////////////// - /// Always waits for repo and other updating methods - - Future updateToken(T token, T Function(T) updater) async { - await updatingTokens; - updatingTokens = Future(() async { - await loadingRepo; - final current = state.currentOf(token); - if (current == null) { - Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#updateToken'); - return null; - } - final updated = updater(current); - return _replaceTokens([updated]); - }); - return (await updatingTokens)?.whereType().firstOrNull; + /// Updating layer is always use updatingTokensMutex for the latest state + */ + + /// Updates a token and returns the updated token if successful, the old token if not and null if the token does not exist. + Future _updateToken(T token, T Function(T) updater) async { + await _updatingTokensMutex.acquire(); + await _loadingRepoMutex.acquire(); + _loadingRepoMutex.release(); + final current = state.currentOf(token); + if (current == null) { + Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#updateToken'); + _updatingTokensMutex.release(); + return null; + } + final updated = updater(current); + final replaced = await _replaceToken(updated); + _updatingTokensMutex.release(); + return replaced ? updated : current; } - Future> updateTokens(List tokens, T Function(T) updater) async { - await updatingTokens; - updatingTokens = Future(() async { - await loadingRepo; - List updatedTokens = []; - for (final token in tokens) { - final current = state.currentOf(token) ?? token; - updatedTokens.add(updater(current)); - } - await _replaceTokens(updatedTokens); - return updatedTokens; - }); - return (await updatingTokens)?.whereType().toList() ?? []; - } + /// Updates a list of tokens and returns the updated tokens if successful, the old tokens if not and an empty list if the tokens does not exist. + Future> _updateTokens(List tokens, T Function(T) updater) async { + await _updatingTokensMutex.acquire(); + final oldState = state; - Future incrementCounter(HOTPToken token) async { - await updatingTokens; - updatingTokens = Future(() async { - await loadingRepo; - token = state.currentOf(token)?.copyWith(counter: token.counter + 1) ?? token.copyWith(counter: token.counter + 1); - return await _replaceTokens([token]); - }); - await updatingTokens; + List updatedTokens = []; + for (final token in tokens) { + final current = state.currentOf(token) ?? token; + updatedTokens.add(updater(current)); + } + final failed = await _replaceTokens(updatedTokens); + final recoveredTokens = oldState.tokens.whereType().where((oldToken) => failed.contains(oldToken)).toList(); + + // Merge the updated tokens with the recovered tokens, so the returned list has the same tokens as the repository. + final mergedTokens = updatedTokens + .map((updated) => recoveredTokens.firstWhere( + (recoveredToken) => recoveredToken == updated, + orElse: () => updated, + )) + .toList(); + _updatingTokensMutex.release(); + return mergedTokens; } - Future hideToken(Token token) async { - await updatingTokens; - updatingTokens = Future(() async { - await loadingRepo; - token = state.currentOf(token)?.copyWith(isHidden: true) ?? token.copyWith(isHidden: true); - return await _replaceTokens([token]); - }); - await updatingTokens; - } + /* + ////////////////////////////////////////////////////////////////////////////// + //////////////////////// UI Interaction Methods ////////////////////////////// + /////// These methods are used to interact with the UI and the user. ///////// + ////////////////////////////////////////////////////////////////////////////// + /// There is no need to use mutexes because the updating functions are always using the latest version of the updating tokens. + */ - Future showToken(Token token) async { - var authenticated = await lockAuth(localizedReason: AppLocalizations.of(globalNavigatorKey.currentContext!)!.authenticateToShowOtp); - await Future.delayed(const Duration(milliseconds: 200)); - await updatingTokens; - updatingTokens = Future(() async { - if (!authenticated) return null; - await loadingRepo; - token = state.currentOf(token)?.copyWith(isHidden: false) ?? token.copyWith(isHidden: false); - return _addOrReplaceTokens([token]); - }); - authenticated = (await updatingTokens)?.isNotEmpty ?? false; - _timers[token.id]?.cancel(); - _timers[token.id] = Timer(token.showDuration, () async { - await hideToken(token); - }); - return authenticated; - } + /// Adds or replaces a token and returns true if successful, false if not. + Future addOrReplaceToken(Token token) => _addOrReplaceToken(token); - Future showTokenById(String tokenId) async { - await updatingTokens; - final token = getTokenFromId(tokenId); - if (token != null) { - return await showToken(token); - } - return false; - } + /// Adds or replaces a list of tokens and returns the tokens that could not be added or replaced. + Future> addOrReplaceTokens(List tokens) => _addOrReplaceTokens(tokens); - Future addOrReplaceToken(Token token) async { - await updatingTokens; - updatingTokens = Future(() async { - await loadingRepo; - return _addOrReplaceTokens([token]); - }); - await updatingTokens; - } + /// Updates a token and returns the updated token if successful, the old token if not and null if the token does not exist. + Future updateToken(T token, T Function(T) updater) async => _updateToken(token, updater); - Future addOrReplaceTokens(List updatedTokens) async { - await updatingTokens; - updatingTokens = Future(() async { - await loadingRepo; - return _addOrReplaceTokens(updatedTokens); - }); - await updatingTokens; + Future> updateTokens(List tokens, T Function(T) updater) async => _updateTokens(tokens, updater); + + /// Increments the counter of a HOTPToken and returns the updated token if successful, the old token if not and null if the token does not exist. + Future incrementCounter(HOTPToken token) => _updateToken(token, (p0) => p0.copyWith(counter: token.counter + 1)); + + /// Hides a token and returns the updated token if successful, the old token if not and null if the token does not exist. + Future hideToken(T token) => _updateToken(token, (p0) => p0.copyWith(isHidden: true) as T); + + /// Shows a token and returns the updated token if successful, the old token if not and null if the token does not exist or the user is not authenticated. + Future showToken(T token) async { + final authenticated = await lockAuth(localizedReason: AppLocalizations.of(globalNavigatorKey.currentContext!)!.authenticateToShowOtp); + if (!authenticated) return null; + final updated = await _updateToken(token, (p0) => p0.copyWith(isHidden: false) as T); + if (updated?.isHidden == false) { + _hidingTimers[token.id]?.cancel(); + _hidingTimers[token.id] = Timer(token.showDuration, () async { + await hideToken(token); + }); + } + return updated; } - ////////////////////////////////////////////////////////////////////////////// - //////////////////////// UI Interaction Methods ////////////////////////////// - /////// These methods are used to interact with the UI and the user. ///////// - ////////////////////////////////////////////////////////////////////////////// - /// Always waits for updating Functions to use the latest state + /// Shows a token and returns the updated token if successful, the old token if not and null if the token does not exist or the user is not authenticated. + Future showTokenById(String tokenId) { + final token = getTokenById(tokenId); + if (token == null) { + Logger.warning('Tried to show a token that does not exist.', name: 'token_notifier.dart#showTokenById'); + return Future.value(null); + } + if (token is! OTPToken) { + Logger.warning('Tried to show a token that is not an OTPToken.', name: 'token_notifier.dart#showTokenById'); + return Future.value(null); + } + return showToken(token); + } Future loadStateFromRepo() async { - await updatingTokens; try { return await _loadFromRepo(); } catch (_) { @@ -271,78 +335,21 @@ class TokenNotifier extends StateNotifier { } Future saveStateToRepo() async { - await updatingTokens; - _cancelTimers(); - await hideLockedTokens(); try { - await _repo.saveOrReplaceTokens(state.tokens); + await _saveStateToRepo(state); Logger.info('Saved ${state.tokens.length} Tokens to storage.', name: 'token_notifier.dart#saveStateToRepo'); return true; } catch (_) { - Logger.warning('Saving tokens to storage failed.', name: 'token_notifier.dart#saveStateToRepo'); + Logger.error('Saving tokens to storage failed.', name: 'token_notifier.dart#saveStateToRepo'); return false; } } - Future removeToken(Token token) async { - await _removeToken(token); - } - - Future addPushRequestToToken(PushRequest pr) async { - await updatingTokens; - PushToken? token = state.tokens.whereType().firstWhereOrNull((t) => t.serial == pr.serial && t.isRolledOut); - Logger.info('Adding push request to token', name: 'token_notifier.dart#addPushRequestToToken'); - if (token == null) { - Logger.warning('The requested token does not exist or is not rolled out.', name: 'token_notifier.dart#addPushRequestToToken'); - return false; - } - String signature = pr.signature; - String signedData = '${pr.nonce}|' - '${pr.uri}|' - '${pr.serial}|' - '${pr.question}|' - '${pr.title}|' - '${pr.sslVerify ? '1' : '0'}'; - - // Re-add url and sslverify to android legacy tokens: - if (token.url == null) { - token = await updateToken(token, (p0) => p0.copyWith(url: pr.uri, sslVerify: pr.sslVerify)); - } - if (token == null) { - Logger.warning('The requested token does not exist anymore', name: 'token_notifier.dart#addPushRequestToToken'); - return false; - } - - bool isVerified = token.privateTokenKey == null - ? await _legacy.verify(token.serial, signedData, signature) - : _rsaUtils.verifyRSASignature(token.rsaPublicServerKey!, utf8.encode(signedData), base32.decode(signature)); - - if (!isVerified) { - Logger.warning( - 'Validating incoming message failed.', - name: 'token_notifier.dart#addPushRequestToToken', - error: 'Signature does not match signed data.', - ); - return false; - } - Logger.info('Validating incoming message was successful.', name: 'token_notifier.dart#addPushRequestToToken'); - - if (token.knowsRequestWithId(pr.id)) { - Logger.info( - 'The push request already exists.', - name: 'token_notifier.dart#addPushRequestToToken', - ); - return false; - } - // Save the pending request. - token = await updateToken(token, (p0) => p0.withPushRequest(pr)) ?? token; - - // Remove the request after it expires. - int time = pr.expirationDate.difference(DateTime.now()).inMilliseconds; - Future.delayed(Duration(milliseconds: time < 1 ? 1 : time), () async => globalRef?.read(tokenProvider.notifier).removePushRequest(pr)); - Logger.info('Added push request ${pr.id} to token ${token.id}', name: 'token_notifier.dart#addPushRequestToToken'); - - return true; + /// Minimizing the app needs to cancel all timers and save the state to the repository. + Future saveStateOnMinimizeApp() async { + _cancelTimers(); + await hideLockedTokens(); + return _saveStateToRepo(state); } Future> hideLockedTokens() async { @@ -355,140 +362,197 @@ class TokenNotifier extends StateNotifier { return await updateTokens(hideLockedTokens, (p0) => p0.copyWith(isHidden: true)); } - Future removePushRequest(PushRequest pushRequest) async { - await updatingTokens; - Logger.info('Removing push request ${pushRequest.id}'); - PushToken? token = state.tokens.whereType().firstWhereOrNull((t) => t.serial == pushRequest.serial); + Future removeToken(Token token) async { + if (token is PushToken) { + await _removePushToken(token); + return; + } + await _removeToken(token); + } - if (token == null) { - Logger.warning('The requested token with serial "${pushRequest.serial}" does not exist.', name: 'token_notifier.dart#removePushRequest'); - return false; + Future _removePushToken(PushToken token) async { + try { + await _firebaseUtils.deleteFirebaseToken(); + } on SocketException { + Logger.warning('Could not delete firebase token.', name: 'token_notifier.dart#_removePushToken'); + globalRef?.read(statusMessageProvider.notifier).state = ( + AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorUnlinkingPushToken(token.label), + AppLocalizations.of(globalNavigatorKey.currentContext!)!.checkYourNetwork, + ); } - token = await updateToken(token, (p0) => p0.withoutPushRequest(pushRequest)) ?? token; - Logger.info('Removed push request from token ${token.id}', name: 'token_notifier.dart#removePushRequest'); - return true; + _firebaseUtils.getFBToken().then((fbToken) async { + if (fbToken == null) { + await _updateTokens(state.pushTokens, (p0) => p0.copyWith(fbToken: null)); + Logger.warning('Could not update firebase token because no firebase token is available.', name: 'token_notifier.dart#_removePushToken'); + globalRef?.read(statusMessageProvider.notifier).state = ( + AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorSynchronizationNoNetworkConnection, + AppLocalizations.of(globalNavigatorKey.currentContext!)!.pleaseSyncManuallyWhenNetworkIsAvailable, + ); + } + final (notUpdated, _) = (await updateFirebaseToken(fbToken)) ?? ([], []); + await _updateTokens(notUpdated, (p0) => p0.copyWith(fbToken: null)); + return; + }); + await _removeToken(token); + Logger.info('Push token "${token.id}" removed successfully.', name: 'token_notifier.dart#_removePushToken'); } Future rolloutPushToken(PushToken token) async { - await updatingTokens; - token = (getTokenFromId(token.id)) as PushToken? ?? token; - assert(token.url != null, 'Token url is null. Cannot rollout token without url.'); - Logger.info('Rolling out token "${token.id}"', name: 'token_notifier.dart#rolloutPushToken'); - if (token.isRolledOut) { - Logger.info('Ignoring rollout request: Token "${token.id}" already rolled out.', name: 'token_notifier.dart#rolloutPushToken'); + PushToken? pushToken; + pushToken = (getTokenById(token.id)) as PushToken?; + if (pushToken == null) { + Logger.warning('Tried to rollout a token that does not exist.', name: 'token_notifier.dart#rolloutPushToken'); + return false; + } + assert(pushToken.url != null, 'Token url is null. Cannot rollout token without url.'); + Logger.info('Rolling out token "${pushToken.id}"', name: 'token_notifier.dart#rolloutPushToken'); + if (pushToken.isRolledOut) { + Logger.info('Ignoring rollout request: Token "${pushToken.id}" already rolled out.', name: 'token_notifier.dart#rolloutPushToken'); return true; } - if (token.rolloutState.rollOutInProgress) { - Logger.info('Ignoring rollout request: Rollout of token "${token.id}" already started. Tokenstate: ${token.rolloutState} ', + if (pushToken.rolloutState.rollOutInProgress) { + Logger.info('Ignoring rollout request: Rollout of token "${pushToken.id}" already started. Tokenstate: ${pushToken.rolloutState} ', name: 'token_notifier.dart#rolloutPushToken'); return false; } - if (token.expirationDate?.isBefore(DateTime.now()) == true) { - Logger.info('Ignoring rollout request: Token "${token.id}" is expired. ', name: 'token_notifier.dart#rolloutPushToken'); + if (pushToken.expirationDate?.isBefore(DateTime.now()) == true) { + Logger.info('Ignoring rollout request: Token "${pushToken.id}" is expired. ', name: 'token_notifier.dart#rolloutPushToken'); if (globalNavigatorKey.currentContext != null) { globalRef?.read(statusMessageProvider.notifier).state = ( AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutNotPossibleAnymore, - AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorTokenExpired(token.label), + AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorTokenExpired(pushToken.label), ); } - _removeToken(token); + await _removeToken(pushToken); return false; } - if (token.privateTokenKey == null) { - Logger.info('Updating rollout state of token "${token.id}" to generatingRSAKeyPair', name: 'token_notifier.dart#rolloutPushToken'); - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.generatingRSAKeyPair)) ?? token; - Logger.info('Updated token "${token.id}"', name: 'token_notifier.dart#rolloutPushToken'); + if (pushToken.privateTokenKey == null) { + Logger.info('Updating rollout state of token "${pushToken.id}" to generatingRSAKeyPair', name: 'token_notifier.dart#rolloutPushToken'); + pushToken = await _updateToken(pushToken, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.generatingRSAKeyPair)); + if (pushToken == null) { + Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#rolloutPushToken'); + return false; + } + Logger.info('Updated token "${pushToken.id}"', name: 'token_notifier.dart#rolloutPushToken'); try { final keyPair = await _rsaUtils.generateRSAKeyPair(); - token = token.withPrivateTokenKey(keyPair.privateKey); - token = token.withPublicTokenKey(keyPair.publicKey); - token = await updateToken(token, (p0) { + pushToken = pushToken.withPrivateTokenKey(keyPair.privateKey); + pushToken = pushToken.withPublicTokenKey(keyPair.publicKey); + pushToken = await _updateToken(pushToken, (p0) { p0 = p0.withPrivateTokenKey(keyPair.privateKey); return p0.withPublicTokenKey(keyPair.publicKey); }) ?? - token; - Logger.info('Updated token "${token.id}"', name: 'token_notifier.dart#rolloutPushToken'); + pushToken; + Logger.info('Updated token "${pushToken.id}"', name: 'token_notifier.dart#rolloutPushToken'); } catch (e, s) { Logger.error('Error while generating RSA key pair.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s); - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.generatingRSAKeyPairFailed)) ?? token; + if (pushToken == null) { + Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#rolloutPushToken'); + return false; + } + pushToken = await _updateToken(pushToken, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.generatingRSAKeyPairFailed)); return false; } } - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKey)) ?? token; + pushToken = await _updateToken(pushToken, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKey)); + if (pushToken == null) { + Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#rolloutPushToken'); + return false; + } if (!kIsWeb && Platform.isIOS) { - Logger.warning('Triggering network access permission for token "${token.id}"', name: 'token_notifier.dart#rolloutPushToken'); - if (await _ioClient.triggerNetworkAccessPermission(url: token.url!, sslVerify: token.sslVerify) == false) { - Logger.warning('Network access permission for token "${token.id}" failed.', name: 'token_notifier.dart#rolloutPushToken'); - updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed)); + Logger.warning('Triggering network access permission for token "${pushToken.id}"', name: 'token_notifier.dart#rolloutPushToken'); + if (await _ioClient.triggerNetworkAccessPermission(url: pushToken.url!, sslVerify: pushToken.sslVerify) == false) { + Logger.warning('Network access permission for token "${pushToken.id}" failed.', name: 'token_notifier.dart#rolloutPushToken'); + _updateToken(pushToken, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed)); return false; } - Logger.warning('Network access permission for token "${token.id}" successful.', name: 'token_notifier.dart#rolloutPushToken'); + Logger.warning('Network access permission for token "${pushToken.id}" successful.', name: 'token_notifier.dart#rolloutPushToken'); } try { // TODO What to do with poll only tokens if google-services is used? - Logger.warning('SSLVerify: ${token.sslVerify}', name: 'token_notifier.dart#rolloutPushToken'); + Logger.warning('SSLVerify: ${pushToken.sslVerify}', name: 'token_notifier.dart#rolloutPushToken'); Response response = await _ioClient.doPost( - sslVerify: token.sslVerify, - url: token.url!, + sslVerify: pushToken.sslVerify, + url: pushToken.url!, body: { - 'enrollment_credential': token.enrollmentCredentials, - 'serial': token.serial, + 'enrollment_credential': pushToken.enrollmentCredentials, + 'serial': pushToken.serial, 'fbtoken': await _firebaseUtils.getFBToken(), - 'pubkey': _rsaUtils.serializeRSAPublicKeyPKCS8(token.rsaPublicTokenKey!), + 'pubkey': _rsaUtils.serializeRSAPublicKeyPKCS8(pushToken.rsaPublicTokenKey!), }, ); if (response.statusCode == 200) { - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.parsingResponse)) ?? token; + pushToken = await _updateToken(pushToken, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.parsingResponse)); + if (pushToken == null) { + Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#rolloutPushToken'); + return false; + } try { RSAPublicKey publicServerKey = await _parseRollOutResponse(response); - token = await updateToken(token, (p0) => p0.withPublicServerKey(publicServerKey)) ?? token; + pushToken = await _updateToken(pushToken, (p0) => p0.withPublicServerKey(publicServerKey)); + if (pushToken == null) { + Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#rolloutPushToken'); + return false; + } } on FormatException catch (e, s) { showMessage(message: "Couldn't parsing RSA public key: ${e.message}", duration: const Duration(seconds: 3)); Logger.warning('Error while parsing RSA public key.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s); - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.parsingResponseFailed)) ?? token; + if (pushToken == null) { + Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#rolloutPushToken'); + return false; + } + pushToken = await _updateToken(pushToken, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.parsingResponseFailed)); return false; } Logger.info('Roll out successful', name: 'token_notifier.dart#rolloutPushToken'); - token = await updateToken(token, (p0) => p0.copyWith(isRolledOut: true, rolloutState: PushTokenRollOutState.rolloutComplete)) ?? token; + pushToken = await _updateToken(pushToken, (p0) => p0.copyWith(isRolledOut: true, rolloutState: PushTokenRollOutState.rolloutComplete)); checkNotificationPermission(); return true; } else { Logger.warning('Post request on roll out failed.', name: 'token_notifier.dart#rolloutPushToken', - error: 'Token: ${token.serial}\nStatus code: ${response.statusCode},\nURL:${response.request?.url}\nBody: ${response.body}'); + error: 'Token: ${pushToken.serial}\nStatus code: ${response.statusCode},\nURL:${response.request?.url}\nBody: ${response.body}'); try { final message = response.body.isNotEmpty ? (json.decode(response.body)['result']?['error']?['message']) : ''; globalRef?.read(statusMessageProvider.notifier).state = ( - AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutFailed(token.label), + AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutFailed(pushToken.label), message, ); } on FormatException { // Format Exception is thrown if the response body is not a valid json. This happens if the server is not reachable. globalRef?.read(statusMessageProvider.notifier).state = ( - AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutFailed(token.label), + AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutFailed(pushToken.label), AppLocalizations.of(globalNavigatorKey.currentContext!)!.statusCode(response.statusCode) ); } - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed)) ?? token; + pushToken = await _updateToken(pushToken, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed)); return false; } } catch (e, s) { - token = await updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed)) ?? token; + if (pushToken == null) { + Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#rolloutPushToken'); + return false; + } + pushToken = await _updateToken(pushToken, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed)); + if (pushToken == null) { + Logger.warning('Tried to update a token that does not exist.', name: 'token_notifier.dart#rolloutPushToken'); + return false; + } if (e is PlatformException && e.code == FIREBASE_TOKEN_ERROR_CODE || e is SocketException || e is TimeoutException || e is FirebaseException) { Logger.warning('Connection error: Roll out push token failed.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s); showMessage( - message: AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutNoConnectionToServer(token.label), + message: AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutNoConnectionToServer(pushToken.label), duration: const Duration(seconds: 3), ); } else if (e is HandshakeException) { @@ -510,6 +574,69 @@ class TokenNotifier extends StateNotifier { } } + /// This method attempts to update the fbToken for all PushTokens that can be + /// updated. I.e. all tokens that know the url of their respective privacyIDEA + /// server. + /// If the fbToken is not provided, it will be fetched from the firebase instance. + /// If the fbToken is not available, this method will return null. + /// Returns a tuple of two lists. The first list contains all tokens that + /// could not be updated. The second list contains all tokens that do not + /// support updating the fbToken. + /// + /// This should only be used to attempt to update the fbToken automatically, + /// as this can not be guaranteed to work. There is a manual option available + /// through the settings also. + Future<(List, List)?> updateFirebaseToken([String? firebaseToken]) async { + firebaseToken ??= await _firebaseUtils.getFBToken(); + if (firebaseToken == null) { + Logger.warning('Could not update firebase token because no firebase token is available.', name: 'push_provider.dart#updateFirebaseToken'); + return null; + } + List tokenList = state.pushTokens.where((t) => t.isRolledOut && t.fbToken != firebaseToken).toList(); + Logger.info('Updating firebase token for ${tokenList.length} push tokens.', name: 'push_provider.dart#updateFirebaseToken'); + bool allUpdated = true; + final List failedTokens = []; + final List unsuportedTokens = []; + + for (PushToken p in tokenList) { + if (p.url == null) { + unsuportedTokens.add(p); + continue; + } + // POST /ttype/push HTTP/1.1 + //Host: example.com + // + //new_fb_token= + //serial=element + //timestamp= + //signature=SIGNATURE(||) + Logger.warning('Updating firebase token for push token "${p.serial}"', name: 'push_provider.dart#updateFirebaseToken'); + String timestamp = DateTime.now().toUtc().toIso8601String(); + String message = '$firebaseToken|${p.serial}|$timestamp'; + String? signature = await const RsaUtils().trySignWithToken(p, message); + if (signature == null) { + failedTokens.add(p); + allUpdated = false; + continue; + } + Response response = await _ioClient.doPost( + sslVerify: p.sslVerify, url: p.url!, body: {'new_fb_token': firebaseToken, 'serial': p.serial, 'timestamp': timestamp, 'signature': signature}); + if (response.statusCode == 200) { + Logger.info('Updating firebase token for push token succeeded!', name: 'push_provider.dart#updateFirebaseToken'); + _updateToken(p, (p0) => p0.copyWith(fbToken: firebaseToken)); + } else { + Logger.warning('Updating firebase token for push token failed!', name: 'push_provider.dart#updateFirebaseToken'); + failedTokens.add(p); + allUpdated = false; + } + } + + if (allUpdated) { + await _firebaseUtils.setCurrentFirebaseToken(firebaseToken); + } + return (failedTokens, unsuportedTokens); + } + ///////////////////////////////////////////////////////////////////////////// //////////////////////// Add New Tokens Methods ///////////////////////////// ///////////////////////////////////////////////////////////////////////////// @@ -524,27 +651,31 @@ class TokenNotifier extends StateNotifier { uri = Uri.parse(qrCode); } catch (_) { showMessage(message: 'The scanned QR code is not a valid URI.', duration: const Duration(seconds: 3)); + Logger.warning('Scanned Data: $qrCode', error: 'Scanned QR code is not a valid URI.', name: 'token_notifier.dart#handleQrCode'); return; } List tokens = await _tokensFromUri(uri); - tokens = tokens.map((e) => TokenOriginSourceType.qrScan.addOriginToToken(token: e, data: qrCode)).toList(); - await addOrReplaceTokens(tokens); + tokens = tokens.map((e) => TokenOriginSourceType.qrScan.addOriginToToken(token: e, data: qrCode, isPrivacyIdeaToken: null)).toList(); + await _addOrReplaceTokens(tokens); await _handlePushTokensIfExist(); } Future handleLink(Uri uri) async { List tokens = await _tokensFromUri(uri); - tokens = tokens.map((e) => TokenOriginSourceType.link.addOriginToToken(token: e, data: uri.toString())).toList(); - await addOrReplaceTokens(tokens); + tokens = tokens.map((e) => TokenOriginSourceType.link.addOriginToToken(token: e, data: uri.toString(), isPrivacyIdeaToken: null)).toList(); + await _addOrReplaceTokens(tokens); await _handlePushTokensIfExist(); } Future> _tokensFromUri(Uri uri) async { - List? tokens; try { - tokens = await TokenImportSchemeProcessor.processUriByAny(uri); - } catch (_) {} - return tokens ?? []; + final results = await TokenImportSchemeProcessor.processUriByAny(uri); + return results?.whereType>().map((e) => e.resultData).toList() ?? []; + } catch (error, stackTrace) { + showMessage(message: 'The scanned QR code is not a valid URI.', duration: const Duration(seconds: 3)); + Logger.warning('Scanned Data: $uri', error: error, name: 'token_notifier.dart#handleQrCode', stackTrace: stackTrace); + return []; + } } ////////////////////////////////////////////////////////////////////////////// @@ -566,12 +697,16 @@ class TokenNotifier extends StateNotifier { } Future _handlePushTokensIfExist() async { - await loadingRepo; - if (state.hasPushTokens == false || state.hasOTPTokens == false) { + final pushTokens = state.pushTokens; + if (pushTokens.isNotEmpty || state.hasOTPTokens == false) { if (globalRef?.read(settingsProvider).hidePushTokens == true) { globalRef!.read(settingsProvider.notifier).setHidePushTokens(false); } } + if (pushTokens.firstWhereOrNull((element) => element.isRolledOut && element.fbToken == null) != null) { + // If there is a push token without fbToken, then update the fbToken + await updateFirebaseToken(); + } if (state.hasRolledOutPushTokens) { checkNotificationPermission(); } @@ -581,14 +716,14 @@ class TokenNotifier extends StateNotifier { } } - Token? getTokenFromId(String id) { + Token? getTokenById(String id) { return state.tokens.firstWhereOrNull((element) => element.id == id); } void _cancelTimers() { - for (final key in _timers.keys) { - _timers[key]?.cancel(); + for (final key in _hidingTimers.keys) { + _hidingTimers[key]?.cancel(); } - _timers.clear(); + _hidingTimers.clear(); } } diff --git a/lib/utils/app_info_utils.dart b/lib/utils/app_info_utils.dart index d39970db8..b063843cf 100644 --- a/lib/utils/app_info_utils.dart +++ b/lib/utils/app_info_utils.dart @@ -3,18 +3,20 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'version.dart'; + +import '../model/version.dart'; class AppInfoUtils { static bool isInitialized = false; static final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin(); - static final packageInfo = PackageInfo.fromPlatform(); + static final _packageInfo = PackageInfo.fromPlatform(); static Future init() async { - _appName = (await packageInfo).appName; - _packageName = (await packageInfo).packageName; - _appVersion = Version.parse((await packageInfo).version); - _appBuildNumber = (await packageInfo).buildNumber; + final packageInfo = await _packageInfo; + _appName = packageInfo.appName; + _packageName = packageInfo.packageName; + _appVersion = Version.parse(packageInfo.version); + _appBuildNumber = packageInfo.buildNumber; _androidInfo = !kIsWeb && Platform.isAndroid ? await _deviceInfo.androidInfo : null; _iosInfo = !kIsWeb && Platform.isIOS ? await _deviceInfo.iosInfo : null; @@ -80,15 +82,7 @@ class AppInfoUtils { '\nsupportedAbis: ${androidInfo!.supportedAbis}' '\ntags: ${androidInfo!.tags}' '\ntype: ${androidInfo!.type}' - '\nisPhysicalDevice: ${androidInfo!.isPhysicalDevice}' - // '\ndisplaySizeInches: ${((androidInfo!.displayMetrics.sizeInches * 10).roundToDouble() / 10)}' - // '\ndisplayWidthPixels: ${androidInfo!.displayMetrics.widthPx}' - // '\ndisplayWidthInches: ${androidInfo!.displayMetrics.widthInches}' - // '\ndisplayHeightPixels: ${androidInfo!.displayMetrics.heightPx}' - // '\ndisplayHeightInches: ${androidInfo!.displayMetrics.heightInches}' - // '\ndisplayXDpi: ${androidInfo!.displayMetrics.xDpi}' - // '\ndisplayYDpi: ${androidInfo!.displayMetrics.yDpi}' - '\nserialNumber: ${androidInfo!.serialNumber}'; + '\nisPhysicalDevice: ${androidInfo!.isPhysicalDevice}'; static IosDeviceInfo? get iosInfo => isInitialized ? _iosInfo : throw Exception('AppInfoUtils not initialized'); static late final IosDeviceInfo? _iosInfo; diff --git a/lib/utils/crypto_utils.dart b/lib/utils/crypto_utils.dart index 9fccbf852..5915edd25 100644 --- a/lib/utils/crypto_utils.dart +++ b/lib/utils/crypto_utils.dart @@ -22,11 +22,10 @@ import 'dart:convert'; import 'dart:core'; import 'dart:math' as math; -import 'package:base32/base32.dart' as base32_converter; import 'package:base32/base32.dart'; import 'package:flutter/foundation.dart'; -import 'package:hex/hex.dart' as hex_converter; import 'package:pointycastle/export.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import '../model/enums/encodings.dart'; @@ -42,7 +41,7 @@ Future pbkdf2({required Uint8List salt, required int iterations, requ map['keyLength'] = keyLength; // Funky converting of password because that is what the server does too. - map['password'] = utf8.encode(encodeSecretAs(password, Encodings.hex)); + map['password'] = utf8.encode(Encodings.hex.encode(password)); return compute(_pbkdfIsolate, map); } @@ -88,24 +87,3 @@ SecureRandom secureRandom() { return secureRandom; } - -Uint8List decodeSecretToUint8(String secret, Encodings encoding) => switch (encoding) { - Encodings.none => Uint8List.fromList(utf8.encode(secret)), - Encodings.hex => Uint8List.fromList(hex_converter.HEX.decode(secret)), - Encodings.base32 => base32_converter.base32.decode(secret), - }; - -String encodeSecretAs(Uint8List secret, Encodings encoding) => switch (encoding) { - Encodings.none => utf8.decode(secret), - Encodings.hex => hex_converter.HEX.encode(secret), - Encodings.base32 => base32_converter.base32.encode(secret), - }; - -bool isValidEncoding(String secret, Encodings encoding) { - try { - decodeSecretToUint8(secret, encoding); - } catch (_) { - return false; - } - return true; -} diff --git a/lib/utils/custom_int_buffer.dart b/lib/utils/custom_int_buffer.dart index 6461741a1..8e9290847 100644 --- a/lib/utils/custom_int_buffer.dart +++ b/lib/utils/custom_int_buffer.dart @@ -1,46 +1,50 @@ +import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; part 'custom_int_buffer.g.dart'; @JsonSerializable() class CustomIntBuffer { - final int maxSize = 30; - - CustomIntBuffer(); - - List? _list; - - // The get and set methods are needed for serialization. - List get list { - _list ??= []; - return _list!; + final int maxSize; + final List _list; + const CustomIntBuffer({this.maxSize = 100, List list = const []}) : _list = list; + + CustomIntBuffer copyWith({int? maxSize, List? list}) { + return CustomIntBuffer( + maxSize: maxSize ?? this.maxSize, + list: list ?? _list, + ); } - set list(List l) { - if (_list != null) { - throw ArgumentError('Initializing [list] in [CustomStringBuffer] is only allowed once.'); - } - - if (l.length > maxSize) { - throw ArgumentError('The list $l is to long for a buffer of size $maxSize'); - } - - _list = l; + List toList() => _list.toList(); + CustomIntBuffer put(int value) { + final newList = _list.toList()..add(value); + if (newList.length > maxSize) newList.removeAt(0); + return CustomIntBuffer(maxSize: maxSize, list: newList); } - void put(int value) { - if (list.length >= maxSize) list.removeAt(0); - list.add(value); + CustomIntBuffer putList(List values) { + final newList = _list.toList()..addAll(values); + while (newList.length > maxSize) { + newList.removeAt(0); + } + return CustomIntBuffer(maxSize: maxSize, list: newList); } - int get length => list.length; + int get length => _list.length; + @override + String toString() => 'CustomIntBuffer{maxSize: $maxSize, _list: $_list}'; + bool contains(int value) => _list.contains(value); @override - String toString() => list.toString(); + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is CustomIntBuffer && listEquals(other._list, _list); + } - bool contains(int value) => list.contains(value); + @override + int get hashCode => Object.hashAll([maxSize, ..._list]); factory CustomIntBuffer.fromJson(Map json) => _$CustomIntBufferFromJson(json); - Map toJson() => _$CustomIntBufferToJson(this); } diff --git a/lib/utils/custom_int_buffer.g.dart b/lib/utils/custom_int_buffer.g.dart index 0c9f250c5..d38cf3d06 100644 --- a/lib/utils/custom_int_buffer.g.dart +++ b/lib/utils/custom_int_buffer.g.dart @@ -7,10 +7,11 @@ part of 'custom_int_buffer.dart'; // ************************************************************************** CustomIntBuffer _$CustomIntBufferFromJson(Map json) => - CustomIntBuffer() - ..list = (json['list'] as List).map((e) => e as int).toList(); + CustomIntBuffer( + maxSize: json['maxSize'] as int? ?? 100, + ); Map _$CustomIntBufferToJson(CustomIntBuffer instance) => { - 'list': instance.list, + 'maxSize': instance.maxSize, }; diff --git a/lib/utils/customization/action_theme.dart b/lib/utils/customization/action_theme.dart new file mode 100644 index 000000000..589b64856 --- /dev/null +++ b/lib/utils/customization/action_theme.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class ActionTheme extends ThemeExtension { + final Color deleteColor; + final Color editColor; + final Color lockColor; + final Color foregroundColor; + const ActionTheme({ + required this.deleteColor, + required this.editColor, + required this.lockColor, + required this.foregroundColor, + }); + + @override + ThemeExtension lerp(covariant ActionTheme? other, double t) => ActionTheme( + deleteColor: Color.lerp(deleteColor, other?.deleteColor, t) ?? deleteColor, + editColor: Color.lerp(editColor, other?.editColor, t) ?? editColor, + lockColor: Color.lerp(lockColor, other?.lockColor, t) ?? lockColor, + foregroundColor: Color.lerp(foregroundColor, other?.foregroundColor, t) ?? foregroundColor, + ); + + @override + ThemeExtension copyWith({Color? deleteColor, Color? editColor, Color? lockColor, Color? foregroundColor}) => ActionTheme( + deleteColor: deleteColor ?? this.deleteColor, + editColor: editColor ?? this.editColor, + lockColor: lockColor ?? this.lockColor, + foregroundColor: foregroundColor ?? this.foregroundColor, + ); +} diff --git a/lib/utils/app_customizer.dart b/lib/utils/customization/application_customization.dart similarity index 75% rename from lib/utils/app_customizer.dart rename to lib/utils/customization/application_customization.dart index 18d2e9cb7..ee57f2f16 100644 --- a/lib/utils/app_customizer.dart +++ b/lib/utils/customization/application_customization.dart @@ -1,426 +1,10 @@ import 'dart:convert'; -import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:privacyidea_authenticator/utils/customization/theme_customization.dart'; -import '../model/enums/app_feature.dart'; - -class ThemeCustomization { - static const ThemeCustomization defaultLightTheme = ThemeCustomization.defaultLightWith(); - static const ThemeCustomization defaultDarkTheme = ThemeCustomization.defaultDarkWith(); - - const ThemeCustomization({ - required this.brightness, - required this.primaryColor, - required this.onPrimary, - required this.subtitleColor, - required this.backgroundColor, - required this.foregroundColor, - required this.shadowColor, - required this.deleteColor, - required this.renameColor, - required this.lockColor, - required this.tileIconColor, - required this.navigationBarColor, - Color? actionButtonsForegroundColor, - Color? tilePrimaryColor, - Color? tileSubtitleColor, - Color? navigationBarIconColor, - Color? qrButtonBackgroundColor, - Color? qrButtonIconColor, - }) : _actionButtonsForegroundColor = actionButtonsForegroundColor, - _tilePrimaryColor = tilePrimaryColor, - _tileSubtitleColor = tileSubtitleColor, - _navigationBarIconColor = navigationBarIconColor, - _qrButtonBackgroundColor = qrButtonBackgroundColor, - _qrButtonIconColor = qrButtonIconColor; - - const ThemeCustomization.defaultLightWith({ - Color? primaryColor, - Color? onPrimary, - Color? subtitleColor, - Color? backgroundColor, - Color? foregroundColor, - Color? shadowColor, - Color? deleteColor, - Color? renameColor, - Color? lockColor, - Color? tileIconColor, - Color? navigationBarColor, - // From here on the colors have a default value based on another given color - Color? actionButtonsForegroundColor, // Default: foregroundColor - Color? tilePrimaryColor, // Default: primaryColor - Color? tileSubtitleColor, // Default: subtitleColor - Color? navigationBarIconColor, // Default: foregroundColor - Color? qrButtonBackgroundColor, // Default: primaryColor - Color? qrButtonIconColor, // Default: onPrimary - }) : brightness = Brightness.light, - primaryColor = primaryColor ?? Colors.lightBlue, - onPrimary = onPrimary ?? const Color(0xff282828), - subtitleColor = subtitleColor ?? const Color(0xff9E9E9E), - backgroundColor = backgroundColor ?? const Color(0xffEFEFEF), - foregroundColor = foregroundColor ?? const Color(0xff282828), - shadowColor = shadowColor ?? const Color(0xff303030), - deleteColor = deleteColor ?? const Color(0xffE04D2D), - renameColor = renameColor ?? const Color(0xff6A8FE5), - lockColor = lockColor ?? const Color(0xffFFD633), - tileIconColor = tileIconColor ?? const Color(0xff757575), - navigationBarColor = navigationBarColor ?? Colors.white, - // From here on the colors have a default value based on another given color - _actionButtonsForegroundColor = actionButtonsForegroundColor, - _tilePrimaryColor = tilePrimaryColor, - _tileSubtitleColor = tileSubtitleColor, - _navigationBarIconColor = navigationBarIconColor, - _qrButtonBackgroundColor = qrButtonBackgroundColor, - _qrButtonIconColor = qrButtonIconColor; - - const ThemeCustomization.defaultDarkWith({ - Color? primaryColor, - Color? onPrimary, - Color? subtitleColor, - Color? backgroundColor, - Color? foregroundColor, - Color? shadowColor, - Color? deleteColor, - Color? renameColor, - Color? lockColor, - Color? tileIconColor, - Color? navigationBarColor, - // From here on the colors have a default value based on another given color - Color? actionButtonsForegroundColor, // Default: foregroundColor - Color? tilePrimaryColor, // Default: primaryColor - Color? tileSubtitleColor, // Default: subtitleColor - Color? navigationBarIconColor, // Default: foregroundColor - Color? qrButtonBackgroundColor, // Default: primaryColor - Color? qrButtonIconColor, // Default: onPrimary - }) : brightness = Brightness.dark, - primaryColor = primaryColor ?? Colors.lightBlue, - onPrimary = onPrimary ?? const Color(0xFF282828), - subtitleColor = subtitleColor ?? const Color(0xFF9E9E9E), - backgroundColor = backgroundColor ?? const Color(0xFF303030), - foregroundColor = foregroundColor ?? const Color(0xffF5F5F5), - shadowColor = shadowColor ?? const Color(0xFFEFEFEF), - deleteColor = deleteColor ?? const Color(0xffCD3C14), - renameColor = renameColor ?? const Color(0xff527EDB), - lockColor = lockColor ?? const Color(0xffFFCC00), - tileIconColor = tileIconColor ?? const Color(0xffF5F5F5), - navigationBarColor = navigationBarColor ?? const Color(0xFF282828), - // From here on the colors have a default value based on another given color - _actionButtonsForegroundColor = actionButtonsForegroundColor, - _tilePrimaryColor = tilePrimaryColor, - _tileSubtitleColor = tileSubtitleColor, - _navigationBarIconColor = navigationBarIconColor, - _qrButtonBackgroundColor = qrButtonBackgroundColor, - _qrButtonIconColor = qrButtonIconColor; - - final Brightness brightness; - - // Basic colors - final Color primaryColor; - final Color onPrimary; - final Color subtitleColor; - final Color backgroundColor; - final Color foregroundColor; - final Color shadowColor; - - // Slide action - final Color deleteColor; - final Color renameColor; - final Color lockColor; - final Color? _actionButtonsForegroundColor; // Default: foregroundColor - Color get actionButtonsForegroundColor => _actionButtonsForegroundColor ?? foregroundColor; - - // List tile - final Color? _tilePrimaryColor; // Default: primaryColor - Color get tilePrimaryColor => _tilePrimaryColor ?? primaryColor; - final Color tileIconColor; - final Color? _tileSubtitleColor; // Default: subtitleColor - Color get tileSubtitleColor => _tileSubtitleColor ?? subtitleColor; - - // Navigation bar - final Color navigationBarColor; - final Color? _navigationBarIconColor; // Default: foregroundColor - Color get navigationBarIconColor => _navigationBarIconColor ?? foregroundColor; - final Color? _qrButtonBackgroundColor; // Default: primaryColor - Color get qrButtonBackgroundColor => _qrButtonBackgroundColor ?? primaryColor; - final Color? _qrButtonIconColor; // Default: onPrimary - Color get qrButtonIconColor => _qrButtonIconColor ?? onPrimary; - - ThemeCustomization copyWith({ - Brightness? brightness, - Color? primaryColor, - Color? onPrimary, - Color? subtitleColor, - Color? backgroundColor, - Color? foregroundColor, - Color? shadowColor, - Color? deleteColor, - Color? renameColor, - Color? lockColor, - Color? tileIconColor, - Color? navigationBarColor, - // From here on the colors have a default value based on another given color - Color? Function()? actionButtonsForegroundColor, // Default: foregroundColor - Color? Function()? tilePrimaryColor, // Default: primaryColor - Color? Function()? tileSubtitleColor, // Default: subtitleColor - Color? Function()? navigationBarIconColor, // Default: foregroundColor - Color? Function()? qrButtonBackgroundColor, // Default: primaryColor - Color? Function()? qrButtonIconColor, // Default: onPrimary - }) => - ThemeCustomization( - brightness: brightness ?? this.brightness, - primaryColor: primaryColor ?? this.primaryColor, - onPrimary: onPrimary ?? this.onPrimary, - subtitleColor: subtitleColor ?? this.subtitleColor, - backgroundColor: backgroundColor ?? this.backgroundColor, - foregroundColor: foregroundColor ?? this.foregroundColor, - shadowColor: shadowColor ?? this.shadowColor, - deleteColor: deleteColor ?? this.deleteColor, - renameColor: renameColor ?? this.renameColor, - lockColor: lockColor ?? this.lockColor, - actionButtonsForegroundColor: actionButtonsForegroundColor != null ? actionButtonsForegroundColor() : this.actionButtonsForegroundColor, - tilePrimaryColor: tilePrimaryColor != null ? tilePrimaryColor() : this.tilePrimaryColor, - tileIconColor: tileIconColor ?? this.tileIconColor, - tileSubtitleColor: tileSubtitleColor != null ? tileSubtitleColor() : this.tileSubtitleColor, - navigationBarColor: navigationBarColor ?? this.navigationBarColor, - navigationBarIconColor: navigationBarIconColor != null ? navigationBarIconColor() : this.navigationBarIconColor, - qrButtonBackgroundColor: qrButtonBackgroundColor != null ? qrButtonBackgroundColor() : this.qrButtonBackgroundColor, - qrButtonIconColor: qrButtonIconColor != null ? qrButtonIconColor() : this.qrButtonIconColor, - ); - - factory ThemeCustomization.fromJson(Map json) { - bool isLightTheme = json['brightness'] == 'light'; - bool isDarkTheme = json['brightness'] == 'dark'; - if (json['brightness'] == null && json['primaryColor'] != null) { - isLightTheme = _isColorBright(Color(json['primaryColor'] as int)); - } - if (isLightTheme) { - return ThemeCustomization.defaultLightWith( - primaryColor: json['primaryColor'] != null ? Color(json['primaryColor'] as int) : null, - onPrimary: json['onPrimary'] != null ? Color(json['onPrimary'] as int) : null, - subtitleColor: json['subtitleColor'] != null ? Color(json['subtitleColor'] as int) : null, - backgroundColor: json['backgroundColor'] != null ? Color(json['backgroundColor'] as int) : null, - foregroundColor: json['foregroundColor'] != null ? Color(json['foregroundColor'] as int) : null, - shadowColor: json['shadowColor'] != null ? Color(json['shadowColor'] as int) : null, - deleteColor: json['deleteColor'] != null ? Color(json['deleteColor'] as int) : null, - renameColor: json['renameColor'] != null ? Color(json['renameColor'] as int) : null, - lockColor: json['lockColor'] != null ? Color(json['lockColor'] as int) : null, - actionButtonsForegroundColor: json['actionButtonsForegroundColor'] != null ? Color(json['actionButtonsForegroundColor'] as int) : null, - tilePrimaryColor: json['tilePrimaryColor'] != null ? Color(json['tilePrimaryColor'] as int) : null, - tileIconColor: json['tileIconColor'] != null ? Color(json['tileIconColor'] as int) : null, - tileSubtitleColor: json['tileSubtitleColor'] != null ? Color(json['tileSubtitleColor'] as int) : null, - navigationBarColor: json['navigationBarColor'] != null ? Color(json['navigationBarColor'] as int) : null, - navigationBarIconColor: json['navigationBarIconColor'] != null ? Color(json['navigationBarIconColor'] as int) : null, - qrButtonBackgroundColor: json['qrButtonBackgroundColor'] != null ? Color(json['qrButtonBackgroundColor'] as int) : null, - qrButtonIconColor: json['qrButtonIconColor'] != null ? Color(json['qrButtonIconColor'] as int) : null, - ); - } - if (isDarkTheme) { - return ThemeCustomization.defaultDarkWith( - primaryColor: json['primaryColor'] != null ? Color(json['primaryColor'] as int) : null, - onPrimary: json['onPrimary'] != null ? Color(json['onPrimary'] as int) : null, - subtitleColor: json['subtitleColor'] != null ? Color(json['subtitleColor'] as int) : null, - backgroundColor: json['backgroundColor'] != null ? Color(json['backgroundColor'] as int) : null, - foregroundColor: json['foregroundColor'] != null ? Color(json['foregroundColor'] as int) : null, - shadowColor: json['shadowColor'] != null ? Color(json['shadowColor'] as int) : null, - deleteColor: json['deleteColor'] != null ? Color(json['deleteColor'] as int) : null, - renameColor: json['renameColor'] != null ? Color(json['renameColor'] as int) : null, - lockColor: json['lockColor'] != null ? Color(json['lockColor'] as int) : null, - actionButtonsForegroundColor: json['actionButtonsForegroundColor'] != null ? Color(json['actionButtonsForegroundColor'] as int) : null, - tilePrimaryColor: json['tilePrimaryColor'] != null ? Color(json['tilePrimaryColor'] as int) : null, - tileIconColor: json['tileIconColor'] != null ? Color(json['tileIconColor'] as int) : null, - tileSubtitleColor: json['tileSubtitleColor'] != null ? Color(json['tileSubtitleColor'] as int) : null, - navigationBarColor: json['navigationBarColor'] != null ? Color(json['navigationBarColor'] as int) : null, - navigationBarIconColor: json['navigationBarIconColor'] != null ? Color(json['navigationBarIconColor'] as int) : null, - qrButtonBackgroundColor: json['qrButtonBackgroundColor'] != null ? Color(json['qrButtonBackgroundColor'] as int) : null, - qrButtonIconColor: json['qrButtonIconColor'] != null ? Color(json['qrButtonIconColor'] as int) : null, - ); - } - throw Exception('Invalid brightness value: ${json['brightness']}'); - } - - Map toJson() => { - 'brightness': brightness == Brightness.light ? 'light' : 'dark', - 'primaryColor': primaryColor.value, - 'onPrimary': onPrimary.value, - 'subtitleColor': subtitleColor.value, - 'backgroundColor': backgroundColor.value, - 'foregroundColor': foregroundColor.value, - 'shadowColor': shadowColor.value, - 'deleteColor': deleteColor.value, - 'renameColor': renameColor.value, - 'lockColor': lockColor.value, - 'tileIconColor': tileIconColor.value, - 'navigationBarColor': navigationBarColor.value, - '_actionButtonsForegroundColor': _actionButtonsForegroundColor?.value, - '_tilePrimaryColor': _tilePrimaryColor?.value, - '_tileSubtitleColor': _tileSubtitleColor?.value, - '_navigationBarIconColor': _navigationBarIconColor?.value, - '_qrButtonBackgroundColor': _qrButtonBackgroundColor?.value, - }; - - ThemeData generateTheme() => ThemeData( - useMaterial3: false, - brightness: brightness, - primaryColor: primaryColor, - canvasColor: backgroundColor, - textTheme: const TextTheme().copyWith( - bodyLarge: TextStyle(color: foregroundColor), - bodyMedium: TextStyle(color: foregroundColor), - titleMedium: TextStyle(color: foregroundColor), - titleSmall: TextStyle(color: foregroundColor), - displayLarge: TextStyle(color: foregroundColor), - displayMedium: TextStyle(color: foregroundColor), - displaySmall: TextStyle(color: foregroundColor), - headlineMedium: TextStyle(color: foregroundColor), - headlineSmall: TextStyle(color: foregroundColor), - titleLarge: TextStyle(color: primaryColor), - bodySmall: TextStyle(color: subtitleColor), - labelLarge: TextStyle(color: foregroundColor), - labelSmall: TextStyle(color: foregroundColor), - ), - iconButtonTheme: IconButtonThemeData( - style: ButtonStyle( - foregroundColor: MaterialStateProperty.all(foregroundColor), - ), - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - foregroundColor: onPrimary, - backgroundColor: primaryColor, - padding: const EdgeInsets.all(6), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - shadowColor: shadowColor, - elevation: 1.5, - ), - ), - scaffoldBackgroundColor: backgroundColor, - cardColor: backgroundColor, - shadowColor: shadowColor, - // shadowColor: Colors.transparent, - appBarTheme: const AppBarTheme().copyWith( - backgroundColor: backgroundColor, - shadowColor: shadowColor, - foregroundColor: foregroundColor, - elevation: 0, - titleSpacing: 6, - ), - inputDecorationTheme: InputDecorationTheme( - labelStyle: TextStyle(color: foregroundColor), - hintStyle: TextStyle(color: primaryColor), - errorStyle: TextStyle(color: deleteColor), - border: UnderlineInputBorder( - borderSide: BorderSide(color: shadowColor), - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: subtitleColor), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: primaryColor), - ), - ), - primaryIconTheme: IconThemeData(color: onPrimary), - iconTheme: IconThemeData(color: foregroundColor), - navigationBarTheme: const NavigationBarThemeData().copyWith( - backgroundColor: navigationBarColor, - shadowColor: shadowColor, - iconTheme: MaterialStatePropertyAll(IconThemeData(color: navigationBarIconColor)), - elevation: 3, - ), - floatingActionButtonTheme: FloatingActionButtonThemeData( - backgroundColor: qrButtonBackgroundColor, - foregroundColor: qrButtonIconColor, - elevation: 0, - ), - textButtonTheme: TextButtonThemeData( - style: ButtonStyle( - overlayColor: MaterialStateColor.resolveWith((states) => foregroundColor.withOpacity(0.1)), - ), - ), - listTileTheme: ListTileThemeData( - tileColor: Colors.transparent, - titleTextStyle: TextStyle(color: tilePrimaryColor), - subtitleTextStyle: TextStyle(color: tileSubtitleColor), - iconColor: tileIconColor, - ), - colorScheme: brightness == Brightness.light - ? ColorScheme.light( - primary: primaryColor, - secondary: primaryColor, - onPrimary: onPrimary, - onSecondary: onPrimary, - error: deleteColor, - errorContainer: deleteColor, - ) - : ColorScheme.dark( - primary: primaryColor, - secondary: primaryColor, - onPrimary: onPrimary, - onSecondary: onPrimary, - error: deleteColor, - errorContainer: deleteColor, - ), - checkboxTheme: CheckboxThemeData( - checkColor: MaterialStateProperty.resolveWith((_) => onPrimary), - fillColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { - return null; - } - if (states.contains(MaterialState.selected)) { - return primaryColor; - } - return null; - }), - ), - radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { - return null; - } - if (states.contains(MaterialState.selected)) { - return primaryColor; - } - return null; - }), - ), - switchTheme: SwitchThemeData( - thumbColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { - return null; - } - if (states.contains(MaterialState.selected)) { - return primaryColor; - } - return null; - }), - trackColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { - return null; - } - if (states.contains(MaterialState.selected)) { - return primaryColor; - } - return null; - }), - ), - extensions: [ - ActionTheme( - deleteColor: deleteColor, - editColor: renameColor, - lockColor: lockColor, - foregroundColor: actionButtonsForegroundColor, - ), - ExtendedTextTheme( - tokenTile: TextStyle( - color: primaryColor, - ), - tokenTileSubtitle: TextStyle( - color: tileSubtitleColor, - ), - ), - ]); -} +import '../../model/enums/app_feature.dart'; class ApplicationCustomization { // Edit in android/app/src/main/AndroidManifest.xml file @@ -513,7 +97,7 @@ class ApplicationCustomization { appImageUint8List: json['appImageBASE64'] != null ? base64Decode(json['appImageBASE64'] as String) : null, lightTheme: ThemeCustomization.fromJson(json['lightTheme'] as Map), darkTheme: ThemeCustomization.fromJson(json['darkTheme'] as Map), - disabledFeatures: (json['disabledFeatures'] as List).map((e) => AppFeatureX.fromName(e as String)).toSet(), + disabledFeatures: (json['disabledFeatures'] as List).map((e) => AppFeature.values.byName(e as String)).toSet(), ); Map toJson() { @@ -529,76 +113,6 @@ class ApplicationCustomization { } } -class ExtendedTextTheme extends ThemeExtension { - final TextStyle tokenTile; - final TextStyle tokenTileSubtitle; - final String veilingCharacter; - - ExtendedTextTheme({ - this.veilingCharacter = '●', - TextStyle? tokenTile, - TextStyle? tokenTileSubtitle, - }) : tokenTile = const TextStyle( - fontFamily: 'monospace', - fontWeight: FontWeight.bold, - ).merge(tokenTile), - tokenTileSubtitle = const TextStyle( - fontFamily: 'monospace', - fontWeight: FontWeight.bold, - ).merge(tokenTileSubtitle); - - @override - ThemeExtension copyWith({ - TextStyle? otpTextStyle, - TextStyle? otpSubtitleTextStyle, - }) => - ExtendedTextTheme( - tokenTile: otpTextStyle ?? tokenTile, - tokenTileSubtitle: otpSubtitleTextStyle ?? tokenTileSubtitle, - ); - - @override - ThemeExtension lerp(ExtendedTextTheme? other, double t) => ExtendedTextTheme( - tokenTile: TextStyle.lerp(tokenTile, other?.tokenTile, t) ?? tokenTile, - ); -} - -class ActionTheme extends ThemeExtension { - final Color deleteColor; - final Color editColor; - final Color lockColor; - final Color foregroundColor; - const ActionTheme({ - required this.deleteColor, - required this.editColor, - required this.lockColor, - required this.foregroundColor, - }); - - @override - ThemeExtension lerp(covariant ActionTheme? other, double t) => ActionTheme( - deleteColor: Color.lerp(deleteColor, other?.deleteColor, t) ?? deleteColor, - editColor: Color.lerp(editColor, other?.editColor, t) ?? editColor, - lockColor: Color.lerp(lockColor, other?.lockColor, t) ?? lockColor, - foregroundColor: Color.lerp(foregroundColor, other?.foregroundColor, t) ?? foregroundColor, - ); - - @override - ThemeExtension copyWith({Color? deleteColor, Color? editColor, Color? lockColor, Color? foregroundColor}) => ActionTheme( - deleteColor: deleteColor ?? this.deleteColor, - editColor: editColor ?? this.editColor, - lockColor: lockColor ?? this.lockColor, - foregroundColor: foregroundColor ?? this.foregroundColor, - ); -} - -// /// Calculate HSP and check if the primary color is bright or dark -// /// brightness = sqrt( .299 R^2 + .587 G^2 + .114 B^2 ) -// /// c.f., http://alienryderflex.com/hsp.html -bool _isColorBright(Color color) { - return sqrt(0.299 * pow(color.red, 2) + 0.587 * pow(color.green, 2) + 0.114 * pow(color.blue, 2)) > 150; -} - final Uint8List defaultIconUint8List = base64Decode( "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADr8AAA6/ATgFUyQAAB7TSURBVHhe7X0JuF1Vefa79z77THeeM9yEYMKgEALEBhmMIFADiK1WqOPvw////WsLTyu/Px0stFpbq7Y+aC1K0TIIte2PUgUqRhEtgojBiAGChARIcpM7j2ee+777nHOb5E7nnnv2uecm933ud9fe65yz91rre9e3vrX22msZOI7Qff2tAQarKRsop1JOoZxMWUPpojRSvBSLMhMylAQlROmn7Ke8SnmZsoeyl3K457Yb4wyXPJY0AajwBgZnUN5EOZ+ykSJl11PcwgTlAGUX5acF2U1CRBguOSw5AlDpqxi8hXIF5QKKarhJWSzIYuyjPEF5hPJjkkGWY0lgSRCASm9hcDnlGoqU30GpVUj5j1H+v0KSQRajZlHTBKDiNzH4H5TfopykuCUG+Qv3U+4jEXY7MTWGmiNA9/WfZ5pyl/Hweso2ik/xSxxRykOUL5EIjzsxNYKaIgBr/FUMPkq5xIk4/pCjfJfyORLhB07MIqMmCEDFb2XwZ5RfdyJODDxI+WsS4Wf508XBohKAin8dg7+gfICymJ78YiFJuZPyVyTCISemylgUAlDxHgY3UG6mtCnuBMdhysdJgq/kT6uHqhOAyj+bwecp6s4t42hoHOFGEuGl/Kn7mG1ItOKg8v+Qwb0UDdMuYyo0dP3exi3bhid2bP9FPspdVMUCUPEah7+Nov78MkrDPZSP0BqM5U/dgesEoPI1Rn83ZbnWzx/PUj5EEui5gytw1fOm8j/I4HuUZeWXB/lLj7EcfzN/Wnm45gMw0fLwv0jR49dllI8g5Rr6BaP0Cyo+ZlDxJoCK1zX/nqJu3jIqCw0cqWJVDBW1AN3Xf179+7sov+NELKPS2EpL0EZLoO5iRVAxArDm2wy+RtGo3jLcw3kkwQqS4D8K5wtCRQhA5cuZVM1/vxOxDLfxRpKgvRKWoFK9APXx5fEvo3q4gRXvU4XjsrFgC8BE/CUDPcJdRvXxZlqCCVoCzUssCwvqBVD5/5tB1R9gLOMoaI7BtewdfCN/Oj+UTQAq/2IGaoP8TsQyFhMaLr6UJNiZPy0dZRGAytfU6x9TluI8veMVz1MuJgmG86elYd5OYPcNt8pvuINSlvLFuFg2h554Fj0JSiqLsUwOqVzOsWUnApRP5XeU+Vb+nXJgeSQYt4A2+UyKRl7nhXnfj7VfI1GfzJ/NHxNU/kVtdbji7A4MjSXQP5rGofEkDkwk8Hw8hUxG0+xZRKSmTao1mwZsw1iS04WylCSVOsY8p5UtRbDIPZYHZwZsrG3worvZh84WC+2NPvz7M33YORFHHfO8AFxPK/ClwvGcmNedqPyLGGgyY9nj+2L8h0/twtWXHkAo8SyVuw5Grh2ZVBMS8SBCERsj48DgWAaHR5M4OJHEy9EkhtJp8oIlKCZQWlhIfhLDWlBZVQasyIirRlPRjpIlholOjwcb6rxY0+jFymYvOpsttDQBDfUp+PwRWJ5x5MwhpLKvodl3Lv7lm/W4ZzCJbs+C6K73EC4iCZ7Ln86OkouPyq9joCnN5zoRZUIE+N1TunD51ufQH/4MRHbq0Qkt02Kt3wyvdRJscxXMXAey6RYkEnWIRHwYCxm0Gln0jqbQQ6vxWiSJV5IpFjhL3MhbjQYyok7X4kVLzlwJkNlOU8kRHoSlcSk5p4SbWO+1sa6etbnJixXNNjqaTTQ3ZlEXTMLrD8O0xpAxBpDOHkYysx+pzA5k+HvxhZd0LrWq7k/xwP0NuLc3ju6W1vwH5eOHlMtJAtmdWVFyGZEA6u/fkj8rH/9NgBcwGPkbR/FHwsm2CsY5c8rXqeW2dQblZBJjNSy955ltRSrZiGjUh4mQheHxHPqc5iSB/aGk05ywPeEVeCVew8vmpIk3m6050T0lap/HqZ3kEWbbsCxs9Mts+7Catbmr2YO2ZgNNDRkEAnHY3hDvM4wM+lmjD/G3r9Ls755UdP4q+X/HFro+X1H3ZyRAPe59qQerOlbADDYwMYUflgdNJvlC4XhGHJuWaUHl67n0kxQ9mlwQ5iLATHCKgv+KRSKrYVGTHnMtibGBCl4Lj4hRaE7i8QBCITYnNIgDY2k2JykcDCXwWiyFfjXIjlaOLGBekGxbRfO7lu3zGrbJq1ibO6nootn2+6OwbJltKjrXRyUfYG1+mTW7x1F0UV9OlvivxKwdQ4BD6CRbPS1dMP0s7vJJoN7AFpLglfzp9CgpjSTAwwz00saCUS4BZoPKqFhMxebEYwZJjE0UEsRcyRaiE7l0M5LJAJsUG6mUiWTaw9Yj51gZrycDrzdLSbE2R2B6xmjhB1mbewtm+xdUcnLSbDv3KtxvoZhCAJsJUnPY2gXD618ICe4lAfRq3YyYM/lU/jsZPJA/WzjcIMBMULFNlh3vo3upOTHNLoYdVF4jo01+L8PvjbNWD1LJg45CJEVWSckuJnNaC6CEGx7bsQSGTZ+7PBKoAdMA0Y/yp1MxU3PogMrXe3kVnYBQTUhpUrojimAZylSn0v2IpZ5HNPkTRJJPMHyK57sZT+WryPg9fb/4W12n6iDrcukU0mMDyGXYAyrP1Cgbt3Tf4EzSmRazEoDQ490Fef21CpXIsVJzEAlSSWTGBlmX5ZGWlcq3ktDvKBxPwYwEYO2Xw3dj/mwZiwaSIJuIIj0un07mqSz8P+pzWl3PZgHeTdHwYsVhMlOGyXaXx8X2VscnEo7Nu2lmnXKZFoaJbCyEzMRoIWLe0ACeVlSZgmnnA5Atmtv3ZR06ERVEmM7M+oAXm05aiaB3E+p858Bnr4dltTKjaRZGyPF3HCn8przmrzZQzIeTF+ZDPQ7b6obfPhf13ovQ4L0MjfZVMJLr8eTOIewJx6YfClZzkIw7FzB9Wgtr3mid2LH9nwvHk5i2aEmAtzHQe+yuIMnSWMluzslWjH1t9t47/GhttdHI/nagLgmPN0xqjiINDZMeYhfsNcqvnCFXjQYLTsL5b9oMVBmOcpU2hUxQ0Xm0rddT1jmjmh6tapNpRjpRj1jEi4lxdtRHUugfSuDwUBR7hkI4lIzBP53yj0IOnqZ2mMFGHjp3LBX0JHEhewRHTS2f9m4kgNa30Xo8riFFGU+lEB8aoFnQAls0Rh4PTgn6cHJLEGs6guhqD6C9zUZzk4FgQxq2PwrDYnfNGEJ6ciBG/fOsY0rzGmCmmKu5irEcOLc48h4Uj8mumrUJXmsNbGMlzFw7vfcmpOIBRMIejI/lMDicpKLjODgUwSujMeyLJqgO6sPkxWSDPSaa2D/1zcPUeZo7YQbq5kuC20mA3yscO5hyRypfa+xpYoGWYHMXyjC92/TYELLxMHJs6zTerieGmTQzpiov55fWoivgw6lNAaxpC2JlRwCdbT40t5iob8jCF4jD9LLpMIaRymkotofE2IN0tt/p1k0qTTJHGTtf5b/CT5zvy2x7zJWszaewNndT0eybZ1uRTTUgEfUjHDIxOprFwHACvYNRHBxmjR6PYSCedPLnKJkK9ngMNLKGL/w5BVPHsvK0rGBzMK+Boj7K2STB5CpmU9JBAtzE4LP5s2qASaBdT08MIxudcDI2HcSFKDMaU0feIYdiWQheG5saAlhLYqymxehs96Ot1YOGxhz8RzUnGtVTc3KIFmMfLUZ8UsmCCsI066mck6no1c7ooQedvE8L0sk6xB2zbWBkJI3+4TgOUdH7R6L4ZSiOrB5IyUN3arOBgGUiSCVr0Mk1SOmWB3brivkOFP1PEkAzuB0clUQqX1nQmP95TkSV4ZAgIhKUXnJyCRK0GCFVdRFDLZ2e0rE5Ob3Oj3WttBoddVhBYrS22CSGAX8gC4+docLVE1FtzNFSWEglLcRjJkITWQw57XMcPWyfXxuN4qVIwWzrqaNcZJpt1WYvZbaulKug0qV8WQKD+S2RBN8lASZ7BMcSQIM+mmGqlzwWBZnwKDKhsXmRYDqoKNSc6KleNi1yMEJsYXPCRhsrbQ/qWFNljjP8XpiWpS/FL6VoWnL/bbatgtn2LNhsuwSRgL0Cu4XWylCi5ySBlsA9hyTQ4pZTyCtmLJryBau+BVbjgp+HO8rSo992KrnTR2NeR2lQaKDDl0XSSGI4y3Y6E3XCFM8V31lv5L8XpPgstPH3uk5NKl9g2nKJGNLjQywzMXxOyLfTMnwOJgnQff0XlEd1/xYdVn0zLHZ18lgYEaaDMqr2WYotis5rVslzgenPxsJIh0YKEXPiykJ4pAXIasUuPfdffLD2W3WN7OpoRVgmcYHW4ISARgvpP2XCmiE+J5W3sLlnm3HENxnxPgZTRorcxlg6gzCl3nHHjgGZnY5FkBztY7PsuP3HDUTpHP2RoM+Lekttd4WgykPrqQo0R8XZRj9g+5EE0NDvh/Nn7sPx3pNJbO3uwLr162EESQF578eafJIAGXaz0lpS73gBHc9sBqGhAezb9Qs8fngYHUF/RZsgWU8zUD8bCbQ24S3OPal8dWzk/W/WudtQkjLpFK44/zx0bdqCCduP+Gz+SyVLpoagqdUd0RGMPP4gbnvsKTqeZY3xTwOWMJsEW5NJ9NxgehI8SgJcXiSAXvLQgkTNOncbkWQKW88+C10XvQ1hpk0josepjmeFUxHo4mw249j7wFdw+89eRKe/QivqSOnOQJFmFPmmI0EP5eyiE6hFnKqifPW5uwM2Vm4811lC2zpBlS8o3xYt3wvw4w1bfx0nmVlnxLMicJrONNKjg87MIuf8aKygnFwkgLZdqQqi2SzWtLXAV99UYrf1+IbUEqN/G2lahTNWtWO8ks4ulZ6j75TWjCLnukeRQM3+6UUCvL4Qug49tfPYHufBzzLyUBM4Ahsef8CxkBWFSJCMzzRQ9PqiFtYXQtfhcLDCeTweIL27ViwkgZ626lnLMTjFpANY3GptGYuJKU10haGBomgImZCmlU3ebIMsgJy/Vud0Gcc3aAk0UpiJjDvHxOoiAdyf/LGM2VGtZlEkCI04zw543CQCaEu2ml3mJasULmHJSXg4G5zKSNH33G4J8sg5TmE2FvEa9AH0ZKgiiw6Wgol0BhevXYF1V/42htkTUTlNBxWGl6XRHu5HMhZlIVWnaCqJHD07rz+IoYYuZyLssTnQubKlUdDXZeN4+Z8/h2/s60Une0nugwkyLYgA1/Ls3/KR7mM+BGiwWDjfuw/f+d6jaAgu+MXkqiMUjWHbpRej7ooPYYLd8CIBilxOpIHhGPCjCHBTMI5DD30OX/sVCeCtBgHyUPnX9CpfaSax3/Rh1LSXnPRbXqTofYvMgmZ863giAeyhM/7oAGUs/5yr+J1qQwSoHt3KBQtOU7eWmhSMvPOXopnvY03fOQh8i/JT+mDDGpcpfm2RMJMFXkaFUMcSDoWAh/uB7SPAbm1MLyyi0o/EMgFcRgv9GCsORDQpdZFr+3QQAZYfybgIte2TrUENQgQoGqVlnIAQAfRi3jJOUIgAWlhwGScoRAA9HjqeZlwuo3RoRppDgOVm4ESEYYRFAL1JIFnGCQbDtPpEAL0sqPfGl3GiwfK8YvbcdqO6qq/mY5ZxIsGw7L2yAELV9qtfRo1Ai03Z3heLBNB2IzWHxXpCdvwjp/Zf6xBPEuBXlFj+sDqYS7n6XK9s+7V5wlJlQk4LUZv51WxqCSxPw2OPmIH6ySZgP+Vg/tBd6IYprdpRglZFgPaGOuQX8F2CIAFsnx/REghs5LJIaf2jajwzMAwSwLevYdPWXocAdAQ1DvBLHbsNTYrQi6HGHEpVOcRZcK3teo19qZqALPz1LRguwQR4chnnbenJ6UJuglbJ8Hqf2X3dmc5AUBFPFEJXkWUGPckEOo0UjwuR00AJc6ZRNa/Bea1BxPVK0RKCUrvaa8LbsgIvFR8FzwR+ZudSiMQS81orsCzQKmkZetP2Ofo+kgBaHUxJdRVeZnA8GkdjJj5tmSjOsRIswQHapYNmF87fdAYm4olZy7DWMJTOYvPqNsTrVqF3LgvAjNnpKIbDUQSUeZdh2L6w4Q04K4YeSYDdlD35Q/eg9Xj2x5IwouNgBZmEiC/RPj2HQ8DTg8BjI8C3whbWnnMJ3hDQi6VLwwpIhblYDJs2bcaeXNOcLdhai4qIj+FAJOmUj6vg9U3bv6vtsvc5W8lMqoB+gHoB2m3KVSh7PekcIkMDaGbGtSiI4qJs/l4ZYwIGgO8z3KvHU/xgP23Sc/5T8f6r345QOFR7HvU06Gftv2RFPZpOuxD36R342XRKcpzmIfFH+zCQmmaZnIoib/4N2/v9Z9/e7DhhR1oAQXsDuQ/LwoEDB1GXzWCItHueNf0h1vgnWPP7lSyVQqEkVCHuYnxow9tw8zsuxfBEyNlhsxahJIdppZqSEVx19W/hx7mVJc232mBnMXSYFdJyeX4ui419/4zp9WvPZwfHEuApyqy7TFUCnbaF5w4cRnBkFN8dBnayliSk02no76ia8Z8et5E5+xp88revRl0yioGk6+7KvKCk9zNNXZkYPvaB9+LlzgvxbeZrziadn6/OjWPP3n3wel1eotEx/75d3hXrflGIcdbDnMTEju2Jxi3btFi0q0vFasr0C9EELlvZCG9DN/bO5SUX8ETcwqmrTsc7zlyD5rED+NmhYURMy1nxczExkM4gQgfumg2dePe1H8Lz7Rfgn0LsazNPc9mqc73ApvEX8JUfPYFmn6+UYigTMv9ercF4+/7PXPdYIfJoAggkgGYIXUdxLy1EjManPT6Oc049HT+Ms10q5W78zs8TNLOBlbho42ZcdlIzWiID2Nk/jHAqiyT7t1pv39WEE7LqI5kswuyZRFNxvHNtOz545TasvPBafNs4CQ+x5s+lfCeN/MJ76jMYe+ZB/LRnGPXaLNkt8F6mPxjz1LfcOPH0d9jg5jGlrLrzC0arj/gmJ8JF9CQS+MxVl+I/G9+I75RQaIIS7LgAPLiaPYNfs0PwD7+E/r278OKel/DLvlEc0ot4JttTjwU/rYO6VrI6+u2UDE+DYhrU6dDmz+FsFjlnlXKaqlwG6/wWzlzRitM2nIKu9Wch3nYKdmaa8C05fLo1b1JC049uJvFGdrzu+MdbMWj4tNC4izDgaWr7zuE7bzlq/8dpb0kS/C6D2/Nn7mGUhXpJiw9vufK9uGlC6wMXPigVhe+f4wO2+HLozo3BF+5FfPggRvt7MDTYj4GRMQyG2MeOpzBG71yLR+d/N13W+QGjG6hB7UHc4rXQGvChrbEe7a2tzqhkY/sqeFtXIx7sQo/RhGcSBnaqx6JrlqhANVhKxh83pxH9zzvwxZ88h07exz3I/JNgLZ3XHLr9pm8UIh3MRIA2BnIU1jgRLkE3P5hI4o9/7TRkz3g7/nbUKskKTIuCAlbTfm1ku7rGyqHVSLCnEYU3FQFSrJ6UbDKObDqJjPbi02YOrOFar8hkz8SkF27SctBLpvWgeIPIeAJIWEGEDT+GcjYO8me7UsgP785D6UXo67JgVwSBS0efwsfuvBONgbop3nhlkYMZaHgh8LqNW179y/fITk1ixuSTBBXZLHouyFSOJhP4+LaL8ULbefgaPRDt0FGKCZ0RRzJIOaS08JodlGYe67VzW8KPVPDyQSXshjtbDoxTxpiAPl1HcmxidM0yoJ9J+d288Ue9PXj43s/jByNJdC5su/i5QYJbDS0f6b3rz6dsJj1jVkiAtQy0dYysgavQ3vudRga/c9U2/CRwJu5nv1+LiKnsXcN0Fy9TsaWgqHy53Z9tGMULD9+Oe547iK6A1918EobH3m93dG/u+YePTFklaka3k13CcfYItGb7hfkY96Dhz4GsgUOv7MVV6xqxoqETz+p9JZaaazrRhY8Vl6D6LeW30un7eMMYXvvBPfjqz/eiK+h3XfmCYXo+zdr//cLpUZjL9nyRMpA/dA8qhGY6XnvSJu54+Lt4/eBP8Ed0kKQUFZyLuqkK5PBdRJfiZn8f9jxyB7780xfRVReoivLpVL1GL+erhbMpmLXjSSswQSsg9/St+Rh3EaAlGKe6H3npVZyHw3hXdwe8nnrs0Z5MhBzEpQInqdIwq9iHG4E3R3bhhw98Ff+6p69qNV+g+f/YwL/93eOF0ymYlQACCbCLwbsorvsCgvbmqfN4sL1nBNbeHdjaksGbuzpZYF68Jk9NqHUiSLtM4zvqgP/lH4bnhYfwlfvvx9OhNLp87rf5RRiWZ4d3xUl/MPH0IzM+QyupKOkQvofBv+TPqgMlTOME4YEevH9dCzZdcBlinRvxTKoeD8X5YdEzrxUyFLXKKvXuAHCuOcY+7jN48vFH8R/7R9BMk6+5EFWDuraBum199/7V9kLMtCg5RSTBtxnMuA25a6AT0DPYy/ZoGNeevgZnnXs+7O6z0GN3YmfCwpMahFksMhyh9LdqTN+bxqpkL6L7n8XPdzyFB18dBPx+dHmsqtX6Ikxf4O6+r39aQ/qzYj4EOI2BnhZqXcEqgknMZZAaHUB/mP3DZBIXtAex+YzTsXrDWWyYXochu43Ngxe/oq/wvJqJCvXbHRyrOV7LZrv+Rnr0G9if7zZjaE4OIT24Dz0v78KO3XvwzBi7MFR8JxW/GDA83h6rueNNh//xjw4VombEvIqGJLiewT/kz6oIms6c1r4f6UMulUSMSgklWPWzKZzfWoc3nLwGq9auR13HWqChC1G7ERNGECM5jzNiN0IZIimG+LtRKXSm6lgojVaGjlDRWkqzjXpsMrNoRhIN2Qjs+BgyE32Y6D+AngOvYPeBXuwcZ7vk8aLJZ7s/r282mCasYOP7eu/5RElN9rxTShJ8i8Fv5M+qCJGAyk+P9h+1AYImi044Gz6m4DVz2Njgw9qOZnS2d6CppR3BxhZ4g00w/fTIbDbOlo2cxPCQB7ymLs3/WszNyKZhkGg5EgspbQkbQSoWRiI8jvDECEZHhtE3PIr9oyHsicnUkBleGy2s6a5P5SoRbPfv7rvvU3Oa/iLKIYCeD+hpoUYKqwuRIJlgc9CX3wBhmkJP0WeQ84g0P9d3tEa+kXN2526yTPYwTASoMB+P8z/XP20dm9+XOE6JpbOYYDjm7FfPzzUsqfFpK/90sU61bN4l5z4Mr2+3t6P7zQf//g9K3kCwrGyQBNph9CFK9Rs5ai2rnTJpCfIbIJSWhaLl18CSXkrRL3Xu/JOOGSgzemgsYui8tCvXAmi9LDtqNbRcdvifbpafVjLKUuDEju17G7ds02Dt5Bak1YQzsZFmPBs/6sHWrCgqVM/qNTdA4w3HiunIUlM+YVps9xtu6L3rL9RTmxfKrsEkwZMkwToeLspuo86W6cx4TiSg4k5YiLT+ulv77v3kpwox8wIbtgXh9yk/yB9WGbTl2h2zEhtNL2WYXv8DLVvfdVPhdN5YEAEK7xJoy1ntOVh9OCRoglWvly9OPBIYtu/Hdkf3dS/+n830dsvDQi2ASKCnhXpW4PpbRTPBamiFGWwgCeTanRigH7ST1u+ag1+4YUHL/C2YAAJJoCVmNDbg+jsFM8HT2Ma2UHvlHv8kMCzPLlq+3zx8x5+wK7QwVIQAAkmgRSY04/RlJ6LaYF/d09QOwxc8vpsD03rWDNRfffjOWyqynkPFCCAUSLCNUpW1Bo4GlW5Zzq7ZM+yVu/RhmE+ZvsCVvfd84kAhZsGoKAEEkkDNgEjwIyeimqDSaR7zJPDYxxcJDOMRevxX9d33172FmIqg4gQQSAKtO/h2yn1ORDUhEthekqDTsQjHBwmMu+D1vbPv63+jVV0rCtdHULqvv/VmBppiXt3RGqO8IeMaxMcHvvmFTxSOKw7Xx/Indmx/vHHLNr1kcjGlqhtUljNkXEPQ8r3XUfm35U/dgesEEEiCl0iCf+fh6RS9fVw1qDnQmnjZhEiwZKzAzynvovJdH2WtCgEEkmC08bxtX+dhmHI+xc2X4Y6CoVe92CTk2CQorHF8mfIBKr9inv5sWJTSoF9wFoO/o1zuRFQJ2jM3E57cOLnWoLUaP0rFfzN/Wh1UzQIcCVqDfjYJ6iFoMGMTRRtYuw72oekPZpBLxWuJBOqm6MWN91P5O5yYKmLRS4HWQCtB/l/KhylNinMV7BamxwbpGGr3bFd6wfPB05SbqfhH86fVR81UAxLhFAYiwgco9YpzB8wyrUBqbGAxfYJ9lM9S7qbyNbF90VAzBCiCRFBP4fcoesysl1MrDzmEzizjfjYHiWqSQA/NvkT5KhVfE7u01BwBiiARNPlUJPgg5QzFVRQiwTSzjF2CXq+7g/J1Kr7io3kLQc0SoAgSIcBAL6e+l6Jeg3yGykAkmGOW8QIQoqgffzdlOxWvF9pqDjVPgCNBMqxkcAlFr6hp3YJuysJApVdwyFgzpDSIo3cnHqTSF+fR+DywpAhwJEgGva18LuUtlAsob6B0UeYPkSAWYe9g3kshqAvXQ5HStQCDnoC+SMUvmSdQS5YAx4KEkPJPpWyknFk4lh8hR1LPIGZfhlMkiIaQHh8qREyBvHW9cCGFv0iR0iW7qfCSX8SoNRw3BJgOJEUdg1aKSFAUDTqJEH5KcZ0o2f4USRBNDfd62T1s4LEUrvl2WldHj7cPF8JhKrzsSZi1BeC/ABatIOvf+x1NAAAAAElFTkSuQmCC", ); diff --git a/lib/utils/customization/extended_text_theme.dart b/lib/utils/customization/extended_text_theme.dart new file mode 100644 index 000000000..289e38395 --- /dev/null +++ b/lib/utils/customization/extended_text_theme.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +class ExtendedTextTheme extends ThemeExtension { + final TextStyle tokenTile; + final TextStyle tokenTileSubtitle; + final String veilingCharacter; + + ExtendedTextTheme({ + this.veilingCharacter = '●', + TextStyle? tokenTile, + TextStyle? tokenTileSubtitle, + }) : tokenTile = const TextStyle( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ).merge(tokenTile), + tokenTileSubtitle = const TextStyle( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ).merge(tokenTileSubtitle); + + @override + ThemeExtension copyWith({ + TextStyle? otpTextStyle, + TextStyle? otpSubtitleTextStyle, + }) => + ExtendedTextTheme( + tokenTile: otpTextStyle ?? tokenTile, + tokenTileSubtitle: otpSubtitleTextStyle ?? tokenTileSubtitle, + ); + + @override + ThemeExtension lerp(ExtendedTextTheme? other, double t) => ExtendedTextTheme( + tokenTile: TextStyle.lerp(tokenTile, other?.tokenTile, t) ?? tokenTile, + ); +} diff --git a/lib/utils/customization/theme_customization.dart b/lib/utils/customization/theme_customization.dart new file mode 100644 index 000000000..93818c883 --- /dev/null +++ b/lib/utils/customization/theme_customization.dart @@ -0,0 +1,498 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:privacyidea_authenticator/utils/customization/action_theme.dart'; +import 'package:privacyidea_authenticator/utils/customization/extended_text_theme.dart'; + +class ThemeCustomization { + static const ThemeCustomization defaultLightTheme = ThemeCustomization.defaultLightWith(); + static const ThemeCustomization defaultDarkTheme = ThemeCustomization.defaultDarkWith(); + + const ThemeCustomization({ + required this.brightness, + required this.primaryColor, + required this.onPrimary, + required this.subtitleColor, + required this.backgroundColor, + required this.foregroundColor, + required this.shadowColor, + required this.deleteColor, + required this.renameColor, + required this.lockColor, + required this.tileIconColor, + required this.navigationBarColor, + Color? actionButtonsForegroundColor, + Color? tilePrimaryColor, + Color? tileSubtitleColor, + Color? navigationBarIconColor, + Color? qrButtonBackgroundColor, + Color? qrButtonIconColor, + }) : _actionButtonsForegroundColor = actionButtonsForegroundColor, + _tilePrimaryColor = tilePrimaryColor, + _tileSubtitleColor = tileSubtitleColor, + _navigationBarIconColor = navigationBarIconColor, + _qrButtonBackgroundColor = qrButtonBackgroundColor, + _qrButtonIconColor = qrButtonIconColor; + + const ThemeCustomization.defaultLightWith({ + Color? primaryColor, + Color? onPrimary, + Color? subtitleColor, + Color? backgroundColor, + Color? foregroundColor, + Color? shadowColor, + Color? deleteColor, + Color? renameColor, + Color? lockColor, + Color? tileIconColor, + Color? navigationBarColor, + // From here on the colors have a default value based on another given color + Color? actionButtonsForegroundColor, // Default: foregroundColor + Color? tilePrimaryColor, // Default: primaryColor + Color? tileSubtitleColor, // Default: subtitleColor + Color? navigationBarIconColor, // Default: foregroundColor + Color? qrButtonBackgroundColor, // Default: primaryColor + Color? qrButtonIconColor, // Default: onPrimary + }) : brightness = Brightness.light, + primaryColor = primaryColor ?? const Color(0xff03A9F4), + onPrimary = onPrimary ?? const Color(0xff282828), + subtitleColor = subtitleColor ?? const Color(0xff9E9E9E), + backgroundColor = backgroundColor ?? const Color(0xffEFEFEF), + foregroundColor = foregroundColor ?? const Color(0xff282828), + shadowColor = shadowColor ?? const Color(0xff303030), + deleteColor = deleteColor ?? const Color(0xffE04D2D), + renameColor = renameColor ?? const Color(0xff6A8FE5), + lockColor = lockColor ?? const Color(0xffFFD633), + tileIconColor = tileIconColor ?? const Color(0xff757575), + navigationBarColor = navigationBarColor ?? const Color(0xFFFFFFFF), + // From here on the colors have a default value based on another given color + _actionButtonsForegroundColor = actionButtonsForegroundColor, + _tilePrimaryColor = tilePrimaryColor, + _tileSubtitleColor = tileSubtitleColor, + _navigationBarIconColor = navigationBarIconColor, + _qrButtonBackgroundColor = qrButtonBackgroundColor, + _qrButtonIconColor = qrButtonIconColor; + + const ThemeCustomization.defaultDarkWith({ + Color? primaryColor, + Color? onPrimary, + Color? subtitleColor, + Color? backgroundColor, + Color? foregroundColor, + Color? shadowColor, + Color? deleteColor, + Color? renameColor, + Color? lockColor, + Color? tileIconColor, + Color? navigationBarColor, + // From here on the colors have a default value based on another given color + Color? actionButtonsForegroundColor, // Default: foregroundColor + Color? tilePrimaryColor, // Default: primaryColor + Color? tileSubtitleColor, // Default: subtitleColor + Color? navigationBarIconColor, // Default: foregroundColor + Color? qrButtonBackgroundColor, // Default: primaryColor + Color? qrButtonIconColor, // Default: onPrimary + }) : brightness = Brightness.dark, + primaryColor = primaryColor ?? const Color(0xff03A9F4), + onPrimary = onPrimary ?? const Color(0xFF282828), + subtitleColor = subtitleColor ?? const Color(0xFF9E9E9E), + backgroundColor = backgroundColor ?? const Color(0xFF303030), + foregroundColor = foregroundColor ?? const Color(0xffF5F5F5), + shadowColor = shadowColor ?? const Color(0xFFEFEFEF), + deleteColor = deleteColor ?? const Color(0xffCD3C14), + renameColor = renameColor ?? const Color(0xff527EDB), + lockColor = lockColor ?? const Color(0xffFFCC00), + tileIconColor = tileIconColor ?? const Color(0xffF5F5F5), + navigationBarColor = navigationBarColor ?? const Color(0xFF282828), + // From here on the colors have a default value based on another given color + _actionButtonsForegroundColor = actionButtonsForegroundColor, + _tilePrimaryColor = tilePrimaryColor, + _tileSubtitleColor = tileSubtitleColor, + _navigationBarIconColor = navigationBarIconColor, + _qrButtonBackgroundColor = qrButtonBackgroundColor, + _qrButtonIconColor = qrButtonIconColor; + + final Brightness brightness; + + // Basic colors + final Color primaryColor; + final Color onPrimary; + final Color subtitleColor; + final Color backgroundColor; + final Color foregroundColor; + final Color shadowColor; + + // Slide action + final Color deleteColor; + final Color renameColor; + final Color lockColor; + final Color? _actionButtonsForegroundColor; // Default: foregroundColor + Color get actionButtonsForegroundColor => _actionButtonsForegroundColor ?? foregroundColor; + + // List tile + final Color? _tilePrimaryColor; // Default: primaryColor + Color get tilePrimaryColor => _tilePrimaryColor ?? primaryColor; + final Color tileIconColor; + final Color? _tileSubtitleColor; // Default: subtitleColor + Color get tileSubtitleColor => _tileSubtitleColor ?? subtitleColor; + + // Navigation bar + final Color navigationBarColor; + final Color? _navigationBarIconColor; // Default: foregroundColor + Color get navigationBarIconColor => _navigationBarIconColor ?? foregroundColor; + final Color? _qrButtonBackgroundColor; // Default: primaryColor + Color get qrButtonBackgroundColor => _qrButtonBackgroundColor ?? primaryColor; + final Color? _qrButtonIconColor; // Default: onPrimary + Color get qrButtonIconColor => _qrButtonIconColor ?? onPrimary; + + ThemeCustomization copyWith({ + Brightness? brightness, + Color? primaryColor, + Color? onPrimary, + Color? subtitleColor, + Color? backgroundColor, + Color? foregroundColor, + Color? shadowColor, + Color? deleteColor, + Color? renameColor, + Color? lockColor, + Color? tileIconColor, + Color? navigationBarColor, + // From here on the colors have a default value based on another given color + Color? Function()? actionButtonsForegroundColor, // Default: foregroundColor + Color? Function()? tilePrimaryColor, // Default: primaryColor + Color? Function()? tileSubtitleColor, // Default: subtitleColor + Color? Function()? navigationBarIconColor, // Default: foregroundColor + Color? Function()? qrButtonBackgroundColor, // Default: primaryColor + Color? Function()? qrButtonIconColor, // Default: onPrimary + }) => + ThemeCustomization( + brightness: brightness ?? this.brightness, + primaryColor: primaryColor ?? this.primaryColor, + onPrimary: onPrimary ?? this.onPrimary, + subtitleColor: subtitleColor ?? this.subtitleColor, + backgroundColor: backgroundColor ?? this.backgroundColor, + foregroundColor: foregroundColor ?? this.foregroundColor, + shadowColor: shadowColor ?? this.shadowColor, + deleteColor: deleteColor ?? this.deleteColor, + renameColor: renameColor ?? this.renameColor, + lockColor: lockColor ?? this.lockColor, + actionButtonsForegroundColor: actionButtonsForegroundColor != null ? actionButtonsForegroundColor() : this.actionButtonsForegroundColor, + tilePrimaryColor: tilePrimaryColor != null ? tilePrimaryColor() : this.tilePrimaryColor, + tileIconColor: tileIconColor ?? this.tileIconColor, + tileSubtitleColor: tileSubtitleColor != null ? tileSubtitleColor() : this.tileSubtitleColor, + navigationBarColor: navigationBarColor ?? this.navigationBarColor, + navigationBarIconColor: navigationBarIconColor != null ? navigationBarIconColor() : this.navigationBarIconColor, + qrButtonBackgroundColor: qrButtonBackgroundColor != null ? qrButtonBackgroundColor() : this.qrButtonBackgroundColor, + qrButtonIconColor: qrButtonIconColor != null ? qrButtonIconColor() : this.qrButtonIconColor, + ); + + factory ThemeCustomization.fromJson(Map json) { + bool isLightTheme = json['brightness'] == 'light'; + bool isDarkTheme = json['brightness'] == 'dark'; + if (json['brightness'] == null && json['primaryColor'] != null) { + isLightTheme = _isColorBright(Color(json['primaryColor'] as int)); + } + if (isLightTheme) { + return ThemeCustomization.defaultLightWith( + primaryColor: json['primaryColor'] != null ? Color(json['primaryColor'] as int) : null, + onPrimary: json['onPrimary'] != null ? Color(json['onPrimary'] as int) : null, + subtitleColor: json['subtitleColor'] != null ? Color(json['subtitleColor'] as int) : null, + backgroundColor: json['backgroundColor'] != null ? Color(json['backgroundColor'] as int) : null, + foregroundColor: json['foregroundColor'] != null ? Color(json['foregroundColor'] as int) : null, + shadowColor: json['shadowColor'] != null ? Color(json['shadowColor'] as int) : null, + deleteColor: json['deleteColor'] != null ? Color(json['deleteColor'] as int) : null, + renameColor: json['renameColor'] != null ? Color(json['renameColor'] as int) : null, + lockColor: json['lockColor'] != null ? Color(json['lockColor'] as int) : null, + tileIconColor: json['tileIconColor'] != null ? Color(json['tileIconColor'] as int) : null, + navigationBarColor: json['navigationBarColor'] != null ? Color(json['navigationBarColor'] as int) : null, + actionButtonsForegroundColor: json['_actionButtonsForegroundColor'] != null ? Color(json['_actionButtonsForegroundColor'] as int) : null, + tilePrimaryColor: json['_tilePrimaryColor'] != null ? Color(json['_tilePrimaryColor'] as int) : null, + tileSubtitleColor: json['_tileSubtitleColor'] != null ? Color(json['_tileSubtitleColor'] as int) : null, + navigationBarIconColor: json['_navigationBarIconColor'] != null ? Color(json['_navigationBarIconColor'] as int) : null, + qrButtonBackgroundColor: json['_qrButtonBackgroundColor'] != null ? Color(json['_qrButtonBackgroundColor'] as int) : null, + qrButtonIconColor: json['_qrButtonIconColor'] != null ? Color(json['_qrButtonIconColor'] as int) : null, + ); + } + if (isDarkTheme) { + return ThemeCustomization.defaultDarkWith( + primaryColor: json['primaryColor'] != null ? Color(json['primaryColor'] as int) : null, + onPrimary: json['onPrimary'] != null ? Color(json['onPrimary'] as int) : null, + subtitleColor: json['subtitleColor'] != null ? Color(json['subtitleColor'] as int) : null, + backgroundColor: json['backgroundColor'] != null ? Color(json['backgroundColor'] as int) : null, + foregroundColor: json['foregroundColor'] != null ? Color(json['foregroundColor'] as int) : null, + shadowColor: json['shadowColor'] != null ? Color(json['shadowColor'] as int) : null, + deleteColor: json['deleteColor'] != null ? Color(json['deleteColor'] as int) : null, + renameColor: json['renameColor'] != null ? Color(json['renameColor'] as int) : null, + lockColor: json['lockColor'] != null ? Color(json['lockColor'] as int) : null, + tileIconColor: json['tileIconColor'] != null ? Color(json['tileIconColor'] as int) : null, + navigationBarColor: json['navigationBarColor'] != null ? Color(json['navigationBarColor'] as int) : null, + actionButtonsForegroundColor: json['_actionButtonsForegroundColor'] != null ? Color(json['_actionButtonsForegroundColor'] as int) : null, + tilePrimaryColor: json['_tilePrimaryColor'] != null ? Color(json['_tilePrimaryColor'] as int) : null, + tileSubtitleColor: json['_tileSubtitleColor'] != null ? Color(json['_tileSubtitleColor'] as int) : null, + navigationBarIconColor: json['_navigationBarIconColor'] != null ? Color(json['_navigationBarIconColor'] as int) : null, + qrButtonBackgroundColor: json['_qrButtonBackgroundColor'] != null ? Color(json['_qrButtonBackgroundColor'] as int) : null, + qrButtonIconColor: json['_qrButtonIconColor'] != null ? Color(json['_qrButtonIconColor'] as int) : null, + ); + } + throw Exception('Invalid brightness value: ${json['brightness']}'); + } + + Map toJson() => { + 'brightness': brightness == Brightness.light ? 'light' : 'dark', + 'primaryColor': primaryColor.value, + 'onPrimary': onPrimary.value, + 'subtitleColor': subtitleColor.value, + 'backgroundColor': backgroundColor.value, + 'foregroundColor': foregroundColor.value, + 'shadowColor': shadowColor.value, + 'deleteColor': deleteColor.value, + 'renameColor': renameColor.value, + 'lockColor': lockColor.value, + 'tileIconColor': tileIconColor.value, + 'navigationBarColor': navigationBarColor.value, + '_actionButtonsForegroundColor': _actionButtonsForegroundColor?.value, + '_tilePrimaryColor': _tilePrimaryColor?.value, + '_tileSubtitleColor': _tileSubtitleColor?.value, + '_navigationBarIconColor': _navigationBarIconColor?.value, + '_qrButtonBackgroundColor': _qrButtonBackgroundColor?.value, + '_qrButtonIconColor': _qrButtonIconColor?.value, + }; + + ThemeData generateTheme() => ThemeData( + useMaterial3: false, + brightness: brightness, + primaryColor: primaryColor, + canvasColor: backgroundColor, + textTheme: const TextTheme().copyWith( + bodyLarge: TextStyle(color: foregroundColor), + bodyMedium: TextStyle(color: foregroundColor), + titleMedium: TextStyle(color: foregroundColor), + titleSmall: TextStyle(color: foregroundColor), + displayLarge: TextStyle(color: foregroundColor), + displayMedium: TextStyle(color: foregroundColor), + displaySmall: TextStyle(color: foregroundColor), + headlineMedium: TextStyle(color: foregroundColor), + headlineSmall: TextStyle(color: foregroundColor), + titleLarge: TextStyle(color: primaryColor), + bodySmall: TextStyle(color: subtitleColor), + labelLarge: TextStyle(color: foregroundColor), + labelSmall: TextStyle(color: foregroundColor), + ), + iconButtonTheme: IconButtonThemeData( + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all(foregroundColor), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + foregroundColor: onPrimary, + backgroundColor: primaryColor, + padding: const EdgeInsets.all(6), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shadowColor: shadowColor, + elevation: 1.5, + ), + ), + scaffoldBackgroundColor: backgroundColor, + cardColor: backgroundColor, + shadowColor: shadowColor, + // shadowColor: Colors.transparent, + appBarTheme: const AppBarTheme().copyWith( + backgroundColor: backgroundColor, + shadowColor: shadowColor, + foregroundColor: foregroundColor, + elevation: 0, + titleSpacing: 6, + ), + inputDecorationTheme: InputDecorationTheme( + labelStyle: TextStyle(color: foregroundColor), + hintStyle: TextStyle(color: primaryColor), + errorStyle: TextStyle(color: deleteColor), + border: UnderlineInputBorder( + borderSide: BorderSide(color: shadowColor), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: subtitleColor), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor), + ), + ), + primaryIconTheme: IconThemeData(color: onPrimary), + iconTheme: IconThemeData(color: foregroundColor), + navigationBarTheme: const NavigationBarThemeData().copyWith( + backgroundColor: navigationBarColor, + shadowColor: shadowColor, + iconTheme: MaterialStatePropertyAll(IconThemeData(color: navigationBarIconColor)), + elevation: 3, + ), + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: qrButtonBackgroundColor, + foregroundColor: qrButtonIconColor, + elevation: 0, + ), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith((states) => foregroundColor.withOpacity(0.1)), + ), + ), + listTileTheme: ListTileThemeData( + tileColor: Colors.transparent, + titleTextStyle: TextStyle(color: tilePrimaryColor), + subtitleTextStyle: TextStyle(color: tileSubtitleColor), + iconColor: tileIconColor, + ), + colorScheme: brightness == Brightness.light + ? ColorScheme.light( + primary: primaryColor, + secondary: primaryColor, + onPrimary: onPrimary, + onSecondary: onPrimary, + error: deleteColor, + errorContainer: deleteColor, + ) + : ColorScheme.dark( + primary: primaryColor, + secondary: primaryColor, + onPrimary: onPrimary, + onSecondary: onPrimary, + error: deleteColor, + errorContainer: deleteColor, + ), + checkboxTheme: CheckboxThemeData( + checkColor: MaterialStateProperty.resolveWith((_) => onPrimary), + fillColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return primaryColor; + } + return null; + }), + ), + radioTheme: RadioThemeData( + fillColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return primaryColor; + } + return null; + }), + ), + switchTheme: SwitchThemeData( + thumbColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return primaryColor; + } + return null; + }), + trackColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return primaryColor; + } + return null; + }), + ), + extensions: [ + ActionTheme( + deleteColor: deleteColor, + editColor: renameColor, + lockColor: lockColor, + foregroundColor: actionButtonsForegroundColor, + ), + ExtendedTextTheme( + tokenTile: TextStyle( + color: primaryColor, + ), + tokenTileSubtitle: TextStyle( + color: tileSubtitleColor, + ), + ), + ]); + + @override + String toString() => 'ThemeCustomization(' + 'brightness: $brightness, ' + 'primaryColor: $primaryColor, ' + 'onPrimary: $onPrimary, ' + 'subtitleColor: $subtitleColor, ' + 'backgroundColor: $backgroundColor, ' + 'foregroundColor: $foregroundColor, ' + 'shadowColor: $shadowColor, ' + 'deleteColor: $deleteColor, ' + 'renameColor: $renameColor, ' + 'lockColor: $lockColor, ' + 'actionButtonsForegroundColor: $actionButtonsForegroundColor, ' + 'tilePrimaryColor: $tilePrimaryColor, ' + 'tileIconColor: $tileIconColor, ' + 'tileSubtitleColor: $tileSubtitleColor, ' + 'navigationBarColor: $navigationBarColor, ' + 'navigationBarIconColor: $navigationBarIconColor, ' + 'qrButtonBackgroundColor: $qrButtonBackgroundColor, ' + 'qrButtonIconColor: $qrButtonIconColor' + ')'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ThemeCustomization && + other.brightness == brightness && + other.primaryColor == primaryColor && + other.onPrimary == onPrimary && + other.subtitleColor == subtitleColor && + other.backgroundColor == backgroundColor && + other.foregroundColor == foregroundColor && + other.shadowColor == shadowColor && + other.deleteColor == deleteColor && + other.renameColor == renameColor && + other.lockColor == lockColor && + other.actionButtonsForegroundColor == actionButtonsForegroundColor && + other.tilePrimaryColor == tilePrimaryColor && + other.tileIconColor == tileIconColor && + other.tileSubtitleColor == tileSubtitleColor && + other.navigationBarColor == navigationBarColor && + other.navigationBarIconColor == navigationBarIconColor && + other.qrButtonBackgroundColor == qrButtonBackgroundColor && + other.qrButtonIconColor == qrButtonIconColor; + } + + @override + int get hashCode => Object.hashAll([ + brightness, + primaryColor, + onPrimary, + subtitleColor, + backgroundColor, + foregroundColor, + shadowColor, + deleteColor, + renameColor, + lockColor, + actionButtonsForegroundColor, + tilePrimaryColor, + tileIconColor, + tileSubtitleColor, + navigationBarColor, + navigationBarIconColor, + qrButtonBackgroundColor, + qrButtonIconColor, + ]); +} + +// /// Calculate HSP and check if the primary color is bright or dark +// /// brightness = sqrt( .299 R^2 + .587 G^2 + .114 B^2 ) +// /// c.f., http://alienryderflex.com/hsp.html +bool _isColorBright(Color color) { + return sqrt(0.299 * pow(color.red, 2) + 0.587 * pow(color.green, 2) + 0.114 * pow(color.blue, 2)) > 150; +} diff --git a/lib/utils/errors.dart b/lib/utils/errors.dart new file mode 100644 index 000000000..5bc7e3de6 --- /dev/null +++ b/lib/utils/errors.dart @@ -0,0 +1,53 @@ +import '../l10n/app_localizations.dart'; + +class LocalizedArgumentError extends LocalizedException implements ArgumentError { + final T _invalidValue; + final String? _name; + final StackTrace? _stackTrace; + + factory LocalizedArgumentError({ + required String Function(AppLocalizations localizations, T value, String name) localizedMessage, + required String unlocalizedMessage, + required T invalidValue, + required String name, + StackTrace? stackTrace, + }) => + LocalizedArgumentError._( + unlocalizedMessage: unlocalizedMessage, + localizedMessage: (localizations) => localizedMessage(localizations, invalidValue, name), + invalidValue: invalidValue, + name: name, + stackTrace: stackTrace, + ); + + const LocalizedArgumentError._({ + required super.unlocalizedMessage, + required super.localizedMessage, + required dynamic invalidValue, + String? name, + StackTrace? stackTrace, + }) : _invalidValue = invalidValue, + _name = name, + _stackTrace = stackTrace; + + @override + dynamic get invalidValue => _invalidValue; + @override + dynamic get message => super.unlocalizedMessage; + @override + String? get name => _name; + @override + StackTrace? get stackTrace => _stackTrace; + @override + String toString() => 'ArgumentError: $message'; +} + +class LocalizedException implements Exception { + final String Function(AppLocalizations localizations) localizedMessage; + final String unlocalizedMessage; + + const LocalizedException({required this.localizedMessage, required this.unlocalizedMessage}); + + @override + String toString() => 'Exception: $unlocalizedMessage'; +} diff --git a/lib/utils/firebase_utils.dart b/lib/utils/firebase_utils.dart index e7dc6adfb..a0e011ae1 100644 --- a/lib/utils/firebase_utils.dart +++ b/lib/utils/firebase_utils.dart @@ -1,9 +1,14 @@ +// ignore_for_file: constant_identifier_names + +import 'dart:io'; + import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mutex/mutex.dart'; -import '../repo/secure_token_repository.dart'; import 'globals.dart'; import 'identifiers.dart'; import 'logger.dart'; @@ -29,8 +34,9 @@ class FirebaseUtils { } _initialized = true; Logger.info('Initializing Firebase', name: 'firebase_utils.dart#initFirebase'); - await Firebase.initializeApp(); - + final app = await Firebase.initializeApp(); + await app.setAutomaticDataCollectionEnabled(false); + Logger.warning('Automatic data collection: ${app.isAutomaticDataCollectionEnabled}', name: 'firebase_utils.dart#initFirebase'); try { // await FirebaseMessaging.instance.requestPermission(); } on FirebaseException catch (e, s) { @@ -56,7 +62,7 @@ class FirebaseUtils { try { String? firebaseToken = await getFBToken(); - if (firebaseToken != await SecureTokenRepository.getCurrentFirebaseToken() && firebaseToken != null) { + if (firebaseToken != await getCurrentFirebaseToken() && firebaseToken != null) { updateFirebaseToken(firebaseToken); } } on PlatformException catch (error) { @@ -91,8 +97,8 @@ class FirebaseUtils { } FirebaseMessaging.instance.onTokenRefresh.listen((String newToken) async { - if ((await SecureTokenRepository.getCurrentFirebaseToken()) != newToken) { - await SecureTokenRepository.setNewFirebaseToken(newToken); + if ((await getCurrentFirebaseToken()) != newToken) { + await setNewFirebaseToken(newToken); // TODO what if this fails, when should a retry be attempted? try { updateFirebaseToken(newToken); @@ -123,9 +129,10 @@ class FirebaseUtils { // Fall back to the last known firebase token if (firebaseToken == null) { - firebaseToken = await SecureTokenRepository.getCurrentFirebaseToken(); + firebaseToken = await getCurrentFirebaseToken(); } else { - await SecureTokenRepository.setNewFirebaseToken(firebaseToken); + Logger.info('New Firebase token retrieved', name: 'push_provider.dart#getFBToken'); + await setNewFirebaseToken(firebaseToken); } if (firebaseToken == null) { @@ -139,4 +146,68 @@ class FirebaseUtils { return firebaseToken; } + + // ########################################################################### + // FIREBASE CONFIG + // ########################################################################### + static const _CURRENT_APP_TOKEN_KEY = '${GLOBAL_SECURE_REPO_PREFIX}CURRENT_APP_TOKEN'; + static const _NEW_APP_TOKEN_KEY = '${GLOBAL_SECURE_REPO_PREFIX}NEW_APP_TOKEN'; + static const _storage = FlutterSecureStorage(); + static final _m = Mutex(); + static Future _protect(Future Function() f) => _m.protect(f); + + // Future deleteFirebaseToken() => _protect(() async { + // final firebaseToken = await getCurrentFirebaseToken(); + // if (firebaseToken == null) { + // return false; + // } + // await _storage.delete(key: _CURRENT_APP_TOKEN_KEY); + // return true; + // }); + + // Future renewFirebaseToken() async { + // if (_initialized == false || await deleteFirebaseToken() == false) { + // return null; + // } + + // String? newToken = await FirebaseMessaging.instance.getToken(); + // if (newToken == null) { + // return null; + // } + // await setNewFirebaseToken(newToken); + // await setCurrentFirebaseToken(newToken); + // return newToken; + // } + + Future deleteFirebaseToken() async { + Logger.info('Deleting firebase token..', name: 'firebase_utils.dart#deleteFBToken'); + try { + final app = await Firebase.initializeApp(); + await app.setAutomaticDataCollectionEnabled(false); + await FirebaseMessaging.instance.deleteToken(); + Logger.warning('Firebase token deleted from Firebase', name: 'firebase_utils.dart#deleteFBToken'); + } on FirebaseException catch (e) { + if (e.message?.contains('IOException') == true) throw SocketException(e.message!); + rethrow; + } + await _storage.delete(key: _CURRENT_APP_TOKEN_KEY); + await _storage.delete(key: _NEW_APP_TOKEN_KEY); + Logger.info('Firebase token deleted from secure storage', name: 'firebase_utils.dart#deleteFBToken'); + return true; + } + + // FIXME: WHY CURRENT AND NEW TOKEN? + Future setCurrentFirebaseToken(String str) { + Logger.info('Setting current firebase token', name: 'secure_token_repository.dart#setCurrentFirebaseToken'); + return _protect(() => _storage.write(key: _CURRENT_APP_TOKEN_KEY, value: str)); + } + + Future getCurrentFirebaseToken() => _protect(() => _storage.read(key: _CURRENT_APP_TOKEN_KEY)); + + // This is used for checking if the token was updated. + Future setNewFirebaseToken(String str) => _protect(() { + Logger.info('Setting new firebase token', name: 'secure_token_repository.dart#setNewFirebaseToken'); + return _storage.write(key: _NEW_APP_TOKEN_KEY, value: str); + }); + Future getNewFirebaseToken() => _protect(() => _storage.read(key: _NEW_APP_TOKEN_KEY)); } diff --git a/lib/utils/globals.dart b/lib/utils/globals.dart index 184c5143f..274ed708e 100644 --- a/lib/utils/globals.dart +++ b/lib/utils/globals.dart @@ -24,7 +24,7 @@ import 'package:flutter/material.dart'; import '../l10n/app_localizations.dart'; import '../model/enums/patch_note_type.dart'; -import 'version.dart'; +import '../model/version.dart'; Map>> getLocalizedPatchNotes(AppLocalizations localizations) => { const Version(4, 3, 0): { @@ -50,7 +50,14 @@ Map>> getLocalizedPatchNotes(AppLocaliz final globalSnackbarKey = GlobalKey(); final globalNavigatorKey = GlobalKey(); final Future> contextedGlobalNavigatorKey = Future(() async => await _getContextedGlobalNavigatorKey()); -BuildContext? globalContextSync = globalNavigatorKey.currentContext; +BuildContext? get globalContextSync { + try { + return globalNavigatorKey.currentContext; + } catch (e) { + return null; + } +} + final Future globalContext = Future(() async => await _getContextedGlobalNavigatorKey()).then((value) => value.currentContext!); Future> _getContextedGlobalNavigatorKey() async { if (globalNavigatorKey.currentContext != null) { @@ -61,3 +68,7 @@ Future> _getContextedGlobalNavigatorKey() async { } final policyStatementUri = Uri.parse("https://netknights.it/en/privacy-statement/"); +final piAuthenticatorGitHubUri = Uri.parse("https://github.com/privacyidea/pi-authenticator"); + +// The highest version of the pipush Tokentype that this client supports. +const maxPushTokenVersion = 1; diff --git a/lib/utils/home_widget_utils.dart b/lib/utils/home_widget_utils.dart index cd5929568..0319e0653 100644 --- a/lib/utils/home_widget_utils.dart +++ b/lib/utils/home_widget_utils.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; - import 'dart:io'; import 'package:collection/collection.dart'; @@ -8,12 +7,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:home_widget/home_widget.dart'; import 'package:mutex/mutex.dart'; -import 'package:privacyidea_authenticator/utils/riverpod_providers.dart'; -import '../model/token_folder.dart'; +import 'package:privacyidea_authenticator/utils/customization/theme_customization.dart'; import '../interfaces/repo/token_folder_repository.dart'; import '../interfaces/repo/token_repository.dart'; import '../mains/main_netknights.dart'; +import '../model/token_folder.dart'; import '../model/tokens/day_password_token.dart'; import '../model/tokens/hotp_token.dart'; import '../model/tokens/otp_token.dart'; @@ -29,8 +28,8 @@ import '../widgets/home_widgets/home_widget_copied.dart'; import '../widgets/home_widgets/home_widget_hidden.dart'; import '../widgets/home_widgets/home_widget_otp.dart'; import '../widgets/home_widgets/home_widget_unlinked.dart'; -import 'app_customizer.dart'; import 'logger.dart'; +import 'riverpod_providers.dart'; const appGroupId = 'group.authenticator_home_widget_group'; @@ -45,20 +44,20 @@ class HomeWidgetUtils { HomeWidgetUtils._(); static HomeWidgetUtils? _instance; - /// Check widget_layout.xml for the sizes. Double it for better quality + /// Check android/app/src/main/res/layout/widget_layout.xml for the sizes. Double it for better quality static const _widgetBackgroundSize = Size(130 * 2, 65 * 2); - /// Check widget_layout.xml for the sizes. Double it for better quality + /// Check android/app/src/main/res/layout/widget_layout.xml for the sizes. Double it for better quality static const _widgetOtpSize = Size(98 * 2, 40 * 2); - /// Check widget_layout.xml for the sizes. Double it for better quality + /// Check android/app/src/main/res/layout/widget_layout.xml for the sizes. Double it for better quality static const _widgetSettingsSize = Size(14 * 2, 14 * 2); - /// Check widget_layout.xml for the sizes. Double it for better quality + /// Check android/app/src/main/res/layout/widget_layout.xml for the sizes. Double it for better quality static const _widgetActionSize = Size(24 * 2, 24 * 2); /// Default duration for showing the OTP - static const _showDuration = Duration(seconds: 30); + static const _showDuration = Duration(seconds: 15); factory HomeWidgetUtils({TokenRepository? tokenRepository, TokenFolderRepository? tokenFolderRepository}) { if (Platform.isIOS) return UnsupportedHomeWidgetUtils(); // Not supported on iOS @@ -80,7 +79,11 @@ class HomeWidgetUtils { static TokenRepository? _tokenRepository; static TokenFolderRepository? _folderRepository; static final Mutex _repoMutex = Mutex(); - static Future> get _otpTokens async => (await _loadTokensFromRepo()).whereType().toList(); + static Future> get _otpTokens async { + final tokens = globalRef?.read(tokenProvider).tokens; + return tokens?.whereType().toList() ?? (await _loadTokensFromRepo()).whereType().toList(); + } + static Future _getTokenOfTokenId(String? tokenId) async { await _repoMutex.acquire(); final token = (await _loadTokensFromRepo()).firstWhereOrNull((token) => token.id == tokenId); @@ -168,25 +171,18 @@ class HomeWidgetUtils { return widgetId == null ? null : _getTokenOfTokenId(await getTokenIdOfWidgetId(widgetId)); } - Future> _getTokensOfWidgetIds(List widgetIds) async { - final tokenMap = {}; - final allTokens = await _otpTokens; - for (String widgetId in widgetIds) { - final tokenId = await HomeWidget.getWidgetData('$keyTokenId$widgetId'); - final token = allTokens.firstWhereOrNull((element) => element.id == tokenId); - tokenMap[widgetId] = token; - } - return tokenMap; - } - - Future> _getWidgetIdsOfTokens(List tokenIds) async { - final widgetIds = []; - for (String widgetId in (await _widgetIds)) { - if (tokenIds.contains(await getTokenIdOfWidgetId(widgetId))) { - widgetIds.add(widgetId); + /// a token can be linked to multiple widgets but widgetIs can only be linked to one token + Future> _getWidgetIdsOfTokens(List tokenIds) async { + final tokenWigetPairs = {}; + for (final widgetId in (await _widgetIds)) { + for (final tokenId in tokenIds) { + if (tokenId == await getTokenIdOfWidgetId(widgetId)) { + tokenWigetPairs[widgetId] = tokenId; + } } } - return widgetIds; + Logger.info('Found ${tokenWigetPairs.length} linked Widgets', name: 'home_widget_utils.dart#_getWidgetIdsOfTokens'); + return tokenWigetPairs; } ThemeData? _themeDataDark; @@ -227,6 +223,18 @@ class HomeWidgetUtils { await _notifyUpdate(await _widgetIds); } + Future hideAllOtps() async { + final widgetIds = await _widgetIds; + final futures = []; + for (String widgetId in widgetIds) { + futures.add(HomeWidget.saveWidgetData('$keyShowToken$widgetId', false)); + } + if (futures.isEmpty) return false; + await Future.wait(futures); + await _notifyUpdate(widgetIds); + return true; + } + // Call AFTER saving to the repository Future updateTokenIfLinked(Token token) async { final updatedIds = await _updateTokenIfLinked(token); @@ -236,23 +244,20 @@ class HomeWidgetUtils { // Call AFTER saving to the repository Future updateTokensIfLinked(List tokens) async { // Map - Map widgetIdTokenIdMap = {}; final hotpTokens = tokens.whereType().toList(); final hotpTokenIds = hotpTokens.map((e) => e.id).toList(); final linkedWidgetIds = await _getWidgetIdsOfTokens(hotpTokenIds); - for (String widgetId in linkedWidgetIds) { - final tokenId = await getTokenIdOfWidgetId(widgetId); - final hotpToken = hotpTokens.firstWhereOrNull((element) => element.id == tokenId); - if (tokenId != null) { - widgetIdTokenIdMap[widgetId] = hotpToken; - } - } - for (String widgetId in widgetIdTokenIdMap.keys) { - final hotpToken = widgetIdTokenIdMap[widgetId]; + final futures = []; + for (String widgetId in linkedWidgetIds.keys) { + final hotpToken = hotpTokens.firstWhereOrNull((element) => element.id == linkedWidgetIds[widgetId]); if (hotpToken == null) continue; - await _updateHomeWidgetHideOtp(hotpToken, widgetId); + futures.addAll([ + _updateHomeWidgetHideOtp(hotpToken, widgetId), + _updateHomeWidgetShowOtp(hotpToken, widgetId), + ]); } - await _notifyUpdate(widgetIdTokenIdMap.keys); + await Future.wait(futures); + await _notifyUpdate(linkedWidgetIds.keys); } Future link(String widgetId, String tokenId) async { @@ -286,22 +291,22 @@ class HomeWidgetUtils { _hideOtpDelayed(widgetId, otpToken.otpValue.length); } - Future handleChangedTokenState() async { - final idTokenPairs = await _getTokensOfWidgetIds(await _widgetIds); - final homeWidgetChanges = []; - for (String widgetId in idTokenPairs.keys) { - final token = idTokenPairs[widgetId]; - if (token == null) { - homeWidgetChanges.add(_unlink(widgetId)); - continue; - } - homeWidgetChanges.add(HomeWidget.saveWidgetData('$keyTokenLocked$widgetId', token.isLocked || ((await _folderOf(token))?.isLocked ?? false))); - homeWidgetChanges.add(HomeWidget.saveWidgetData('$keyShowToken$widgetId', false)); - homeWidgetChanges.add(_updateHomeWidgetHideOtp(token, widgetId)); - } - await Future.wait(homeWidgetChanges); - await _notifyUpdate(idTokenPairs.keys); - } + // Future handleChangedTokenState() async { + // final idTokenPairs = await _getTokensOfWidgetIds(await _widgetIds); + // final homeWidgetChanges = []; + // for (String widgetId in idTokenPairs.keys) { + // final token = idTokenPairs[widgetId]; + // if (token == null) { + // homeWidgetChanges.add(_unlink(widgetId)); + // continue; + // } + // homeWidgetChanges.add(HomeWidget.saveWidgetData('$keyTokenLocked$widgetId', token.isLocked || ((await _folderOf(token))?.isLocked ?? false))); + // homeWidgetChanges.add(HomeWidget.saveWidgetData('$keyShowToken$widgetId', false)); + // homeWidgetChanges.add(_updateHomeWidgetHideOtp(token, widgetId)); + // } + // await Future.wait(homeWidgetChanges); + // await _notifyUpdate(idTokenPairs.keys); + // } /// widgetId,Timer final Map _copyTimers = {}; @@ -348,7 +353,7 @@ class HomeWidgetUtils { return; } HomeWidget.saveWidgetData('$keyActionBlocked$tokenId', true); - final widgetIds = await _getWidgetIdsOfTokens([token.id]); + final widgetIds = (await _getWidgetIdsOfTokens([token.id])).keys.toList(); _actionTimers[tokenId] = Timer(const Duration(seconds: 1), () async { Logger.info('Unblocked action', name: 'home_widget_utils.dart#performAction'); await HomeWidget.saveWidgetData('$keyActionBlocked$tokenId', false); @@ -393,10 +398,12 @@ class HomeWidgetUtils { final Map _hideTimers = {}; void _hideOtpDelayed(String widgetId, int otpLength) { _hideTimers[widgetId]?.cancel(); - _hideTimers[widgetId] = Timer(_showDuration, () async { - await HomeWidget.saveWidgetData('$keyShowToken$widgetId', false); - await _notifyUpdate([widgetId]); - }); + _hideTimers[widgetId] = Timer(_showDuration, () => _hideOtp(widgetId, otpLength)); + } + + Future _hideOtp(String widgetId, int otpLength) async { + await HomeWidget.saveWidgetData('$keyShowToken$widgetId', false); + await _notifyUpdate([widgetId]); } Future _link(String widgetId, OTPToken token) async { @@ -445,7 +452,7 @@ class HomeWidgetUtils { // Call AFTER saving to the repository Future> _updateTokenIfLinked(Token token) async { if (token is! OTPToken) return []; - final widgetIds = await _getWidgetIdsOfTokens([token.id]); + final widgetIds = (await _getWidgetIdsOfTokens([token.id])).keys.toList(); final futures = []; for (String widgetId in widgetIds) { futures.add(_updateHomeWidgetHideOtp(token, widgetId)); @@ -587,7 +594,7 @@ class HomeWidgetUtils { /// This method has to be called after change to the HomeWidget to notify the HomeWidget to update Future _notifyUpdate(Iterable updatedWidgetIds) async { if (updatedWidgetIds.isEmpty) return; - Logger.info('Update requested for: $updatedWidgetIds', name: 'home_widget_utils.dart#_notifyUpdate'); + Logger.info('Update requested for: $updatedWidgetIds', name: 'home_widget_utils.dart#_notifyUpdate', stackTrace: StackTrace.current); if (await _widgetIsRebuilding || _lastUpdate != null && DateTime.now().difference(_lastUpdate!) < _updateDelay) { Logger.info('Update delayed: $updatedWidgetIds', name: 'home_widget_utils.dart#_notifyUpdate'); _updatedWidgetIds.addAll(updatedWidgetIds); @@ -624,9 +631,7 @@ class UnsupportedHomeWidgetUtils implements HomeWidgetUtils { @override Future _getThemeData({bool dark = false}) => Future.value(ThemeData.light()); @override - Future> _getTokensOfWidgetIds(List widgetIds) => Future.value({}); - @override - Future> _getWidgetIdsOfTokens(List tokenIds) => Future.value([]); + Future> _getWidgetIdsOfTokens(List tokenIds) => Future.value({}); @override void _hideOtpDelayed(String widgetId, int otpLength) {} @override @@ -690,8 +695,6 @@ class UnsupportedHomeWidgetUtils implements HomeWidgetUtils { @override Future getTokenOfWidgetId(String? widgetId) => Future.value(null); @override - Future handleChangedTokenState() async {} - @override Future homeWidgetInit({TokenRepository? repository}) async {} @override Future link(String widgetId, String tokenId) async {} @@ -717,4 +720,8 @@ class UnsupportedHomeWidgetUtils implements HomeWidgetUtils { Future registerInteractivityCallback(void Function(Uri? uri) homeWidgetBackgroundCallback) => Future.value(null); @override Future setAppGroupId(String appGroupId) => Future.value(null); + @override + Future _hideOtp(String widgetId, int otpLength) async {} + @override + Future hideAllOtps() async => false; } diff --git a/lib/utils/identifiers.dart b/lib/utils/identifiers.dart index 022f2bb79..e56679f64 100644 --- a/lib/utils/identifiers.dart +++ b/lib/utils/identifiers.dart @@ -29,12 +29,13 @@ 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_SECRET = 'URI_SECRET'; // Should be base32 encoded const String URI_COUNTER = 'URI_COUNTER'; const String URI_PERIOD = 'URI_PERIOD'; const String URI_ISSUER = 'URI_ISSUER'; const String URI_PIN = 'URI_PIN'; const String URI_IMAGE = 'URI_IMAGE'; +const String URI_ORIGIN = 'URI_ORIGIN'; // 2 step: const String URI_SALT_LENGTH = 'URI_SALT_LENGTH'; @@ -63,6 +64,8 @@ const String PUSH_REQUEST_TITLE = 'title'; // 5. const String PUSH_REQUEST_SSL_VERIFY = 'sslverify'; // 6. const String PUSH_REQUEST_SIGNATURE = 'signature'; // 7. +const String GLOBAL_SECURE_REPO_PREFIX = 'app_v3_'; + bool validateMap(Map map, List keys) { for (String key in keys) { if (!map.containsKey(key)) { diff --git a/lib/utils/image_converter.dart b/lib/utils/image_converter.dart index 455a11c1c..c4181945e 100644 --- a/lib/utils/image_converter.dart +++ b/lib/utils/image_converter.dart @@ -178,12 +178,15 @@ class ImageConverter { } factory ImageConverter._fromJPEG(CameraImage image) { + Logger.info('Converting JPEG image to Image'); return ImageConverter(image: imglib.decodeJpg(image.planes[0].bytes)!); } factory ImageConverter._fromBGRA8888(CameraImage image, int rotation, bool mirror, int cropLeft, int cropRight, int cropTop, int cropBottom) { + Logger.info('Converting BGRA8888 image to Image'); + rotation = 360 - (rotation % 360); // if the image is rotated by 90, we need to rotate by another 270 to get the correct rotation (0/360) const numChannels = 4; // 1 for alpha, 3 for RGB - final img = imglib.Image.fromBytes( + var img = imglib.Image.fromBytes( width: image.width, height: image.height, rowStride: image.planes[0].bytesPerRow, @@ -192,14 +195,20 @@ class ImageConverter { bytes: (image.planes[0].bytes).buffer, order: imglib.ChannelOrder.bgra, ); - return ImageConverter( - image: imglib.copyCrop( + if (rotation != 0) { + img = imglib.copyRotate(img, angle: rotation); + } + if (mirror) { + img = imglib.flip(img, direction: imglib.FlipDirection.horizontal); + } + img = imglib.copyCrop( img, x: cropLeft, y: cropTop, width: img.width - cropLeft - cropRight, height: img.height - cropTop - cropBottom, - )); + ); + return ImageConverter(image: img); } factory ImageConverter._fromYUV420( @@ -211,6 +220,7 @@ class ImageConverter { int cropTop = 0, int cropBottom = 0, ]) { + Logger.info('Converting YUV420 image to Image'); rotation = 360 - (rotation % 360); // if the rotation is 90, we need to rotate by 270 to get the correct rotation const alpha = 0xFF; diff --git a/lib/utils/license_utils.dart b/lib/utils/license_utils.dart deleted file mode 100644 index 684f134ca..000000000 --- a/lib/utils/license_utils.dart +++ /dev/null @@ -1,629 +0,0 @@ -// ignore_for_file: constant_identifier_names - -/* - privacyIDEA Authenticator - - Authors: Timo Sturm - Frank Merkel - Copyright (c) 2017-2023 NetKnights GmbH - - Licensed under the Apache License, Version 2.0 (the 'License'); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an 'AS IS' BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -import 'package:flutter/foundation.dart'; - -/// This method removes all licenses from the LicenseRegistry. -/// It can be used for testing purposes, if one wishes to inspect a specifically -/// added license. -clearLicenses() { - // ignore: invalid_use_of_visible_for_testing_member - LicenseRegistry.reset(); -} - -addAllLicenses() { - _addNewLicense('privacyIDEA Authenticator', _PI_AUTHENTICATOR_LICENSE); - _addNewLicense('dart-hex', _DART_HEX_LICENSE); - _addNewLicense('dart-base32', _DART_BASE32_LICENSE); - _addNewLicense('otp', _DART_OTP_LICENSE); - _addNewLicense('dart-uuid', _DART_UUID_LICENSE); - _addNewLicense('json_serializabel', _JSON_SERIALIZABLE_LICENSE); - _addNewLicense('flutter_secure_storage', _FLUTTER_SECURE_STORAGE_LICENSE); - _addNewLicense('flutter_slidable', _FLUTTER_SLIDABLE_LICENSE); - _addNewLicense('intl', _INTL_LICENSE); - _addNewLicense('package_info', _PACKAGE_INFO_LICENSE); - _addNewLicense('pointycastle', _POINTYCASTLE_LICENSE); - _addNewLicense('dynamic_theme', _DYNAMIC_THEME_LICENSE); - _addNewLicense('flutterfire', _FLUTTERFIRE_LICENSE); - _addNewLicense('firebase_core', _FIREBASE_CORE_LICENSE); - _addNewLicense('asn1lib', _ASN1LIB_LICENSE); - _addNewLicense('http', _HTTP_LICENSE); - _addNewLicense('flutter_local_notifications#', _FLUTTER_LOCAL_NOTIFICATIONS); - _addNewLicense('dart-mutex', _DART_MUTEX_LICENSE); -} - -_addNewLicense(String packageName, String licenseText) { - LicenseRegistry.addLicense(() async* { - yield LicenseEntryWithLineBreaks([packageName], licenseText); - }); -} - -const String _DART_MUTEX_LICENSE = ''' -Copyright (c) 2016, Hoylen Sue. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - '''; - -const String _FLUTTER_LOCAL_NOTIFICATIONS = ''' -Copyright 2018 Michael Bui. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of the copyright holder nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -'''; - -const String _HTTP_LICENSE = ''' -Copyright 2014, the Dart project authors. All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -'''; - -const String _ASN1LIB_LICENSE = ''' -http://opensource.org/licenses/BSD-3-Clause -Copyright (c) 2015, Warren Strange -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - - Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -'''; - -const String _FIREBASE_CORE_LICENSE = ''' -// Copyright 2017 The Chromium Authors. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -'''; -const String _FLUTTERFIRE_LICENSE = ''' -Copyright 2017 The Chromium Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -'''; -const String _DYNAMIC_THEME_LICENSE = ''' -MIT License - -Copyright (c) 2019 Norbert Kozsir - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the 'Software'), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -'''; -const String _PI_AUTHENTICATOR_LICENSE = ''' - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - 'License' shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - 'Licensor' shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - 'Legal Entity' shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - 'control' means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - 'You' (or 'Your') shall mean an individual or Legal Entity - exercising permissions granted by this License. - - 'Source' form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - 'Object' form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - 'Work' shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - 'Derivative Works' shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - 'Contribution' shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, 'submitted' - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as 'Not a Contribution.' - - 'Contributor' shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a 'NOTICE' text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an 'AS IS' BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS'''; -const String _DART_HEX_LICENSE = ''' - -The MIT License (MIT) - -Copyright (c) 2016 Dartcoin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the 'Software'), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -'''; -const String _DART_BASE32_LICENSE = ''' Copyright (c) 2012 Yulian Kuncheff - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.'''; -const String _DART_OTP_LICENSE = ''' Copyright (c) 2012 Yulian Kuncheff - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.'''; -const String _DART_UUID_LICENSE = ''' Copyright (c) 2012 Yulian Kuncheff - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.'''; -const String _JSON_SERIALIZABLE_LICENSE = ''' -Copyright 2017, the Dart project authors. All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''; -const String _FLUTTER_SECURE_STORAGE_LICENSE = ''' -// Copyright 2017 Your Company. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Your Company nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''; -const String _FLUTTER_SLIDABLE_LICENSE = ''' -MIT License - -Copyright (c) 2018 Romain Rastel - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the 'Software'), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.'''; -const String _INTL_LICENSE = ''' -Copyright 2013, the Dart project authors. All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -'''; -const String _PACKAGE_INFO_LICENSE = ''' Copyright 2017 The Chromium Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''; -const String _POINTYCASTLE_LICENSE = ''' -Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the 'Software'), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -'''; diff --git a/lib/utils/lock_auth.dart b/lib/utils/lock_auth.dart index a9c5a4fbe..fa7392fc5 100644 --- a/lib/utils/lock_auth.dart +++ b/lib/utils/lock_auth.dart @@ -13,7 +13,7 @@ import 'globals.dart'; import 'logger.dart'; import 'view_utils.dart'; -bool authenticationInProgress = false; +bool _authenticationInProgress = false; /// Sends a request to the OS to authenticate the user. Returns true if the user was authenticated, false otherwise. Future lockAuth({required String localizedReason}) async { @@ -65,16 +65,16 @@ Future lockAuth({required String localizedReason}) async { ); try { - if (!authenticationInProgress) { - authenticationInProgress = true; + if (!_authenticationInProgress) { + _authenticationInProgress = true; didAuthenticate = await localAuth.authenticate(localizedReason: localizedReason, authMessages: [ androidAuthStrings, iOSAuthStrings, ]); - authenticationInProgress = false; + _authenticationInProgress = false; } } on PlatformException catch (e, s) { - authenticationInProgress = false; + _authenticationInProgress = false; Logger.info("Authentication failed", error: e, stackTrace: s); } return didAuthenticate; diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index c3b480f81..c5e75431e 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -110,17 +110,20 @@ class Logger { /*----------- LOGGING METHODS -----------*/ - static void info(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) { - String infoString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.INFO); + void logInfo(String message, {String? name, bool verbose = false}) { + String infoString = _convertLogToSingleString(message, name: name, logLevel: LogLevel.INFO); infoString = _textFilter(infoString); - if (instance._verbose || verbose) { - instance._logToFile(infoString); + if (_verbose || verbose) { + _logToFile(infoString); } _print(infoString); } - static void warning(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) { - String warningString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.WARNING); + static void info(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) => + instance.logInfo(message, name: name, verbose: verbose); + + void logWarning(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) { + String warningString = _convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.WARNING); warningString = _textFilter(warningString); if (instance._verbose || verbose) { instance._logToFile(warningString); @@ -128,16 +131,19 @@ class Logger { _printWarning(warningString); } - static void error(String? message, {dynamic error, dynamic stackTrace, String? name}) { - String errorString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.ERROR); + static void warning(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) => + instance.logWarning(message, error: error, stackTrace: stackTrace, name: name, verbose: verbose); + + void logError(String? message, {dynamic error, dynamic stackTrace, String? name}) { + String errorString = _convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.ERROR); errorString = _textFilter(errorString); if (message != null) { - instance._lastError = message.substring(0, min(message.length, 100)); + _lastError = message.substring(0, min(message.length, 100)); } else if (error != null) { - instance._lastError = error.toString().substring(0, min(error.toString().length, 100)); + _lastError = error.toString().substring(0, min(error.toString().length, 100)); } - instance._logToFile(errorString); - instance._showSnackbar(); + _logToFile(errorString); + _showSnackbar(); StackTrace? stackTraceObject; if (stackTrace is StackTrace) { stackTraceObject = stackTrace; @@ -147,6 +153,9 @@ class Logger { _printError(message, error: error, stackTrace: stackTraceObject, name: name); } + static void error(String? message, {dynamic error, dynamic stackTrace, String? name}) => + instance.logError(message, error: error, stackTrace: stackTrace, name: name); + Future _logToFile(String fileMessage) async { if (_enableLoggingToFile == false) return; await _mutexWriteFile.acquire(); @@ -164,11 +173,11 @@ class Logger { _mutexWriteFile.release(); } - static void sendErrorLog() { - instance._sendErrorLog(); + static void sendErrorLog([String? message]) { + instance._sendErrorLog(message); } - Future _sendErrorLog() { + Future _sendErrorLog([String? message]) { if (_fullPath == null || kIsWeb) return Future.value(false); final File file = File(_fullPath!); if (!file.existsSync() || file.lengthSync() == 0) { @@ -176,7 +185,7 @@ class Logger { } String deviceInfo = AppInfoUtils.deviceInfoString; - final completeMailBody = """$_mailBody + final completeMailBody = """${message ?? _mailBody} --------------------------------------------------------- Device Parameters $deviceInfo"""; @@ -322,7 +331,8 @@ Device Parameters $deviceInfo"""; static String _textFilter(String text) { for (var key in filterParameterKeys) { - final regex = RegExp(r'(?<=' + key + r':\s).+?(?=[},])'); + // It searches for the key, ignores following characters until it finds base64 caracters (plus padding and separator) and replaces it with "******" + final regex = RegExp(r'(?<=' + key + r'[^A-Z0-9+/=,]*)[A-Z0-9+/=,]+', caseSensitive: false); text = text.replaceAll(regex, '******'); } return text; @@ -347,7 +357,7 @@ Device Parameters $deviceInfo"""; } } -final filterParameterKeys = ['fbtoken', 'new_fb_token']; +final filterParameterKeys = ['fbtoken', 'new_fb_token', 'secret']; enum LogLevel { INFO, diff --git a/lib/utils/network_utils.dart b/lib/utils/network_utils.dart index 566b5f03a..9d63c4103 100644 --- a/lib/utils/network_utils.dart +++ b/lib/utils/network_utils.dart @@ -18,6 +18,7 @@ limitations under the License. */ +import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -48,8 +49,20 @@ class PrivacyIdeaIOClient { IOClient ioClient = IOClient(httpClient); try { - await ioClient.post(url, body: ''); - } on SocketException { + await ioClient.post(url, body: '').timeout(const Duration(seconds: 15)); + } on ClientException { + Logger.warning('ClientException', name: 'utils.dart#triggerNetworkAccessPermission'); + ioClient.close(); + if (globalNavigatorKey.currentState?.context == null) return false; + globalRef?.read(statusMessageProvider.notifier).state = ( + AppLocalizations.of(await globalContext)!.connectionFailed, + AppLocalizations.of(await globalContext)!.checkYourNetwork, + ); + return false; + } catch (e, _) { + if (e is! SocketException && e is! TimeoutException) { + rethrow; + } if (isRetry) { Logger.warning('SocketException while retrying', name: 'utils.dart#triggerNetworkAccessPermission'); if (globalNavigatorKey.currentState?.context != null) { @@ -66,15 +79,6 @@ class PrivacyIdeaIOClient { const Duration(seconds: 10), () => triggerNetworkAccessPermission(url: url, sslVerify: sslVerify, isRetry: true), ); - } on ClientException { - Logger.warning('ClientException', name: 'utils.dart#triggerNetworkAccessPermission'); - ioClient.close(); - if (globalNavigatorKey.currentState?.context == null) return false; - globalRef?.read(statusMessageProvider.notifier).state = ( - AppLocalizations.of(await globalContext)!.connectionFailed, - AppLocalizations.of(await globalContext)!.checkYourNetwork, - ); - return false; } finally { ioClient.close(); } @@ -107,11 +111,12 @@ class PrivacyIdeaIOClient { Response response; try { - response = await ioClient.post(url, body: body); - } on SocketException catch (e, s) { - response = Response('${e.runtimeType} : $s', 404); + response = await ioClient.post(url, body: body).timeout(const Duration(seconds: 15)); } on HandshakeException catch (e, s) { response = Response('${e.runtimeType} : $s', 525); + } catch (e, s) { + if (e is! TimeoutException && e is! SocketException) rethrow; + response = Response('${e.runtimeType} : $s', 404); } if (response.statusCode != 200) { @@ -157,13 +162,14 @@ class PrivacyIdeaIOClient { Response response; Uri uri = Uri.parse(buffer.toString()); try { - response = await ioClient.get(uri); - } on SocketException catch (e, s) { - response = Response('${e.runtimeType} : $s', 404); + response = await ioClient.get(uri).timeout(const Duration(seconds: 15)); } on HandshakeException catch (e, s) { Logger.warning('Handshake failed. sslVerify: $sslVerify', name: 'utils.dart#doGet', error: e, stackTrace: s); showMessage(message: 'Handshake failed, please check the server certificate and try again.'); rethrow; + } catch (e, s) { + if (e is! TimeoutException && e is! SocketException) rethrow; + response = Response('${e.runtimeType} : $s', 404); } if (response.statusCode != 200) { diff --git a/lib/utils/patch_notes_utils.dart b/lib/utils/patch_notes_utils.dart index ac45040c1..5dbe0ca0f 100644 --- a/lib/utils/patch_notes_utils.dart +++ b/lib/utils/patch_notes_utils.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import '../l10n/app_localizations.dart'; import '../model/enums/patch_note_type.dart'; +import '../model/version.dart'; import '../widgets/dialog_widgets/patch_notes_dialog.dart'; import 'app_info_utils.dart'; import 'globals.dart'; import 'logger.dart'; -import 'version.dart'; class PatchNotesUtils { static Map>> _getNewPatchNotes({required BuildContext context, required Version latestStartedVersion}) { diff --git a/lib/utils/pi_mailer.dart b/lib/utils/pi_mailer.dart index aa02a6e20..8de0a3311 100644 --- a/lib/utils/pi_mailer.dart +++ b/lib/utils/pi_mailer.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mailer/flutter_mailer.dart'; -import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; -import 'package:privacyidea_authenticator/utils/view_utils.dart'; -import 'package:privacyidea_authenticator/widgets/dialog_widgets/default_dialog.dart'; + +import '../l10n/app_localizations.dart'; +import '../widgets/dialog_widgets/default_dialog.dart'; import 'app_info_utils.dart'; import 'logger.dart'; +import 'view_utils.dart'; class PiMailer { static String get _mailRecipient => 'app-crash@netknights.it'; diff --git a/lib/utils/pi_notifications.dart b/lib/utils/pi_notifications.dart new file mode 100644 index 000000000..8a98113a7 --- /dev/null +++ b/lib/utils/pi_notifications.dart @@ -0,0 +1,42 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class PiNotifications { + static PiNotifications? _instance; + int id = 0; + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + late NotificationDetails notificationDetails; + + PiNotifications._(); + + static Future show(String title, String body) async => (await _getInstance)._show(title, body); + + static Future get _getInstance async { + if (_instance == null) { + _instance = PiNotifications._(); + await _instance!._initialize(); + } + return _instance!; + } + + Future _initialize() async { + var initializationSettingsAndroid = const AndroidInitializationSettings('@mipmap/ic_launcher'); // <- default icon name is @mipmap/ic_launcher + // var initializationSettingsIOS = IOSInitializationSettings(onDidReceiveLocalNotification: onDidReceiveLocalNotification); + var initializationSettings = InitializationSettings(android: initializationSettingsAndroid); + flutterLocalNotificationsPlugin.initialize(initializationSettings); + AndroidNotificationDetails androidNotificationDetails = const AndroidNotificationDetails( + 'PiNotifications', + 'PiNotifications', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker', + ); + + notificationDetails = NotificationDetails(android: androidNotificationDetails); + } + + Future _show(String title, String body) async { + final id = this.id++; + await flutterLocalNotificationsPlugin.show(id, title, body, notificationDetails); + return id; + } +} diff --git a/lib/utils/push_provider.dart b/lib/utils/push_provider.dart index e798c9bc0..2e62c5473 100644 --- a/lib/utils/push_provider.dart +++ b/lib/utils/push_provider.dart @@ -25,12 +25,14 @@ import 'package:collection/collection.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:http/http.dart'; +import 'package:pi_authenticator_legacy/pi_authenticator_legacy.dart'; +import 'package:privacyidea_authenticator/repo/secure_push_request_repository.dart'; +import 'package:privacyidea_authenticator/utils/pi_notifications.dart'; import '../l10n/app_localizations.dart'; import '../model/push_request.dart'; import '../model/tokens/push_token.dart'; import '../repo/secure_token_repository.dart'; -import '../state_notifiers/push_request_notifier.dart'; import 'firebase_utils.dart'; import 'globals.dart'; import 'logger.dart'; @@ -43,24 +45,30 @@ import 'utils.dart'; /// firebase, polling, notifications. class PushProvider { static PushProvider? instance; + // Needed for background handling + static const _defaultPushRequestRepo = SecurePushRequestRepository(); + // Needed for background handling + static const _defaultTokenRepo = SecureTokenRepository(); + bool pollingIsEnabled = false; - bool _initialized = false; Timer? _pollTimer; - PushRequestNotifier? pushSubscriber; // must be set before receiving push messages - FirebaseUtils? firebaseUtils; + final List _subscribers = []; + + FirebaseUtils _firebaseUtils; PrivacyIdeaIOClient _ioClient; RsaUtils _rsaUtils; - PushProvider._({PrivacyIdeaIOClient? ioClient, RsaUtils? rsaUtils}) - : _ioClient = ioClient ?? const PrivacyIdeaIOClient(), - _rsaUtils = rsaUtils ?? const RsaUtils(); - - Future initialize({required PushRequestNotifier pushSubscriber, required FirebaseUtils firebaseUtils}) async { - if (_initialized) return; - Logger.warning('PushProvider is already initialized', name: 'push_provider.dart#initializePushProvider'); - _initialized = true; - this.firebaseUtils = firebaseUtils; - this.pushSubscriber = pushSubscriber; - await firebaseUtils.initFirebase( + LegacyUtils _legacyUtils; + + PushProvider._({ + FirebaseUtils? firebaseUtils, + PrivacyIdeaIOClient? ioClient, + RsaUtils? rsaUtils, + LegacyUtils? legacyUtils, + }) : _firebaseUtils = firebaseUtils ?? FirebaseUtils(), + _ioClient = ioClient ?? const PrivacyIdeaIOClient(), + _rsaUtils = rsaUtils ?? const RsaUtils(), + _legacyUtils = legacyUtils ?? const LegacyUtils() { + _firebaseUtils.initFirebase( foregroundHandler: _foregroundHandler, backgroundHandler: _backgroundHandler, updateFirebaseToken: updateFirebaseToken, @@ -77,9 +85,14 @@ class PushProvider { bool? pollingEnabled, PrivacyIdeaIOClient? ioClient, RsaUtils? rsaUtils, + FirebaseUtils? firebaseUtils, }) { if (instance == null) { - instance = PushProvider._(ioClient: ioClient, rsaUtils: rsaUtils); + instance = PushProvider._( + ioClient: ioClient, + rsaUtils: rsaUtils, + firebaseUtils: firebaseUtils, + ); } else { if (ioClient != null) { instance!._ioClient = ioClient; @@ -87,10 +100,11 @@ class PushProvider { if (rsaUtils != null) { instance!._rsaUtils = rsaUtils; } + if (firebaseUtils != null) { + instance!._firebaseUtils = firebaseUtils; + } } - instance!.setPollingEnabled(pollingEnabled); - return instance!; } @@ -126,44 +140,56 @@ class PushProvider { Future _foregroundHandler(RemoteMessage remoteMessage) async { Logger.info('Foreground message received.', name: 'push_provider.dart#_foregroundHandler'); - await SecureTokenRepository.protect(() async { - Map data; - try { - data = _getAndValidateDataFromRemoteMessage(remoteMessage); - } on ArgumentError catch (_) { - Logger.info('Try requesting the challenge by polling.', name: 'push_provider.dart#_foregroundHandler'); - await pollForChallenges(isManually: true); - return; - } - // Here we can be sure that the data is valid - try { - return _handleIncomingRequestForeground(data); - } catch (e, s) { - final errorMessage = AppLocalizations.of(globalNavigatorKey.currentContext!)!.unexpectedError; - Logger.error(errorMessage, name: 'push_provider.dart#_foregroundHandler', error: e, stackTrace: s); - } - }); + Map data; + try { + data = _getAndValidateDataFromRemoteMessage(remoteMessage); + } on ArgumentError catch (_) { + Logger.info('Try requesting the challenge by polling.', name: 'push_provider.dart#_foregroundHandler'); + await pollForChallenges(isManually: true); + return; + } + // Here we can be sure that the data is valid + try { + return _handleIncomingRequestForeground(data); + } catch (e, s) { + final errorMessage = AppLocalizations.of(globalNavigatorKey.currentContext!)!.unexpectedError; + Logger.error(errorMessage, name: 'push_provider.dart#_foregroundHandler', error: e, stackTrace: s); + } } // BACKGROUND HANDLING @pragma('vm:entry-point') static Future _backgroundHandler(RemoteMessage remoteMessage) async { Logger.info('Background message received.', name: 'push_provider.dart#_backgroundHandler'); - await SecureTokenRepository.protect(() async { - Map data; - try { - data = _getAndValidateDataFromRemoteMessage(remoteMessage); - } on ArgumentError catch (_) { - return; - } - // Here we can be sure that the data is valid - try { - return _handleIncomingRequestBackground(data); - } catch (e, s) { - Logger.error('Something went wrong while handling the push request in the background.', - name: 'push_provider.dart#_backgroundHandler', error: e, stackTrace: s); - } - }); + + Map data; + try { + data = _getAndValidateDataFromRemoteMessage(remoteMessage); + } on ArgumentError catch (_) { + return; + } + // Here we can be sure that the data is valid + final bool success; + try { + success = await _handleIncomingRequestBackground(data); + } catch (e, s) { + Logger.error('Something went wrong while handling the push request in the background.', + name: 'push_provider.dart#_backgroundHandler', error: e, stackTrace: s); + return; + } + if (!success) { + Logger.warning('Handling the push request in the background failed.', name: 'push_provider.dart#_backgroundHandler'); + return; + } + // PiNotifications.show('Push request', 'A new push request has been received.'); + if (remoteMessage.notification == null) { + PiNotifications.show( + // AppLocalizations.of(globalNavigatorKey.currentContext!)!.notificationTitle, + 'Notification Title', + // AppLocalizations.of(globalNavigatorKey.currentContext!)!.notificationBody, + 'Notification Body', + ); + } } // HANDLING @@ -172,32 +198,45 @@ class PushProvider { Future _handleIncomingRequestForeground(Map data) async { Logger.info('Incoming push challenge.', name: 'push_provider.dart#_handleIncomingRequestForeground'); PushRequest pushRequest = PushRequest.fromMessageData(data); - Logger.info('Incoming push challenge for token with serial.', name: 'push_provider.dart#_handleIncomingChallenge'); - pushSubscriber?.newRequest(pushRequest); + Logger.info('Parsing data of push request succeeded.', name: 'push_provider.dart#_handleIncomingRequestForeground'); + final pushToken = globalRef?.read(tokenProvider).getTokenBySerial(pushRequest.serial); + if (pushToken == null) { + Logger.warning('No token found for serial ${pushRequest.serial}.', name: 'push_provider.dart#_handleIncomingRequestForeground'); + return; + } + if (!await pushRequest.verifySignature(pushToken, rsaUtils: _rsaUtils, legacyUtils: _legacyUtils)) { + Logger.warning('Signature verification failed.', name: 'push_provider.dart#_handleIncomingRequestForeground'); + return; + } + Logger.info('Signature verification succeeded, notifying ${_subscribers.length} subscribers.', name: 'push_provider.dart#_handleIncomingRequestForeground'); + for (var subscriber in _subscribers) { + subscriber(pushRequest); + } } // HANDLING /// Handles incoming push requests by verifying the challenge and adding it /// to the token. This should be guarded by a lock. - static Future _handleIncomingRequestBackground(Map data) async { + static Future _handleIncomingRequestBackground(Map data) async { Logger.info('Incoming push challenge.', name: 'push_provider.dart#_handleIncomingRequestBackground'); PushRequest pushRequest = PushRequest.fromMessageData(data); - Logger.info('Incoming push challenge for token with serial.', name: 'push_provider.dart#_handleIncomingRequestBackground'); - _addPushRequestToTokenInSecureStoreage(pushRequest); - } + final pushToken = (await _defaultTokenRepo.loadTokens()).whereType().firstWhereOrNull((t) => t.serial == pushRequest.serial); + if (pushToken == null) { + Logger.warning('No token found for serial ${pushRequest.serial}.', name: 'push_provider.dart#_handleIncomingRequestBackground'); + return false; + } + if (!await pushRequest.verifySignature(pushToken)) { + Logger.warning('Signature verification failed.', name: 'push_provider.dart#_handleIncomingRequestBackground'); + return false; + } - static void _addPushRequestToTokenInSecureStoreage(PushRequest pushRequest) async { - Logger.info('Adding push request to token in secure storage.', name: 'push_provider.dart#_addPushRequestToTokenInSecureStoreage'); - var tokens = await const SecureTokenRepository().loadTokens(); - PushToken? token = tokens.firstWhereOrNull((token) => token is PushToken && token.serial == pushRequest.serial) as PushToken?; - if (token == null) { - Logger.warning('Token not found.', name: 'push_provider.dart#_addPushRequestToTokenInSecureStoreage'); - return; + try { + await _defaultPushRequestRepo.add(pushRequest); + } catch (e) { + Logger.error('Could not save push request state.', name: 'push_provider.dart#_handleIncomingRequestBackground', error: e); + return false; } - final prList = token.pushRequests; - prList.add(pushRequest); - token = token.copyWith(pushRequests: prList); - await const SecureTokenRepository().saveOrReplaceTokens([token]); + return true; } void _startOrStopPolling(bool pollingEnabled) { @@ -221,7 +260,7 @@ class PushProvider { Future pollForChallenges({required bool isManually}) async { // Get all push tokens - await globalRef?.read(tokenProvider.notifier).loadingRepo; + await globalRef?.read(tokenProvider.notifier).initState; List pushTokens = globalRef?.read(tokenProvider).tokens.whereType().where((t) => t.isRolledOut && t.url != null).toList() ?? []; // Disable polling if no push tokens exist @@ -235,7 +274,7 @@ class PushProvider { } final connectivityResult = await (Connectivity().checkConnectivity()); - if (connectivityResult == ConnectivityResult.none) { + if (connectivityResult.contains(ConnectivityResult.none)) { if (isManually) { Logger.info('Tried to poll without any internet connection available.', name: 'push_provider.dart#pollForChallenges'); globalRef?.read(statusMessageProvider.notifier).state = ( @@ -257,11 +296,15 @@ class PushProvider { } Future pollForChallenge(PushToken token, {bool isManually = true}) async { + if (instance == null) { + Logger.warning('Polling push tokens failed. PushProvider is not initialized.', name: 'push_provider.dart#pollForChallenge'); + return; + } String timestamp = DateTime.now().toUtc().toIso8601String(); String message = '${token.serial}|$timestamp'; - RsaUtils rsaUtils = instance == null ? PushProvider()._rsaUtils : instance!._rsaUtils; + RsaUtils rsaUtils = instance!._rsaUtils; Logger.info(rsaUtils.runtimeType.toString(), name: 'push_provider.dart#pollForChallenge'); String? signature = await rsaUtils.trySignWithToken(token, message); if (signature == null) { @@ -338,84 +381,60 @@ class PushProvider { return; } - /// Checks if the firebase token was changed and updates it if necessary. - static Future updateFbTokenIfChanged() async { - String? firebaseToken = await instance?.firebaseUtils?.getFBToken(); - - if (firebaseToken != null && (await SecureTokenRepository.getCurrentFirebaseToken()) != firebaseToken) { - try { - await updateFirebaseToken(firebaseToken); - } catch (error, stackTrace) { - Logger.error('Could not update firebase token.', name: 'push_provider.dart#updateFbTokenIfChanged', error: error, stackTrace: stackTrace); - } - } - } - - /// This method attempts to update the fbToken for all PushTokens that can be - /// updated. I.e. all tokens that know the url of their respective privacyIDEA - /// server. - /// If the fbToken is not provided, it will be fetched from the firebase instance. - /// If the fbToken is not available, this method will return null. - /// Returns a tuple of two lists. The first list contains all tokens that - /// could not be updated. The second list contains all tokens that do not - /// support updating the fbToken. - /// - /// This should only be used to attempt to update the fbToken automatically, - /// as this can not be guaranteed to work. There is a manual option available - /// through the settings also. - static Future<(List, List)?> updateFirebaseToken([String? firebaseToken]) async { - firebaseToken ??= await instance?.firebaseUtils?.getFBToken(); - if (firebaseToken == null) { - Logger.warning('Could not update firebase token because no firebase token is available.', name: 'push_provider.dart#_updateFirebaseToken'); - return null; - } - - List tokenList = (await const SecureTokenRepository().loadTokens()).whereType().where((t) => t.url != null).toList(); + // /// Checks if the firebase token was changed and updates it if necessary. + // static Future updateFbTokenIfChanged() async { + // String? firebaseToken = await instance?._firebaseUtils.getFBToken(); - bool allUpdated = true; + // if (firebaseToken != null && (await instance?._firebaseUtils.getCurrentFirebaseToken()) != firebaseToken) { + // try { + // await updateFirebaseToken(firebaseToken); + // } catch (error, stackTrace) { + // Logger.error('Could not update firebase token.', name: 'push_provider.dart#updateFbTokenIfChanged', error: error, stackTrace: stackTrace); + // } + // } + // } - final List failedTokens = []; - final List unsuportedTokens = []; + Future<(List, List)?> updateFirebaseToken([String? firebaseToken]) async => + globalRef?.read(tokenProvider.notifier).updateFirebaseToken(firebaseToken); - for (PushToken p in tokenList) { - if (p.url == null) { - unsuportedTokens.add(p); - continue; - } - // POST /ttype/push HTTP/1.1 - //Host: example.com - // - //new_fb_token= - //serial=element - //timestamp= - //signature=SIGNATURE(||) - - String timestamp = DateTime.now().toUtc().toIso8601String(); - String message = '$firebaseToken|${p.serial}|$timestamp'; - String? signature = await const RsaUtils().trySignWithToken(p, message); - if (signature == null) { - failedTokens.add(p); - allUpdated = false; - continue; - } - Response response = instance != null - ? await instance!._ioClient.doPost( - sslVerify: p.sslVerify, url: p.url!, body: {'new_fb_token': firebaseToken, 'serial': p.serial, 'timestamp': timestamp, 'signature': signature}) - : await const PrivacyIdeaIOClient().doPost( - sslVerify: p.sslVerify, url: p.url!, body: {'new_fb_token': firebaseToken, 'serial': p.serial, 'timestamp': timestamp, 'signature': signature}); - - if (response.statusCode == 200) { - Logger.info('Updating firebase token for push token succeeded!', name: 'push_provider.dart#_updateFirebaseToken'); - } else { - Logger.warning('Updating firebase token for push token failed!', name: 'push_provider.dart#_updateFirebaseToken'); - failedTokens.add(p); - allUpdated = false; - } - } + void unsubscribe(void Function(PushRequest pushRequest) newRequest) => _subscribers.remove(newRequest); + void subscribe(void Function(PushRequest pushRequest) newRequest) => _subscribers.add(newRequest); +} - if (allUpdated) { - SecureTokenRepository.setCurrentFirebaseToken(firebaseToken); - } - return (failedTokens, unsuportedTokens); - } +class PlaceholderPushProvider implements PushProvider { + @override + FirebaseUtils _firebaseUtils = FirebaseUtils(); + @override + PrivacyIdeaIOClient _ioClient = const PrivacyIdeaIOClient(); + @override + LegacyUtils _legacyUtils = const LegacyUtils(); + @override + Timer? _pollTimer; + @override + RsaUtils _rsaUtils = const RsaUtils(); + @override + bool pollingIsEnabled = false; + @override + Future _foregroundHandler(RemoteMessage remoteMessage) async {} + @override + List> _getAndValidateDataFromResponse(Response response) => []; + @override + Future _handleIncomingRequestForeground(Map data) async {} + @override + void _startOrStopPolling(bool pollingEnabled) {} + @override + List get _subscribers => []; + @override + Future pollForChallenge(PushToken token, {bool isManually = true}) async {} + @override + Future pollForChallenges({required bool isManually}) async {} + @override + void setPollingEnabled(bool? enablePolling) {} + @override + void subscribe(void Function(PushRequest pushRequest) newRequest) {} + @override + void unsubscribe(void Function(PushRequest pushRequest) newRequest) {} + + @override + Future<(List, List)?> updateFirebaseToken([String? firebaseToken]) => Future.value(null); } diff --git a/lib/utils/riverpod_providers.dart b/lib/utils/riverpod_providers.dart index 4d4ab0614..f8ff04328 100644 --- a/lib/utils/riverpod_providers.dart +++ b/lib/utils/riverpod_providers.dart @@ -1,20 +1,22 @@ -import 'dart:ui'; +import 'dart:developer'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uni_links/uni_links.dart'; import '../l10n/app_localizations.dart'; +import '../model/extensions/sortable_list.dart'; import '../model/mixins/sortable_mixin.dart'; -import '../model/push_request.dart'; import '../model/states/introduction_state.dart'; +import '../model/states/push_request_state.dart'; import '../model/states/settings_state.dart'; import '../model/states/token_filter.dart'; import '../model/states/token_folder_state.dart'; import '../model/states/token_state.dart'; +import '../model/token_folder.dart'; import '../model/tokens/otp_token.dart'; +import '../model/tokens/token.dart'; import '../repo/preference_introduction_repository.dart'; import '../repo/preference_settings_repository.dart'; import '../repo/preference_token_folder_repository.dart'; @@ -24,7 +26,8 @@ import '../state_notifiers/push_request_notifier.dart'; import '../state_notifiers/settings_notifier.dart'; import '../state_notifiers/token_folder_notifier.dart'; import '../state_notifiers/token_notifier.dart'; -import 'app_customizer.dart'; +import 'customization/application_customization.dart'; +import 'firebase_utils.dart'; import 'globals.dart'; import 'home_widget_utils.dart'; import 'logger.dart'; @@ -49,37 +52,6 @@ final tokenProvider = StateNotifierProvider( newTokenNotifier.handleLink(newLink.uri); }); - ref.listen(pushRequestProvider, (previous, newPushRequest) { - if (newPushRequest == null) { - Logger.info("Received null pushRequest", name: 'tokenProvider#pushRequestProvider'); - return; - } - if (newPushRequest.accepted == null) { - Logger.info("Received new pushRequest", name: 'tokenProvider#pushRequestProvider'); - newTokenNotifier.addPushRequestToToken(newPushRequest); - } - if (newPushRequest.accepted != null) { - Logger.info("Received pushRequest with accepted=${newPushRequest.accepted}... removing it from state.", name: 'tokenProvider#pushRequestProvider'); - newTokenNotifier.removePushRequest(newPushRequest); - FlutterLocalNotificationsPlugin().cancelAll(); - } - }); - - ref.listen( - appStateProvider, - (previous, next) { - Logger.info('tokenProvider reviced new AppState. Changed from $previous to $next'); - if (previous == AppLifecycleState.paused && next == AppLifecycleState.resumed) { - Logger.info('Refreshing tokens on resume', name: 'tokenProvider#appStateProvider'); - newTokenNotifier.loadStateFromRepo(); - } - if (previous == AppLifecycleState.resumed && next == AppLifecycleState.paused) { - Logger.info('Saving tokens and cancelling all notifications on pause', name: 'tokenProvider#appStateProvider'); - FlutterLocalNotificationsPlugin().cancelAll(); - newTokenNotifier.saveStateToRepo(); - } - }, - ); return newTokenNotifier; }, name: 'tokenProvider', @@ -93,25 +65,33 @@ final settingsProvider = StateNotifierProvider( name: 'settingsProvider', ); -final pushRequestProvider = StateNotifierProvider( +final pushRequestProvider = StateNotifierProvider( (ref) { Logger.info("New PushRequestNotifier created", name: 'pushRequestProvider'); - final pushProvider = PushProvider(); - ref.listen(settingsProvider, (previous, next) { - if (previous?.enablePolling != next.enablePolling) { - Logger.info("Polling enabled changed from ${previous?.enablePolling} to ${next.enablePolling}", name: 'pushRequestProvider#settingsProvider'); - pushProvider.setPollingEnabled(next.enablePolling); - } - }); - + final tokenState = ref.read(tokenProvider); + PushProvider pushProvider = tokenState.hasPushTokens ? PushProvider() : PlaceholderPushProvider(); // Until the state is loaded from the repo final pushRequestNotifier = PushRequestNotifier( pushProvider: pushProvider, ); - ref.listen(appStateProvider, (previous, next) { - if (previous == AppLifecycleState.paused && next == AppLifecycleState.resumed) { - Logger.info('Polling for challenges on resume', name: 'pushRequestProvider#appStateProvider'); - pushProvider.pollForChallenges(isManually: false); + ref.listen(tokenProvider, (previous, next) { + if (previous?.hasPushTokens == true && next.hasPushTokens == false) { + /// Last push token was deleted + Logger.info('Last push token was deleted. Deactivating push provider and deleting firebase token.', name: 'pushRequestProvider#tokenProvider'); + pushRequestNotifier.swapPushProvider(PlaceholderPushProvider()); + FirebaseUtils().deleteFirebaseToken(); + } + if (previous?.hasPushTokens != true && next.hasPushTokens == true) { + /// First push token was added + Logger.info('First push token was added. Activating push provider.', name: 'pushRequestProvider#tokenProvider'); + pushRequestNotifier.swapPushProvider(PushProvider()); + } + }); + + ref.listen(settingsProvider, (previous, next) { + if (previous?.enablePolling != next.enablePolling) { + Logger.info("Polling enabled changed from ${previous?.enablePolling} to ${next.enablePolling}", name: 'pushRequestProvider#settingsProvider'); + pushRequestNotifier.pushProvider.setPollingEnabled(next.enablePolling); } }); @@ -135,25 +115,10 @@ final deeplinkProvider = StateNotifierProvider( name: 'deeplinkProvider', ); -final appStateProvider = StateProvider( - (ref) { - Logger.info("New AppStateNotifier created", name: 'appStateProvider'); - return null; - }, - name: 'appStateProvider', -); - final tokenFolderProvider = StateNotifierProvider( (ref) { Logger.info("New TokenFolderNotifier created", name: 'tokenFolderProvider'); - final newTokenFolderNotifier = TokenFolderNotifier(repository: PreferenceTokenFolderRepository()); - ref.listen(appStateProvider, (previous, next) { - if (previous == AppLifecycleState.resumed && next == AppLifecycleState.paused) { - Logger.info('Collapsing locked folders on pause', name: 'tokenFolderProvider#appStateProvider'); - newTokenFolderNotifier.collapseLockedFolders(); - } - }); - return newTokenFolderNotifier; + return TokenFolderNotifier(repository: PreferenceTokenFolderRepository()); }, name: 'tokenFolderProvider', ); @@ -168,14 +133,14 @@ final draggingSortableProvider = StateProvider( final tokenFilterProvider = StateProvider((ref) => null); -final connectivityProvider = StreamProvider( +final connectivityProvider = StreamProvider>( (ref) { Logger.info("New connectivityProvider created", name: 'connectivityProvider'); - ref.read(tokenProvider.notifier).loadingRepo.then( + ref.read(tokenProvider.notifier).initState.then( (newState) { Connectivity().checkConnectivity().then((connectivity) { Logger.info("First connectivity check: $connectivity", name: 'connectivityProvider#initialCheck'); - final hasNoConnection = connectivity == ConnectivityResult.none; + final hasNoConnection = connectivity.contains(ConnectivityResult.none); if (hasNoConnection && newState.hasPushTokens && globalNavigatorKey.currentContext != null) { ref.read(statusMessageProvider.notifier).state = (AppLocalizations.of(globalNavigatorKey.currentContext!)!.noNetworkConnection, null); } @@ -217,5 +182,33 @@ final homeWidgetProvider = StateProvider>( }, ); +final sortableProvider = StateNotifierProvider>( + (ref) { + final SortableNotifier notifier = SortableNotifier(); + Logger.info("New sortableProvider created", name: 'sortableProvider'); + ref.listen(tokenProvider, (previous, next) => notifier.handleNewList(next.tokens)); + ref.listen(tokenFolderProvider, (previous, next) => notifier.handleNewList(next.folders)); + ref.read(tokenProvider.notifier).initState.then((newState) => notifier.handleNewList(newState.tokens)); + ref.read(tokenFolderProvider.notifier).initState.then((newState) => notifier.handleNewList(newState.folders)); + return notifier; + }, +); + +class SortableNotifier extends StateNotifier> { + SortableNotifier({List initState = const []}) : super(initState); + + void handleNewList(List newList) { + log('T type: ${newList.runtimeType}', name: 'SortableNotifier#handleNewList'); + var newState = List.from(state); + newState.removeWhere((element) => element is T); + newState.addAll(newList); + state = newState.sorted.fillNullIndices(); + if (newList.any((element) => element.sortIndex == null)) { + globalRef?.read(tokenProvider.notifier).addOrReplaceTokens(state.whereType().toList()); + globalRef?.read(tokenFolderProvider.notifier).addOrReplaceFolders(state.whereType().toList()); + } + } +} + /// Only used for the app customizer final applicationCustomizerProvider = StateProvider((ref) => ApplicationCustomization.defaultCustomization); diff --git a/lib/utils/riverpod_state_listener.dart b/lib/utils/riverpod_state_listener.dart index ed5923e2d..848b91f67 100644 --- a/lib/utils/riverpod_state_listener.dart +++ b/lib/utils/riverpod_state_listener.dart @@ -9,11 +9,12 @@ import '../state_notifiers/token_notifier.dart'; import 'home_widget_utils.dart'; abstract class StateNotifierProviderListener, S> { - final StateNotifierProvider provider; - final void Function(S? previous, S next) onNewState; - const StateNotifierProviderListener({required this.provider, required this.onNewState}); + final StateNotifierProvider? provider; + final void Function(S? previous, S next)? onNewState; + const StateNotifierProviderListener({this.provider, this.onNewState}); void buildListen(WidgetRef ref) { - ref.listen(provider, onNewState); + if (provider == null || onNewState == null) return; + ref.listen(provider!, onNewState!); } } @@ -65,11 +66,20 @@ abstract class TokenStateListener extends StateNotifierProviderListener HomeWidgetUtils().handleChangedTokenState(); + static void _onNewState(TokenState? previous, TokenState next) => HomeWidgetUtils().updateTokensIfLinked(next.lastlyUpdatedTokens); } class DeepLink { final Uri uri; final bool fromInit; const DeepLink(this.uri, {this.fromInit = false}); + + @override + bool operator ==(Object other) => other is DeepLink && other.uri == uri && other.fromInit == fromInit; + + @override + int get hashCode => Object.hash(uri, fromInit); + + @override + String toString() => 'DeepLink(uri: $uri, fromInit: $fromInit)'; } diff --git a/lib/utils/supported_versions.dart b/lib/utils/supported_versions.dart deleted file mode 100644 index 040ed6501..000000000 --- a/lib/utils/supported_versions.dart +++ /dev/null @@ -1,2 +0,0 @@ -// The highest version of the pipush Tokentype that this client supports. -const maxPushTokenVersion = 1; diff --git a/lib/utils/themes.dart b/lib/utils/themes.dart deleted file mode 100644 index 8b1378917..000000000 --- a/lib/utils/themes.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/utils/token_import_origins.dart b/lib/utils/token_import_origins.dart new file mode 100644 index 000000000..a2d138975 --- /dev/null +++ b/lib/utils/token_import_origins.dart @@ -0,0 +1,118 @@ +import '../mains/main_netknights.dart'; +import '../model/enums/token_import_type.dart'; +import '../model/token_import/token_import_origin.dart'; +import '../model/token_import/token_import_source.dart'; +import '../processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart'; +import '../processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart'; +import '../processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart'; +import '../processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart'; +import '../processors/token_import_file_processor/aegis_import_file_processor.dart'; +import '../processors/token_import_file_processor/authenticator_pro_import_file_processor.dart'; +import '../processors/token_import_file_processor/free_otp_plus_import_file_processor.dart'; +import '../processors/token_import_file_processor/privacyidea_authenticator_import_file_processor.dart'; +import '../processors/token_import_file_processor/two_fas_import_file_processor.dart'; + +class TokenImportOrigins { + static final List appList = [ + privacyIDEAAuthenticator, + googleAuthenticator, + aegisAuthenticator, + twoFasAuthenticator, + authenticatorPro, + freeOtpPlus, + ]; + + static const _importSourceIconFolder = 'assets/images/import_sources/'; + + static final privacyIDEAAuthenticator = TokenImportOrigin( + appName: PrivacyIDEAAuthenticator.currentCustomization?.appName ?? 'privacyIDEA Authenticator', + iconPath: '${_importSourceIconFolder}privacyidea_authenticator.png', + importSources: [ + TokenImportSource( + processor: const PrivacyIDEAAuthenticatorQrProcessor(), + type: TokenImportType.qrScan, + importHint: (localizations) => 'localizations.importHintPrivacyIDEAQrScan', + ), + TokenImportSource( + processor: const PrivacyIDEAAuthenticatorImportFileProcessor(), + type: TokenImportType.backupFile, + importHint: (localizations) => 'localizations.importHintPrivacyIDEAFile'), + ], + ); + + static final googleAuthenticator = TokenImportOrigin( + appName: 'Google Authenticator', + iconPath: '${_importSourceIconFolder}google_authenticator.png', + importSources: [ + TokenImportSource( + processor: const GoogleAuthenticatorQrProcessor(), + type: TokenImportType.qrScan, + importHint: (localizations) => localizations.importHintGoogleQrScan, + ), + TokenImportSource( + processor: const GoogleAuthenticatorQrProcessor(), + type: TokenImportType.qrFile, + importHint: (localizations) => localizations.importHintGoogleQrFile, + ), + ], + ); + static final aegisAuthenticator = TokenImportOrigin( + appName: 'Aegis Authenticator', + iconPath: '${_importSourceIconFolder}aegis_authenticator.png', + importSources: [ + TokenImportSource( + processor: const AegisImportFileProcessor(), + type: TokenImportType.backupFile, + importHint: (localizations) => localizations.importHintAegisBackupFile, + ), + TokenImportSource( + processor: const OtpAuthProcessor(), + type: TokenImportType.qrScan, + importHint: (localizations) => localizations.importHintAegisQrScan, + ), + TokenImportSource( + processor: const OtpAuthProcessor(), + type: TokenImportType.link, + importHint: (localizations) => localizations.importHintAegisLink, + ), + ], + ); + static final twoFasAuthenticator = TokenImportOrigin( + appName: '2FAS Authenticator', + iconPath: '${_importSourceIconFolder}2fas.png', + importSources: [ + TokenImportSource( + processor: const TwoFasAuthenticatorImportFileProcessor(), + type: TokenImportType.backupFile, + importHint: (localizations) => localizations.importHint2FAS, + ), + ], + ); + static final authenticatorPro = TokenImportOrigin( + appName: 'Authenticator Pro', + iconPath: '${_importSourceIconFolder}authenticator_pro.png', + importSources: [ + TokenImportSource( + processor: const AuthenticatorProImportFileProcessor(), + type: TokenImportType.backupFile, + importHint: (localizations) => localizations.importHintAuthenticatorProFile, + ), + ], + ); + static final freeOtpPlus = TokenImportOrigin( + appName: 'FreeOTP+', + iconPath: '${_importSourceIconFolder}freeotp_plus.png', + importSources: [ + TokenImportSource( + processor: const FreeOtpPlusQrProcessor(), + type: TokenImportType.qrScan, + importHint: (localizations) => localizations.importHintFreeOtpPlusQrScan, + ), + TokenImportSource( + processor: const FreeOtpPlusImportFileProcessor(), + type: TokenImportType.backupFile, + importHint: (localizations) => localizations.importHintFreeOtpPlusFile, + ), + ], + ); +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index b258b8436..45cccf80b 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -26,11 +26,8 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:privacyidea_authenticator/model/extensions/enum_extension.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; -import '../model/enums/algorithms.dart'; - /// Inserts [char] at the position [pos] in the given String ([str]), /// and returns the resulting String. /// @@ -56,26 +53,17 @@ String splitPeriodically(String str, int period) { return result.trim(); } -Algorithms mapStringToAlgorithm(String algoAsString) { - for (Algorithms alg in Algorithms.values) { - if (alg.isString(algoAsString)) { - return alg; - } - } - throw ArgumentError.value(algoAsString, 'algorAsString', '$algoAsString cannot be mapped to $Algorithms'); -} - // / This implementation is taken from the library // / [foundation](https://api.flutter.dev/flutter/foundation/describeEnum.html). // / That library sadly depends on [dart.ui] and thus cannot be used in tests. // / Therefore, only using this code enables us to use this library ([utils.dart]) // / in tests. -String enumAsString(Enum enumEntry) { - final String description = enumEntry.toString(); - final int indexOfDot = description.indexOf('.'); - assert(indexOfDot != -1 && indexOfDot < description.length - 1); - return description.substring(indexOfDot + 1); -} +// String enumAsString(Enum enumEntry) { +// final String description = enumEntry.toString(); +// final int indexOfDot = description.indexOf('.'); +// assert(indexOfDot != -1 && indexOfDot < description.length - 1); +// return description.substring(indexOfDot + 1); +// } /// If permission is already given, this function does nothing void checkNotificationPermission() async { @@ -119,3 +107,14 @@ Size textSizeOf(String text, TextStyle style, {int? maxLines = 1, double minWidt } Future getPackageName() async => (await PackageInfo.fromPlatform()).packageName.replaceAll('.debug', ''); + +String removeIllegalFilenameChars(String filename) => filename.replaceAll(RegExp(r'[<>:"/\\|?*]'), ''); + +bool doesThrow(Function() f) { + try { + f(); + return false; + } catch (_) { + return true; + } +} diff --git a/lib/utils/view_utils.dart b/lib/utils/view_utils.dart index ebf8bf78f..a3c5328f5 100644 --- a/lib/utils/view_utils.dart +++ b/lib/utils/view_utils.dart @@ -21,12 +21,12 @@ Future showAsyncDialog({ required WidgetBuilder builder, bool barrierDismissible = true, }) { - if (globalNavigatorKey.currentContext == null) { - Logger.warning('globalNavigatorKey.currentContext is null'); + if (globalContextSync == null) { + Logger.warning('globalContextSync is null'); return Future.value(null); } return showDialog( - context: globalNavigatorKey.currentContext!, + context: globalContextSync!, builder: builder, useRootNavigator: false, barrierDismissible: barrierDismissible, diff --git a/lib/views/add_token_manually_view/add_token_manually_view.dart b/lib/views/add_token_manually_view/add_token_manually_view.dart index 474b7da7c..b5861de54 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view.dart @@ -1,17 +1,18 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:uuid/uuid.dart'; import '../../l10n/app_localizations.dart'; +import '../../mains/main_netknights.dart'; import '../../model/enums/algorithms.dart'; import '../../model/enums/encodings.dart'; import '../../model/enums/token_origin_source_type.dart'; import '../../model/enums/token_types.dart'; -import '../../model/tokens/day_password_token.dart'; -import '../../model/tokens/hotp_token.dart'; -import '../../model/tokens/otp_token.dart'; -import '../../model/tokens/totp_token.dart'; -import '../../utils/crypto_utils.dart'; +import '../../model/extensions/enums/encodings_extension.dart'; +import '../../model/extensions/enums/token_origin_source_type.dart'; +import '../../model/tokens/token.dart'; +import '../../utils/identifiers.dart'; import '../../utils/logger.dart'; import '../../utils/riverpod_providers.dart'; import 'add_token_manually_view_widgets/labeled_dropdown_button.dart'; @@ -97,26 +98,36 @@ class _AddTokenManuallyViewState extends ConsumerState { validator: (value) { if (value!.isEmpty) { return AppLocalizations.of(context)!.pleaseEnterASecretForThisToken; - } else if (!isValidEncoding(value, _encodingNotifier.value)) { + } else if ((_typeNotifier.value == TokenTypes.STEAM && Encodings.base32.isInvalidEncoding(value)) || + (_typeNotifier.value != TokenTypes.STEAM && _encodingNotifier.value.isInvalidEncoding(value))) { return AppLocalizations.of(context)!.theSecretDoesNotFitTheCurrentEncoding; } return null; }, ), - LabeledDropdownButton( - label: AppLocalizations.of(context)!.encoding, - values: Encodings.values, - valueNotifier: _encodingNotifier, + Visibility( + visible: _typeNotifier.value != TokenTypes.STEAM, + child: LabeledDropdownButton( + label: AppLocalizations.of(context)!.encoding, + values: Encodings.values, + valueNotifier: _encodingNotifier, + ), ), - LabeledDropdownButton( - label: AppLocalizations.of(context)!.algorithm, - values: Algorithms.values.reversed.toList(), - valueNotifier: _algorithmNotifier, + Visibility( + visible: _typeNotifier.value != TokenTypes.STEAM, + child: LabeledDropdownButton( + label: AppLocalizations.of(context)!.algorithm, + values: Algorithms.values.reversed.toList(), + valueNotifier: _algorithmNotifier, + ), ), - LabeledDropdownButton( - label: AppLocalizations.of(context)!.digits, - values: AddTokenManuallyView.allowedDigits, - valueNotifier: _digitsNotifier, + Visibility( + visible: _typeNotifier.value != TokenTypes.STEAM, + child: LabeledDropdownButton( + label: AppLocalizations.of(context)!.digits, + values: AddTokenManuallyView.allowedDigits, + valueNotifier: _digitsNotifier, + ), ), LabeledDropdownButton( label: AppLocalizations.of(context)!.type, @@ -168,51 +179,30 @@ class _AddTokenManuallyViewState extends ConsumerState { ); } - OTPToken? _buildTokenIfValid({required BuildContext context}) { + Token? _buildTokenIfValid({required BuildContext context}) { if (_inputIsValid(context) == false) return null; Logger.info('Input is valid, building token'); - return switch (_typeNotifier.value) { - TokenTypes.HOTP => _buildHOTPToken(), - TokenTypes.TOTP => _buildTOTPToken(), - TokenTypes.DAYPASSWORD => _buildDayPasswordToken(), - _ => null, - }; - } - HOTPToken _buildHOTPToken() { - return HOTPToken( - label: _labelController.text, - issuer: '', - id: const Uuid().v4(), - algorithm: _algorithmNotifier.value, - digits: _digitsNotifier.value, - secret: encodeSecretAs(decodeSecretToUint8(_secretController.text, _encodingNotifier.value), Encodings.base32), - origin: TokenOriginSourceType.manually.toTokenOrigin(), - ); + final uriMap = { + URI_TYPE: _typeNotifier.value.name, + URI_LABEL: _labelController.text, + URI_ISSUER: '', + URI_ALGORITHM: _algorithmNotifier.value.name, + URI_DIGITS: _digitsNotifier.value, + URI_SECRET: _encodingNotifier.value.decode(_secretController.text), + URI_COUNTER: 0, + URI_PERIOD: _typeNotifier.value == TokenTypes.DAYPASSWORD ? _periodDayPasswordNotifier.value * 60 * 60 : _periodNotifier.value, + }; + uriMap.addAll({ + URI_ORIGIN: TokenOriginSourceType.manually.toTokenOrigin( + data: jsonEncode(uriMap), + appName: PrivacyIDEAAuthenticator.currentCustomization?.appName, + isPrivacyIdeaToken: false, + ), + }); + return Token.fromUriMap(uriMap); } - TOTPToken _buildTOTPToken() => TOTPToken( - label: _labelController.text, - issuer: '', - id: const Uuid().v4(), - algorithm: _algorithmNotifier.value, - digits: _digitsNotifier.value, - secret: encodeSecretAs(decodeSecretToUint8(_secretController.text, _encodingNotifier.value), Encodings.base32), - period: _periodNotifier.value, - origin: TokenOriginSourceType.manually.toTokenOrigin(), - ); - - DayPasswordToken _buildDayPasswordToken() => DayPasswordToken( - label: _labelController.text, - issuer: '', - id: const Uuid().v4(), - algorithm: _algorithmNotifier.value, - digits: _digitsNotifier.value, - secret: encodeSecretAs(decodeSecretToUint8(_secretController.text, _encodingNotifier.value), Encodings.base32), - period: Duration(hours: _periodDayPasswordNotifier.value), - origin: TokenOriginSourceType.manually.toTokenOrigin(), - ); - /// Validates the inputs of the label and secret. bool _inputIsValid(BuildContext context) { if (_labelInputKey.currentState!.validate()) { diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart index 855706fea..10a387e23 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart @@ -1,19 +1,20 @@ import 'package:flutter/material.dart'; import '../../../utils/logger.dart'; -import '../../../utils/utils.dart'; class LabeledDropdownButton extends StatefulWidget { final String label; final List values; final ValueNotifier valueNotifier; final String postFix; + final Function(T)? onChanged; const LabeledDropdownButton({ required this.label, required this.values, required this.valueNotifier, this.postFix = '', + this.onChanged, super.key, }); @@ -47,7 +48,7 @@ class _LabeledDropdownButtonState extends State> { return DropdownMenuItem( value: value, child: Text( - '${value is Enum ? enumAsString(value) : value}' + '${value is Enum ? value.name : value}' '${widget.postFix}', style: Theme.of(context).textTheme.titleMedium, overflow: TextOverflow.fade, @@ -59,6 +60,7 @@ class _LabeledDropdownButtonState extends State> { if (newValue == null) return; setState(() { Logger.info('DropdownButton onChanged: $newValue'); + widget.onChanged?.call(newValue); widget.valueNotifier.value = newValue; }); }, diff --git a/lib/views/feedback_view/feedback_view.dart b/lib/views/feedback_view/feedback_view.dart index d157a0086..8faeb446c 100644 --- a/lib/views/feedback_view/feedback_view.dart +++ b/lib/views/feedback_view/feedback_view.dart @@ -1,15 +1,15 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + import '../../l10n/app_localizations.dart'; +import '../../utils/app_info_utils.dart'; +import '../../utils/globals.dart'; import '../../utils/pi_mailer.dart'; import '../../utils/view_utils.dart'; +import '../../widgets/dialog_widgets/default_dialog.dart'; import '../main_view/main_view.dart'; import '../view_interface.dart'; -import '../../widgets/dialog_widgets/default_dialog.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../../utils/app_info_utils.dart'; -import '../../utils/globals.dart'; class FeedbackView extends StatefulView { static const String routeName = '/feedback'; diff --git a/lib/views/import_tokens_view/import_tokens_view.dart b/lib/views/import_tokens_view/import_tokens_view.dart index 95e66c6c3..e642e29fb 100644 --- a/lib/views/import_tokens_view/import_tokens_view.dart +++ b/lib/views/import_tokens_view/import_tokens_view.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../view_interface.dart'; import '../../l10n/app_localizations.dart'; -import '../../model/token_import_origin.dart'; +import '../../model/token_import/token_import_origin.dart'; +import '../../utils/token_import_origins.dart'; +import '../view_interface.dart'; import 'pages/import_start_page.dart'; import 'pages/select_import_type_page.dart'; @@ -17,9 +18,9 @@ class ImportTokensView extends ConsumerStatefulView { static const double itemSpacingVertical = 10; static const double iconSize = 100; - final TokenImportOrigin? selectedSource; + final TokenImportOrigin? selectedOrigin; - const ImportTokensView({this.selectedSource, super.key}); + const ImportTokensView({this.selectedOrigin, super.key}); @override ConsumerState createState() => _ImportTokensViewState(); @@ -27,18 +28,18 @@ class ImportTokensView extends ConsumerStatefulView { class _ImportTokensViewState extends ConsumerState { void _onPressed(TokenImportOrigin tokenImportOrigin) { - if (tokenImportOrigin.importEntitys.length == 1) { + if (tokenImportOrigin.importSources.length == 1) { Navigator.of(context).push( MaterialPageRoute( builder: (context) => ImportStartPage( appName: tokenImportOrigin.appName, - selectedEntity: tokenImportOrigin.importEntitys.first, + selectedSource: tokenImportOrigin.importSources.first, ), ), ); return; } - Navigator.of(context).push(MaterialPageRoute(builder: (context) => SelectImportTypePage(tokenImportSource: tokenImportOrigin))); + Navigator.of(context).push(MaterialPageRoute(builder: (context) => SelectImportTypePage(tokenImportOrigin: tokenImportOrigin))); } @override @@ -55,7 +56,7 @@ class _ImportTokensViewState extends ConsumerState { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ - for (final item in TokenImportSourceList.appList) + for (final item in TokenImportOrigins.appList) ListTile( // leading: Image.asset(appList[index].iconPath!), title: TextButton( diff --git a/lib/views/import_tokens_view/pages/import_encrypted_data_page.dart b/lib/views/import_tokens_view/pages/import_encrypted_data_page.dart index acd70f7c0..c161eacaf 100644 --- a/lib/views/import_tokens_view/pages/import_encrypted_data_page.dart +++ b/lib/views/import_tokens_view/pages/import_encrypted_data_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../../l10n/app_localizations.dart'; import '../../../model/enums/token_import_type.dart'; +import '../../../model/processor_result.dart'; import '../../../model/tokens/token.dart'; import '../../../processors/mixins/token_import_processor.dart'; import '../../../processors/token_import_file_processor/two_fas_import_file_processor.dart'; @@ -107,10 +108,9 @@ class _ImportEncryptedDataPageState extends State { setState(() { future = Future( () async { - // Future.delayed(const Duration(seconds: 1)).then((value) => null); - List tokens; try { - tokens = await widget.processor.processTokenMigrate(widget.data, args: _passwordController.text); + final processorResults = await widget.processor.processTokenMigrate(widget.data, args: _passwordController.text); + _pushImportPlainTokensPage(processorResults); } on BadDecryptionPasswordException catch (_) { setState(() { wrongPassword = true; @@ -118,12 +118,8 @@ class _ImportEncryptedDataPageState extends State { }); return; } - setState(() { - future = null; - }); - _pushImportPlainTokensPage(tokens); }, - ); + )..then((_) => setState(() => future = null)); }); }, child: Text( @@ -141,12 +137,12 @@ class _ImportEncryptedDataPageState extends State { ), ); - void _pushImportPlainTokensPage(List tokens) { + void _pushImportPlainTokensPage(List> processorResults) { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => ImportPlainTokensPage( appName: widget.appName, - importedTokens: tokens, + processorResults: processorResults, selectedType: widget.selectedType, ), ), diff --git a/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart b/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart index 0099fdc2e..5771f61a8 100644 --- a/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart +++ b/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart @@ -3,18 +3,38 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../l10n/app_localizations.dart'; import '../../../model/enums/token_import_type.dart'; +import '../../../model/extensions/enums/token_import_type_extension.dart'; +import '../../../model/processor_result.dart'; import '../../../model/tokens/token.dart'; import '../../../utils/riverpod_providers.dart'; import '../import_tokens_view.dart'; import '../widgets/conflicted_import_tokens_list.dart'; import '../widgets/conflicted_import_tokens_tile.dart'; +import '../widgets/failed_imports_list.dart'; import '../widgets/no_conflict_import_tokens_list.dart'; class ImportPlainTokensPage extends ConsumerStatefulWidget { final String appName; final TokenImportType selectedType; final List importedTokens; - const ImportPlainTokensPage({super.key, required this.importedTokens, required this.appName, required this.selectedType}); + final List failedImports; + factory ImportPlainTokensPage({ + Key? key, + required List> processorResults, + required String appName, + required TokenImportType selectedType, + }) { + final importedTokens = processorResults.whereType>().map((e) => e.resultData).toList(); + final failedImports = processorResults.whereType().map((e) => e.message).toList(); + return ImportPlainTokensPage._( + key: key, + importedTokens: importedTokens, + failedImports: failedImports, + appName: appName, + selectedType: selectedType, + ); + } + const ImportPlainTokensPage._({super.key, required this.importedTokens, required this.failedImports, required this.appName, required this.selectedType}); @override ConsumerState createState() => _ImportFileNoPwState(); @@ -24,13 +44,13 @@ class _ImportFileNoPwState extends ConsumerState { ScrollController scrollController = ScrollController(); List? tokensToKeep; List importTokenEntrys = []; - bool isMaxScrollExtent = true; + bool isMaxScrollOffset = true; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - final map = ref.read(tokenProvider).tokensWithSameSectet(widget.importedTokens); + final map = ref.read(tokenProvider).getSameTokens(widget.importedTokens); importTokenEntrys = []; setState(() { map.forEach((key, value) { @@ -39,10 +59,8 @@ class _ImportFileNoPwState extends ConsumerState { }); _setTokensToKeep(importTokenEntrys); }); - - WidgetsBinding.instance.addPostFrameCallback((_) { - scrollController.addListener(_updateIsMaxScrollExtent); - }); + scrollController.addListener(_updateIsMaxScrollExtent); + _updateIsMaxScrollExtent(); } @override @@ -56,26 +74,26 @@ class _ImportFileNoPwState extends ConsumerState { Navigator.of(context).popUntil((route) => route.isFirst); } - void _updateIsMaxScrollExtent() { - if (scrollController.position.maxScrollExtent <= scrollController.offset) { - if (isMaxScrollExtent) return; - setState(() { - isMaxScrollExtent = true; - }); - } else { - if (!isMaxScrollExtent) return; - setState(() { - isMaxScrollExtent = false; - }); - } + void _updateIsMaxScrollExtent() async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await Future.delayed(const Duration(milliseconds: 100)); + if (scrollController.position.maxScrollExtent <= scrollController.offset) { + if (isMaxScrollOffset || !mounted) return; + setState(() { + isMaxScrollOffset = true; + }); + } else { + if (!isMaxScrollOffset || !mounted) return; + setState(() { + isMaxScrollOffset = false; + }); + } + }); } @override Widget build(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _updateIsMaxScrollExtent(); - }); - + _updateIsMaxScrollExtent(); final List conflictedImports = []; final List newImports = []; final List duplicateImport = []; @@ -117,11 +135,15 @@ class _ImportFileNoPwState extends ConsumerState { const SizedBox(height: ImportTokensView.itemSpacingHorizontal), Column( children: [ + if (widget.failedImports.isNotEmpty) + FailedImportsList( + failedImports: widget.failedImports, + ), if (conflictedImports.isNotEmpty) ConflictedImportTokensList( title: AppLocalizations.of(context)!.importConflictToken(conflictedImports.length), titlePadding: const EdgeInsets.symmetric(horizontal: 40), - leadingDivider: false, + leadingDivider: widget.failedImports.isNotEmpty, importEntries: conflictedImports, updateImportTokenEntry: _updateImportTokenEntry, ), @@ -147,7 +169,7 @@ class _ImportFileNoPwState extends ConsumerState { ), ), AnimatedOpacity( - opacity: isMaxScrollExtent ? 0 : 1, + opacity: isMaxScrollOffset ? 0 : 1, duration: const Duration(milliseconds: 250), child: const Divider( thickness: 2, diff --git a/lib/views/import_tokens_view/pages/import_start_page.dart b/lib/views/import_tokens_view/pages/import_start_page.dart index 5952fcae3..d53879944 100644 --- a/lib/views/import_tokens_view/pages/import_start_page.dart +++ b/lib/views/import_tokens_view/pages/import_start_page.dart @@ -7,14 +7,14 @@ import 'package:flutter/material.dart'; import 'package:image/image.dart' as img; import 'package:privacyidea_authenticator/model/enums/token_import_type.dart'; import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; -import 'package:privacyidea_authenticator/processors/scheme_processors/scheme_processor_interface.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_import_type_extension.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/token_import_scheme_processor_interface.dart'; import 'package:zxing2/qrcode.dart'; -// ignore: implementation_imports -import 'package:zxing2/src/format_reader_exception.dart'; import '../../../l10n/app_localizations.dart'; -import '../../../model/token_import_origin.dart'; +import '../../../model/token_import/token_import_source.dart'; import '../../../model/tokens/token.dart'; import '../../../processors/mixins/token_import_processor.dart'; import '../../../processors/token_import_file_processor/token_import_file_processor_interface.dart'; @@ -48,12 +48,12 @@ void _decodeQrFileIsolate(List args) async { class ImportStartPage extends StatefulWidget { final String appName; - final TokenImportEntity selectedEntity; + final TokenImportSource selectedSource; const ImportStartPage({ super.key, required this.appName, - required this.selectedEntity, + required this.selectedSource, }); @override @@ -76,6 +76,7 @@ class _ImportStartPageState extends State { @override Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( title: Text(widget.appName), @@ -89,7 +90,7 @@ class _ImportStartPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( - widget.selectedEntity.type.icon, + widget.selectedSource.type.icon, color: _errorText != null ? Theme.of(context).colorScheme.error : null, size: ImportTokensView.iconSize, ), @@ -99,13 +100,13 @@ class _ImportStartPageState extends State { _errorText!, textAlign: TextAlign.center, ) - : Text(widget.selectedEntity.importHint(context), textAlign: TextAlign.center), - if (widget.selectedEntity.type == TokenImportType.link) ...[ + : Text(widget.selectedSource.importHint(localizations), textAlign: TextAlign.center), + if (widget.selectedSource.type == TokenImportType.link) ...[ const SizedBox(height: ImportTokensView.itemSpacingHorizontal), TextField( controller: _linkController, decoration: InputDecoration( - labelText: AppLocalizations.of(context)!.tokenLink, + labelText: localizations.tokenLink, ), ), ], @@ -115,7 +116,7 @@ class _ImportStartPageState extends State { width: double.infinity, child: ElevatedButton( child: Text( - widget.selectedEntity.type.getButtonText(context), + widget.selectedSource.type.buttonText(localizations), style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimary), overflow: TextOverflow.fade, softWrap: false, @@ -123,11 +124,11 @@ class _ImportStartPageState extends State { onPressed: () { setState(() => _errorText = null); setState(() { - future = Future(() => switch (widget.selectedEntity.type) { - const (TokenImportType.backupFile) => _pickBackupFile(widget.selectedEntity.processor), - const (TokenImportType.qrScan) => _scanQrCode(widget.selectedEntity.processor), - const (TokenImportType.qrFile) => _pickQrFile(widget.selectedEntity.processor), - const (TokenImportType.link) => _validateLink(widget.selectedEntity.processor), + future = Future(() => switch (widget.selectedSource.type) { + const (TokenImportType.backupFile) => _pickBackupFile(widget.selectedSource.processor), + const (TokenImportType.qrScan) => _scanQrCode(widget.selectedSource.processor), + const (TokenImportType.qrFile) => _pickQrFile(widget.selectedSource.processor), + const (TokenImportType.link) => _validateLink(widget.selectedSource.processor), }); future!.then((value) { if (mounted == false) return; @@ -149,33 +150,55 @@ class _ImportStartPageState extends State { Future _pickBackupFile(TokenImportProcessor? processor) async { assert(processor is TokenImportFileProcessor); final fileProcessor = processor as TokenImportFileProcessor; - final XTypeGroup typeGroup = XTypeGroup(label: AppLocalizations.of(context)!.selectFile); + final localizations = AppLocalizations.of(context)!; + final XTypeGroup typeGroup = XTypeGroup(label: localizations.selectFile); final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]); if (file == null) { Logger.warning("No file selected", name: "_pickAFile#ImportSelectFilePage"); return; } - if (await fileProcessor.fileIsValid(file: file) == false) { + if (await fileProcessor.fileIsValid(file) == false) { if (mounted == false) return; - setState(() => _errorText = AppLocalizations.of(context)!.invalidBackupFile(widget.appName)); + setState(() => _errorText = localizations.invalidBackupFile(widget.appName)); return; } setState(() => _errorText = null); - if (await fileProcessor.fileNeedsPassword(file: file)) { + if (await fileProcessor.fileNeedsPassword(file)) { _routeEncryptedData(data: file, processor: fileProcessor); return; } - final tokens = await fileProcessor.processFile(file: file); - if (tokens.isEmpty) { + var importResults = await fileProcessor.processFile(file); + if (importResults.isEmpty) { if (mounted == false) return; - setState(() => _errorText = AppLocalizations.of(context)!.invalidBackupFile(widget.appName)); + setState(() => _errorText = localizations.invalidBackupFile(widget.appName)); return; } - _routeImportPlainTokensPage(tokens: tokens); + String fileString; + try { + fileString = await file.readAsString(); + // ignore: empty_catches + } catch (e) { + fileString = 'No data'; + } + + importResults = importResults.map>((t) { + if (t is! ProcessorResultSuccess) return t; + return ProcessorResultSuccess( + TokenOriginSourceType.backupFile.addOriginToToken( + appName: widget.appName, + token: t.resultData, + isPrivacyIdeaToken: false, + data: t.resultData.origin?.data ?? fileString, + ), + ); + }).toList(); + + _routeImportPlainTokensPage(importResults: importResults); } Future _scanQrCode(TokenImportProcessor? processor) async { assert(processor is TokenImportSchemeProcessor); + final localizations = AppLocalizations.of(context)!; final schemeProcessor = processor as TokenImportSchemeProcessor; final result = await Navigator.of(context).pushNamed(QRScannerView.routeName); if (result is! String) return; @@ -184,61 +207,81 @@ class _ImportStartPageState extends State { uri = Uri.parse(result); } on FormatException catch (_) { if (mounted == false) return; - setState(() => _errorText = AppLocalizations.of(context)!.invalidQrScan(widget.appName)); + setState(() => _errorText = localizations.invalidQrScan(widget.appName)); return; } - List tokens = await schemeProcessor.processUri(uri); - tokens = tokens.map((token) => TokenOriginSourceType.qrScan.addOriginToToken(token: token, data: result, appName: widget.appName)).toList(); - _routeImportPlainTokensPage(tokens: tokens); + var results = await schemeProcessor.processUri(uri); + results = results.map>((t) { + if (t is! ProcessorResultSuccess) return t; + return ProcessorResultSuccess( + TokenOriginSourceType.qrScan.addOriginToToken( + appName: widget.appName, + isPrivacyIdeaToken: false, + token: t.resultData, + data: t.resultData.origin?.data ?? uri.toString(), + ), + ); + }).toList(); + _routeImportPlainTokensPage(importResults: results); return; } - Future _startDecodeQrFile(XFile file) async { - final receivePort = ReceivePort(); - try { - await Isolate.spawn(_decodeQrFileIsolate, [receivePort.sendPort, file]); - } catch (_) { - receivePort.close(); - rethrow; - } - final result = await receivePort.first; - if (result is! Result) { - throw result; - } - receivePort.close(); - return result; - } - Future _pickQrFile(TokenImportProcessor? processor) async { assert(processor is TokenImportSchemeProcessor); - final schemeProcessor = processor as SchemeProcessor; + final schemeProcessor = processor as TokenImportSchemeProcessor; + final localizations = AppLocalizations.of(context)!; final XFile? file = await openFile(); if (file == null) return; - Result result; + Result qrResult; try { - result = await _startDecodeQrFile(file); + qrResult = await _startDecodeQrFile(file); } on FormatReaderException catch (_) { - setState(() => _errorText = AppLocalizations.of(context)!.qrFileDecodeError); + setState(() => _errorText = localizations.qrFileDecodeError); return; } catch (e) { - setState(() => _errorText = AppLocalizations.of(context)!.invalidQrFile(widget.appName)); + setState(() => _errorText = localizations.invalidQrFile(widget.appName)); return; } final Uri uri; try { - uri = Uri.parse(result.text); + uri = Uri.parse(qrResult.text); } on FormatException catch (_) { - setState(() => _errorText = AppLocalizations.of(context)!.invalidQrFile(widget.appName)); + setState(() => _errorText = localizations.invalidQrFile(widget.appName)); return; } - List tokens = await schemeProcessor.processUri(uri); - if (tokens.isEmpty) { - setState(() => _errorText = AppLocalizations.of(context)!.invalidQrFile(widget.appName)); + var processorResults = await schemeProcessor.processUri(uri); + if (processorResults.isEmpty) { + setState(() => _errorText = localizations.invalidQrFile(widget.appName)); return; } - tokens = tokens.map((token) => TokenOriginSourceType.qrFile.addOriginToToken(token: token, data: result.text, appName: widget.appName)).toList(); + processorResults = processorResults.map>((t) { + if (t is! ProcessorResultSuccess) return t; + return ProcessorResultSuccess( + TokenOriginSourceType.qrFile.addOriginToToken( + appName: widget.appName, + token: t.resultData, + isPrivacyIdeaToken: false, + data: t.resultData.origin?.data ?? qrResult.text, + ), + ); + }).toList(); + _routeImportPlainTokensPage(importResults: processorResults); + } - _routeImportPlainTokensPage(tokens: tokens); + Future _startDecodeQrFile(XFile file) async { + final receivePort = ReceivePort(); + try { + await Isolate.spawn(_decodeQrFileIsolate, [receivePort.sendPort, file]); + } catch (_) { + receivePort.close(); + rethrow; + } + final result = await receivePort.first; + if (result is! Result) { + throw result; + } + receivePort.close(); + return result; } Future _validateLink(TokenImportProcessor? processor) async { @@ -246,33 +289,43 @@ class _ImportStartPageState extends State { return; } assert(processor is TokenImportSchemeProcessor); + final localizations = AppLocalizations.of(context)!; final schemeProcessor = processor as TokenImportSchemeProcessor; final Uri uri; try { uri = Uri.parse(_linkController.text); } on FormatException catch (_) { - setState(() => _errorText = AppLocalizations.of(context)!.invalidLink(widget.appName)); + setState(() => _errorText = localizations.invalidLink(widget.appName)); return; } - List tokens = await schemeProcessor.processUri(uri); - if (tokens.isEmpty) { - setState(() => _errorText = AppLocalizations.of(context)!.invalidLink(widget.appName)); + var results = await schemeProcessor.processUri(uri); + if (results.isEmpty) { + setState(() => _errorText = localizations.invalidLink(widget.appName)); return; } - tokens = tokens.map((token) => TokenOriginSourceType.link.addOriginToToken(token: token, data: _linkController.text, appName: widget.appName)).toList(); + results = results.map>((t) { + if (t is! ProcessorResultSuccess) return t; + return ProcessorResultSuccess(TokenOriginSourceType.linkImport.addOriginToToken( + appName: widget.appName, + token: t.resultData, + isPrivacyIdeaToken: false, + data: _linkController.text, + )); + }).toList(); + if (mounted == false) return; setState(() => FocusScope.of(context).unfocus()); - _routeImportPlainTokensPage(tokens: tokens); + _routeImportPlainTokensPage(importResults: results); } - void _routeImportPlainTokensPage({required List tokens}) { + void _routeImportPlainTokensPage({required List> importResults}) { if (mounted == false) return; Navigator.of(context).push( MaterialPageRoute(builder: (context) { return ImportPlainTokensPage( appName: widget.appName, - importedTokens: tokens, - selectedType: widget.selectedEntity.type, + processorResults: importResults, + selectedType: widget.selectedSource.type, ); }), ); @@ -285,7 +338,7 @@ class _ImportStartPageState extends State { return ImportEncryptedDataPage( appName: widget.appName, data: data, - selectedType: widget.selectedEntity.type, + selectedType: widget.selectedSource.type, processor: processor, ); }), diff --git a/lib/views/import_tokens_view/pages/select_import_type_page.dart b/lib/views/import_tokens_view/pages/select_import_type_page.dart index 9084f2420..979bf28d7 100644 --- a/lib/views/import_tokens_view/pages/select_import_type_page.dart +++ b/lib/views/import_tokens_view/pages/select_import_type_page.dart @@ -1,24 +1,25 @@ -// ignore_for_file: prefer_const_constructors - import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import '../../../l10n/app_localizations.dart'; import '../../../model/enums/token_import_type.dart'; -import '../../../model/token_import_origin.dart'; +import '../../../model/extensions/enums/token_import_type_extension.dart'; +import '../../../model/token_import/token_import_origin.dart'; +import '../../../model/token_import/token_import_source.dart'; import '../import_tokens_view.dart'; import 'import_start_page.dart'; class SelectImportTypePage extends StatelessWidget { - final TokenImportOrigin tokenImportSource; + final TokenImportOrigin tokenImportOrigin; - const SelectImportTypePage({super.key, required this.tokenImportSource}); + const SelectImportTypePage({super.key, required this.tokenImportOrigin}); @override Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( - title: Text(tokenImportSource.appName), + title: Text(tokenImportOrigin.appName), ), body: Center( child: SingleChildScrollView( @@ -37,11 +38,11 @@ class SelectImportTypePage extends StatelessWidget { ), const SizedBox(height: ImportTokensView.itemSpacingHorizontal), Text( - AppLocalizations.of(context)!.selectImportType, + localizations.selectImportType, textAlign: TextAlign.center, ), const SizedBox(height: ImportTokensView.itemSpacingHorizontal), - for (final importEntity in tokenImportSource.importEntitys) + for (final importEntity in tokenImportOrigin.importSources) SizedBox( width: double.infinity, child: ElevatedButton( @@ -51,10 +52,10 @@ class SelectImportTypePage extends StatelessWidget { flex: 8, child: Text( switch (importEntity.type) { - const (TokenImportType.backupFile) => AppLocalizations.of(context)!.selectFile, - const (TokenImportType.qrScan) => AppLocalizations.of(context)!.scanQrCode, - const (TokenImportType.qrFile) => AppLocalizations.of(context)!.selectFile, - const (TokenImportType.link) => AppLocalizations.of(context)!.enterLink, + const (TokenImportType.backupFile) => localizations.selectFile, + const (TokenImportType.qrScan) => localizations.scanQrCode, + const (TokenImportType.qrFile) => localizations.selectFile, + const (TokenImportType.link) => localizations.enterLink, }, style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimary), overflow: TextOverflow.fade, @@ -65,10 +66,10 @@ class SelectImportTypePage extends StatelessWidget { Expanded( child: Icon(importEntity.type.icon), ), - Expanded(child: SizedBox()), + const Expanded(child: SizedBox()), ], ), - onPressed: () => _routeStartPage(context: context, importEntity: importEntity), + onPressed: () => _routeStartPage(context: context, importSource: importEntity), ), ), const SizedBox(height: ImportTokensView.itemSpacingHorizontal), @@ -80,6 +81,6 @@ class SelectImportTypePage extends StatelessWidget { ); } - void _routeStartPage({required TokenImportEntity importEntity, required BuildContext context}) => - Navigator.of(context).push(MaterialPageRoute(builder: (context) => ImportStartPage(appName: tokenImportSource.appName, selectedEntity: importEntity))); + void _routeStartPage({required TokenImportSource importSource, required BuildContext context}) => + Navigator.of(context).push(MaterialPageRoute(builder: (context) => ImportStartPage(appName: tokenImportOrigin.appName, selectedSource: importSource))); } diff --git a/lib/views/import_tokens_view/widgets/failed_imports_list.dart b/lib/views/import_tokens_view/widgets/failed_imports_list.dart new file mode 100644 index 000000000..738d7d225 --- /dev/null +++ b/lib/views/import_tokens_view/widgets/failed_imports_list.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import '../../../l10n/app_localizations.dart'; + +class FailedImportsList extends StatelessWidget { + final List failedImports; + + const FailedImportsList({ + super.key, + required this.failedImports, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + AppLocalizations.of(context)!.importFailedToken(failedImports.length), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + for (var i = 0; i < failedImports.length; i++) + Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text( + '${i + 1}.', + textAlign: TextAlign.right, + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 5, + child: Text( + '${failedImports[i]}', + textAlign: TextAlign.left, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_list.dart b/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_list.dart index eb11f5c0a..d7dbffd27 100644 --- a/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_list.dart +++ b/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_list.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../../model/tokens/token.dart'; +import '../../../model/tokens/token.dart'; import 'no_conflict_import_tokens_tile.dart'; class NoConflictImportTokensList extends StatefulWidget { diff --git a/lib/views/link_home_widget_view/link_home_widget_view.dart b/lib/views/link_home_widget_view/link_home_widget_view.dart index fdce9a80e..4c3ef75b4 100644 --- a/lib/views/link_home_widget_view/link_home_widget_view.dart +++ b/lib/views/link_home_widget_view/link_home_widget_view.dart @@ -2,7 +2,7 @@ import 'package:app_minimizer/app_minimizer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../utils/app_customizer.dart'; +import '../../utils/customization/extended_text_theme.dart'; import '../../utils/home_widget_utils.dart'; import '../../utils/riverpod_providers.dart'; import '../../utils/utils.dart'; @@ -35,24 +35,20 @@ class _LinkHomeWidgetViewState extends ConsumerState { body: ListView.builder( itemBuilder: (context, index) { final otpToken = otpTokens[index]; - final folderIsLocked = ref.watch(tokenFolderProvider).getFolderById(otpToken.folderId)?.isLocked ?? false; + final folderIsLocked = ref.watch(tokenFolderProvider).currentById(otpToken.folderId)?.isLocked ?? false; final otpString = otpToken.isLocked || folderIsLocked ? veilingCharacter * otpToken.otpValue.length : otpToken.otpValue; return ListTile( title: Text(otpToken.label), - subtitle: Text(splitPeriodically(otpString, otpString.length ~/ 2)), + subtitle: Text(insertCharAt(otpString, ' ', (otpString.length / 2).ceil())), onTap: alreadyTapped ? () {} : () async { if (alreadyTapped) return; - setState(() { - alreadyTapped = true; - }); + setState(() => alreadyTapped = true); await HomeWidgetUtils().link(widget.homeWidgetId, otpToken.id); await FlutterAppMinimizer.minimize(); await Future.delayed(const Duration(milliseconds: 500)); - if (mounted) { - Navigator.pop(context); - } + if (context.mounted) Navigator.pop(context); }, ); }, diff --git a/lib/views/main_view/main_view.dart b/lib/views/main_view/main_view.dart index 336e2fc4c..ab1007102 100644 --- a/lib/views/main_view/main_view.dart +++ b/lib/views/main_view/main_view.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutterlifecyclehooks/flutterlifecyclehooks.dart'; +import '../../l10n/app_localizations.dart'; import '../../model/states/token_filter.dart'; -import '../../utils/logger.dart'; import '../../utils/patch_notes_utils.dart'; import '../../utils/riverpod_providers.dart'; import '../../widgets/push_request_listener.dart'; @@ -34,7 +33,7 @@ class MainView extends ConsumerStatefulView { ConsumerState createState() => _MainViewState(); } -class _MainViewState extends ConsumerState with LifecycleMixin { +class _MainViewState extends ConsumerState { final globalKey = GlobalKey(); @override @@ -47,18 +46,6 @@ class _MainViewState extends ConsumerState with LifecycleMixin { }); } - @override - void onAppResume() { - Logger.info('MainView Resume', name: 'main_view.dart#onAppResume'); - globalRef?.read(appStateProvider.notifier).state = AppLifecycleState.resumed; - } - - @override - void onAppPause() { - Logger.info('MainView Pause', name: 'main_view.dart#onAppPause'); - globalRef?.read(appStateProvider.notifier).state = AppLifecycleState.paused; - } - @override Widget build(BuildContext context) { final hasFilter = ref.watch(tokenFilterProvider) != null; @@ -82,12 +69,14 @@ class _MainViewState extends ConsumerState with LifecycleMixin { actions: [ hasFilter ? AppBarItem( + tooltip: AppLocalizations.of(context)!.closeSearchTokens, onPressed: () { ref.read(tokenFilterProvider.notifier).state = null; }, icon: const Icon(Icons.close), ) : AppBarItem( + tooltip: AppLocalizations.of(context)!.searchTokens, onPressed: () { ref.read(tokenFilterProvider.notifier).state = TokenFilter( searchQuery: '', diff --git a/lib/views/main_view/main_view_widgets/app_bar_item.dart b/lib/views/main_view/main_view_widgets/app_bar_item.dart index c895c2751..ba285459b 100644 --- a/lib/views/main_view/main_view_widgets/app_bar_item.dart +++ b/lib/views/main_view/main_view_widgets/app_bar_item.dart @@ -1,23 +1,23 @@ import 'package:flutter/material.dart'; class AppBarItem extends StatelessWidget { - const AppBarItem({super.key, required this.onPressed, required this.icon}); + const AppBarItem({super.key, required this.onPressed, required this.icon, required this.tooltip}); final VoidCallback onPressed; + final String tooltip; final Widget icon; @override - Widget build(BuildContext context) { - return IconButton( - padding: const EdgeInsets.all(0), - splashRadius: 20, - onPressed: onPressed, - color: Theme.of(context).navigationBarTheme.iconTheme?.resolve({})?.color, - icon: SizedBox( - height: 24, - width: 24, - child: FittedBox(child: icon), - ), - ); - } + Widget build(BuildContext context) => IconButton( + tooltip: tooltip, + padding: const EdgeInsets.all(0), + splashRadius: 20, + onPressed: onPressed, + color: Theme.of(context).navigationBarTheme.iconTheme?.resolve({})?.color, + icon: SizedBox( + height: 24, + width: 24, + child: FittedBox(child: icon), + ), + ); } diff --git a/lib/views/main_view/main_view_widgets/connectivity_listener.dart b/lib/views/main_view/main_view_widgets/connectivity_listener.dart index c13bc402c..98e87ac66 100644 --- a/lib/views/main_view/main_view_widgets/connectivity_listener.dart +++ b/lib/views/main_view/main_view_widgets/connectivity_listener.dart @@ -13,8 +13,8 @@ class ConnectivityListener extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityProvider).asData?.value; - if (connectivity != null && connectivity == ConnectivityResult.none) { - ref.read(tokenProvider.notifier).loadingRepo.then((newState) { + if (connectivity != null && connectivity.contains(ConnectivityResult.none)) { + ref.read(tokenProvider.notifier).initState.then((newState) { if (newState.hasPushTokens) { Logger.info("Connectivity changed: $connectivity"); ref.read(statusMessageProvider.notifier).state = (AppLocalizations.of(context)!.noNetworkConnection, null); diff --git a/lib/views/main_view/main_view_widgets/drag_target_divider.dart b/lib/views/main_view/main_view_widgets/drag_target_divider.dart index 64180d088..f07fbb7e5 100644 --- a/lib/views/main_view/main_view_widgets/drag_target_divider.dart +++ b/lib/views/main_view/main_view_widgets/drag_target_divider.dart @@ -1,7 +1,9 @@ -import 'package:collection/collection.dart'; +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../model/extensions/sortable_list.dart'; import '../../../model/mixins/sortable_mixin.dart'; import '../../../model/token_folder.dart'; import '../../../model/tokens/token.dart'; @@ -12,16 +14,24 @@ import '../../../widgets/drag_item_scroller.dart'; /// It will accept a Sortable from the type T class DragTargetDivider extends ConsumerStatefulWidget { final TokenFolder? dependingFolder; + final SortableMixin? previousSortable; final SortableMixin? nextSortable; - final double? bottomPaddingIfLast; + final double dividerBaseHeight; + final double dividerExpandedHeight; + final double bottomPaddingIfLast; + final bool isExpandalbe; final bool ignoreFolderId; final bool isLastDivider; const DragTargetDivider({ super.key, required this.dependingFolder, + required this.previousSortable, required this.nextSortable, - this.bottomPaddingIfLast, + this.bottomPaddingIfLast = 0, + this.dividerBaseHeight = 1.5, + this.dividerExpandedHeight = 40, + this.isExpandalbe = true, this.ignoreFolderId = false, this.isLastDivider = false, }); @@ -52,10 +62,10 @@ class _DragTargetDividerState extends ConsumerState DragTarget( - onWillAccept: (data) { - final willAccept = _onWillAccept(data, ref); - if (willAccept) { + Widget build(BuildContext context) => DragTarget( + onWillAcceptWithDetails: (details) { + final willAccept = _onWillAccept(details.data, ref); + if (willAccept && widget.isExpandalbe) { expansionController.forward(); } return willAccept; @@ -63,10 +73,11 @@ class _DragTargetDividerState extends ConsumerState extends ConsumerState(Object? data, WidgetRef ref) { - if (data is! T) return false; +bool _onWillAccept(SortableMixin? data, WidgetRef ref) { if (ref.read(dragItemScrollerStateProvider)) return false; - return true; } void _onAccept({ - required Object? dragedSortable, - SortableMixin? nextSortable, + required SortableMixin? previousSortable, + required SortableMixin dragedSortable, + required SortableMixin? nextSortable, required bool ignoreFolderId, TokenFolder? dependingFolder, required WidgetRef ref, }) { - if (dragedSortable is! SortableMixin) return; - // Higher index = lower in the list final allTokens = ref.read(tokenProvider).tokens; final allFolders = ref.read(tokenFolderProvider).folders; - final allSortables = [...allTokens, ...allFolders]; - allSortables.sort((a, b) => a.compareTo(b)); - final oldIndex = allSortables.indexOf(dragedSortable); - if (oldIndex == -1) return; // If the draged item is not in the list we dont need to do anything - int newIndex; - if (nextSortable == null) { - // If the draged item is moved to the end of the list the nextSortable is null. The newIndex will be set to the last index - newIndex = allSortables.length - 1; - } else { - if (oldIndex < allSortables.indexOf(nextSortable)) { - // If the draged item is moved down it dont pass the nextSortable so the newIndex is before the nextSortable - newIndex = allSortables.indexOf(nextSortable) - 1; - } else { - // If the draged item is moved up it pass the nextSortable so the newIndex will be the place of the nextSortable - newIndex = allSortables.indexOf(nextSortable); - } - } - final dragedItemMovedUp = newIndex < oldIndex; + var allSortables = [...allTokens, ...allFolders]; - final modifiedSortables = []; - for (var i = 0; i < allSortables.length; i++) { - if (i < oldIndex && i < newIndex) { - // This is before dragedSortable and newIndex so no changes needed - continue; - } - if (i > oldIndex && i > newIndex) { - // This is after dragedSortable and newIndex so no changes needed - continue; - } - if (i == oldIndex) { - // This is dragedSortable so it needs to be moved to newIndex - SortableMixin currentSortable = allSortables[i]; - if (currentSortable is Token && !ignoreFolderId) { - // When the draged Sortable is a Token we need to update the folderId so it is in the correct folder - final previousFolderId = dependingFolder?.folderId; - currentSortable = currentSortable.copyWith(folderId: () => previousFolderId); - } - modifiedSortables.add(currentSortable.copyWith(sortIndex: newIndex)); - continue; - } - modifiedSortables.add(allSortables[i] - .copyWith(sortIndex: i + (dragedItemMovedUp ? 1 : -1))); // This is between dragedSortable and newIndex so it needs to be moved up (-1) or down (+1) - continue; + if (dragedSortable is TokenFolder) { + final tokensInFolder = ref.read(tokenProvider).tokens.where((element) => element.folderId == dragedSortable.folderId).toList(); + final allMovingItems = [dragedSortable, ...tokensInFolder]; + allSortables = allSortables.moveAllBetween(moveAfter: previousSortable, movedItems: allMovingItems, moveBefore: nextSortable); + } else if (dragedSortable is Token) { + allSortables = allSortables.moveBetween(moveAfter: previousSortable, movedItem: dragedSortable, moveBefore: nextSortable); + allSortables = allSortables.map((e) { + return e is Token && e.id == dragedSortable.id ? e.copyWith(folderId: () => dependingFolder?.folderId) : e; + }).toList(); } - - globalRef?.read(tokenProvider.notifier).updateTokens(allTokens, (p0) { - final modifiedToken = modifiedSortables.whereType().firstWhereOrNull((updated) => updated.id == p0.id); - return p0.copyWith(sortIndex: modifiedToken?.sortIndex, folderId: modifiedToken != null ? () => modifiedToken.folderId : null); - }); - globalRef?.read(tokenFolderProvider.notifier).updateFolders(modifiedSortables.whereType().toList()); + final modifiedTokens = allSortables.whereType().toList(); + final modifiedFolders = allSortables.whereType().toList(); + ref.read(tokenProvider.notifier).addOrReplaceTokens(modifiedTokens); + ref.read(tokenFolderProvider.notifier).addOrReplaceFolders(modifiedFolders); } diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart b/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart index cb44c4390..48389c67f 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart @@ -47,7 +47,7 @@ class AddTokenFolderDialog extends ConsumerWidget { if (ref.read(introductionProvider).isCompleted(Introduction.addFolder) == false) { ref.read(introductionProvider.notifier).complete(Introduction.addFolder); } - ref.read(tokenFolderProvider.notifier).addFolder(textController.text); + ref.read(tokenFolderProvider.notifier).addNewFolder(textController.text); Navigator.pop(context); }), ], diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/delete_token_folder_action.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/delete_token_folder_action.dart index 4da714246..12ab76272 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/delete_token_folder_action.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/delete_token_folder_action.dart @@ -3,7 +3,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/token_folder.dart'; -import '../../../../../utils/app_customizer.dart'; +import '../../../../../utils/customization/action_theme.dart'; import '../../../../../utils/globals.dart'; import '../../../../../utils/lock_auth.dart'; import '../../../../../utils/riverpod_providers.dart'; diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart index f2047f77c..763d708df 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart @@ -3,7 +3,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/token_folder.dart'; -import '../../../../../utils/app_customizer.dart'; +import '../../../../../utils/customization/action_theme.dart'; import '../../../../../utils/lock_auth.dart'; import '../../../../../utils/riverpod_providers.dart'; @@ -18,7 +18,7 @@ class LockTokenFolderAction extends StatelessWidget { foregroundColor: Theme.of(context).extension()!.foregroundColor, onPressed: (context) async { if (await lockAuth(localizedReason: AppLocalizations.of(context)!.unlock) == false) return; - globalRef?.read(tokenFolderProvider.notifier).updateFolder(folder.copyWith(isLocked: !folder.isLocked)); + globalRef?.read(tokenFolderProvider.notifier).toggleFolderLock(folder); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart index 14cbed59f..1646c70d1 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart @@ -3,7 +3,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/token_folder.dart'; -import '../../../../../utils/app_customizer.dart'; +import '../../../../../utils/customization/action_theme.dart'; import '../../../../../utils/globals.dart'; import '../../../../../utils/lock_auth.dart'; import '../../../../../utils/logger.dart'; @@ -75,18 +75,24 @@ class RenameTokenFolderAction extends StatelessWidget { overflow: TextOverflow.fade, softWrap: false, ), - onPressed: () { + onPressed: () async { final newLabel = nameInputController.text.trim(); if (newLabel.isEmpty) return; - globalRef?.read(tokenFolderProvider.notifier).updateFolder(folder.copyWith(label: newLabel)); - - Logger.info( - 'Renamed token:', - name: 'token_widget_base.dart#TextButton#renameClicked', - error: '\'${folder.label}\' changed to \'$newLabel\'', - ); - - Navigator.of(context).pop(); + final success = await globalRef?.read(tokenFolderProvider.notifier).updateLabel(folder, newLabel); + if (success == true) { + Logger.info( + 'Renamed token:', + name: 'token_widget_base.dart#TextButton#renameClicked', + error: '\'${folder.label}\' changed to \'$newLabel\'', + ); + } else { + Logger.warning( + 'Failed to rename token', + name: 'token_widget_base.dart#TextButton#renameClicked', + error: '\'${folder.label}\' to \'$newLabel\'', + ); + } + if (context.mounted) Navigator.of(context).pop(); }, ), ], diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart index 01fc9adc3..4bfabfab5 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; @@ -11,7 +12,7 @@ import '../../../../model/states/token_filter.dart'; import '../../../../model/token_folder.dart'; import '../../../../model/tokens/push_token.dart'; import '../../../../model/tokens/token.dart'; -import '../../../../utils/app_customizer.dart'; +import '../../../../utils/customization/action_theme.dart'; import '../../../../utils/lock_auth.dart'; import '../../../../utils/riverpod_providers.dart'; import '../../../../widgets/custom_trailing.dart'; @@ -55,7 +56,7 @@ class _TokenFolderExpandableState extends ConsumerState w if (widget.expandOverride != null) return; if (widget.folder.isExpanded != expandableController.expanded) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - globalRef?.read(tokenFolderProvider.notifier).updateFolder(widget.folder.copyWith(isExpanded: expandableController.expanded)); + globalRef?.read(tokenFolderProvider.notifier).updateFolder(widget.folder, (p0) => p0.copyWith(isExpanded: expandableController.expanded)); }); } }); @@ -71,10 +72,9 @@ class _TokenFolderExpandableState extends ConsumerState w @override ExpandablePanel build(BuildContext context) { - List tokens = ref.watch(tokenProvider).tokensInFolder(widget.folder, exclude: ref.watch(settingsProvider).hidePushTokens ? [PushToken] : []); - tokens = widget.filter?.filterTokens(tokens) ?? tokens; + final tokens = ref.watch(tokenProvider).tokensInFolder(widget.folder, exclude: ref.watch(settingsProvider).hidePushTokens ? [PushToken] : []); + tokens.sort((a, b) => a.compareTo(b)); final draggingSortable = ref.watch(draggingSortableProvider); - if (widget.expandOverride == null) { if (tokens.isEmpty && expandableController.expanded) { expandableController.value = false; @@ -115,9 +115,9 @@ class _TokenFolderExpandableState extends ConsumerState w ), child: Padding( padding: const EdgeInsets.only(left: 15, right: 0), - child: DragTarget( - onWillAccept: (data) { - if (data is Token && data.folderId != widget.folder.folderId) { + child: DragTarget( + onWillAcceptWithDetails: (details) { + if (details.data.folderId != widget.folder.folderId) { if (widget.folder.isLocked) return true; _expandTimer?.cancel(); _expandTimer = Timer(const Duration(milliseconds: 500), () { @@ -129,11 +129,11 @@ class _TokenFolderExpandableState extends ConsumerState w return false; }, onLeave: (data) => _expandTimer?.cancel(), - onAccept: (data) { - if (data is! Token) return; + onAcceptWithDetails: (details) { + log('Moving token to folder ${widget.folder.label}', name: 'TokenFolderExpandable'); ref.read(tokenProvider.notifier).updateToken( - data, - (p0) => p0.copyWith(folderId: () => widget.folder.folderId), + details.data, + (p0) => p0.copyWith(folderId: () => widget.folder.folderId, sortIndex: (widget.folder.sortIndex!) + 1), ); }, builder: (context, willAccept, willReject) => Center( @@ -247,11 +247,19 @@ class _TokenFolderExpandableState extends ConsumerState w children: [ for (var i = 0; i < tokens.length; i++) ...[ if (draggingSortable != tokens[i] && (i != 0 || draggingSortable is Token)) - widget.filter == null ? DragTargetDivider(dependingFolder: widget.folder, nextSortable: tokens[i]) : const Divider(), + widget.filter == null + ? DragTargetDivider( + dependingFolder: widget.folder, + previousSortable: (i - 1) < 0 ? null : tokens[i - 1], + nextSortable: tokens[i], + ) + : const Divider(), TokenWidgetBuilder.fromToken(tokens[i]), ], if (tokens.isNotEmpty && draggingSortable is Token) - widget.filter == null ? DragTargetDivider(dependingFolder: widget.folder, nextSortable: null) : const Divider(), + widget.filter == null + ? DragTargetDivider(dependingFolder: widget.folder, previousSortable: tokens.last, nextSortable: null) + : const Divider(), if (tokens.isNotEmpty && draggingSortable is! Token) const SizedBox(height: 8), ], ), diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart index 4514fc15e..f86dafdf2 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart @@ -16,6 +16,7 @@ class TokenFolderWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final draggingSortable = ref.watch(draggingSortableProvider); + final draggingSortableNotifier = ref.read(draggingSortableProvider.notifier); final TokenFolder? draggingFolder = draggingSortable is TokenFolder ? draggingSortable : null; return draggingSortable == null ? LongPressDraggable( @@ -24,15 +25,13 @@ class TokenFolderWidget extends ConsumerWidget { final textSize = textSizeOf(folder.label, Theme.of(context).textTheme.titleLarge!); return Offset(max(textSize.width / 2, 30), textSize.height / 2 + 30); }, - onDragStarted: () { - ref.read(draggingSortableProvider.notifier).state = folder; - }, - onDragCompleted: () { - globalRef?.read(draggingSortableProvider.notifier).state = null; - }, - onDraggableCanceled: (velocity, offset) { - globalRef?.read(draggingSortableProvider.notifier).state = null; + onDragStarted: () => draggingSortableNotifier.state = folder, + onDragCompleted: () async { + await Future.delayed(const Duration(milliseconds: 50)); + // FIXME: The folder may appear before reordering the list. (race condition) This results in a flickering effect. Waiting here is a workaround so the list is updated before the folder visible again. We should find a better solution. + draggingSortableNotifier.state = null; }, + onDraggableCanceled: (velocity, offset) => draggingSortableNotifier.state = null, data: folder, childWhenDragging: const SizedBox(), feedback: Column( diff --git a/lib/views/main_view/main_view_widgets/poll_loading_indicator.dart b/lib/views/main_view/main_view_widgets/loading_indicator.dart similarity index 68% rename from lib/views/main_view/main_view_widgets/poll_loading_indicator.dart rename to lib/views/main_view/main_view_widgets/loading_indicator.dart index b344f5653..e2b4652c1 100644 --- a/lib/views/main_view/main_view_widgets/poll_loading_indicator.dart +++ b/lib/views/main_view/main_view_widgets/loading_indicator.dart @@ -1,27 +1,30 @@ import 'package:flutter/material.dart'; import '../../../utils/logger.dart'; -import '../../../utils/push_provider.dart'; /// This widget is polling for challenges and closes itself when the polling is done. -class PollLoadingIndicator extends StatelessWidget { +class LoadingIndicator extends StatelessWidget { static double widgetSize = 40; static OverlayEntry? _overlayEntry; - static void pollForChallenges(BuildContext context) { - if (_overlayEntry != null) return; + + static Future show(BuildContext context, Future Function() future) async { + if (_overlayEntry != null) return null; _overlayEntry = OverlayEntry( - builder: (context) => const PollLoadingIndicator._(), + builder: (context) => const LoadingIndicator._(), ); Overlay.of(context).insert(_overlayEntry!); - Logger.info('Start polling for challenges', name: 'poll_loading_indicator.dart#initState'); - PushProvider().pollForChallenges(isManually: true).then((_) { - Logger.info('Stop polling for challenges', name: 'poll_loading_indicator.dart#initState'); + Logger.info('Showing loading indicator', name: 'loading_indicator.dart#show'); + + final T result = await future().then((value) { + Logger.info('Hiding loading indicator', name: 'loading_indicator.dart#show'); _overlayEntry?.remove(); _overlayEntry = null; + return value; }); + return result; } - const PollLoadingIndicator._(); + const LoadingIndicator._(); @override Widget build(BuildContext context) { diff --git a/lib/views/main_view/main_view_widgets/main_view_navigation_bar.dart b/lib/views/main_view/main_view_widgets/main_view_navigation_bar.dart index 176aa1fe3..74bfdd2c3 100644 --- a/lib/views/main_view/main_view_widgets/main_view_navigation_bar.dart +++ b/lib/views/main_view/main_view_widgets/main_view_navigation_bar.dart @@ -68,14 +68,15 @@ class MainViewNavigationBar extends ConsumerWidget { child: Padding( padding: EdgeInsets.only(top: navHeight * 0.1, bottom: navHeight * 0.2), child: AppBarItem( + tooltip: AppLocalizations.of(context)!.addTokenManually, onPressed: () { Navigator.pushNamed(context, AddTokenManuallyView.routeName); }, icon: FocusedItemAsOverlay( onComplete: () { - ref.read(introductionProvider.notifier).complete(Introduction.addTokenManually); + ref.read(introductionProvider.notifier).complete(Introduction.addManually); }, - isFocused: ref.watch(introductionProvider).isConditionFulfilled(ref, Introduction.addTokenManually), + isFocused: ref.watch(introductionProvider).isConditionFulfilled(ref, Introduction.addManually), tooltipWhenFocused: AppLocalizations.of(context)!.introAddTokenManually, child: FittedBox( child: Icon( @@ -98,6 +99,7 @@ class MainViewNavigationBar extends ConsumerWidget { tooltipWhenFocused: AppLocalizations.of(context)!.introAddFolder, onComplete: () => ref.read(introductionProvider.notifier).complete(Introduction.addFolder), child: AppBarItem( + tooltip: AppLocalizations.of(context)!.addFolder, onPressed: () { showDialog( context: context, @@ -121,6 +123,7 @@ class MainViewNavigationBar extends ConsumerWidget { child: Padding( padding: EdgeInsets.only(top: navHeight * 0.2, bottom: navHeight * 0.1), child: AppBarItem( + tooltip: AppLocalizations.of(context)!.settings, onPressed: () { Navigator.pushNamed(context, SettingsView.routeName); }, diff --git a/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/license_push_view_button.dart b/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/license_push_view_button.dart index c909bc206..4760ff6e9 100644 --- a/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/license_push_view_button.dart +++ b/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/license_push_view_button.dart @@ -21,11 +21,13 @@ class LicensePushViewButton extends ConsumerWidget { tooltipWhenFocused: AppLocalizations.of(context)!.introHidePushTokens, onComplete: () => ref.read(introductionProvider.notifier).complete(Introduction.hidePushTokens), child: AppBarItem( + tooltip: AppLocalizations.of(context)!.pushTokens, onPressed: () => Navigator.pushNamed(context, PushTokensView.routeName), icon: const Icon(Icons.notifications), ), ) : AppBarItem( + tooltip: AppLocalizations.of(context)!.licenses, onPressed: () => Navigator.of(context).pushNamed(LicenseView.routeName), icon: const Icon(Icons.info_outline), ); diff --git a/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/qr_scanner_button.dart b/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/qr_scanner_button.dart index da70a9dc4..43eae41e7 100644 --- a/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/qr_scanner_button.dart +++ b/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/qr_scanner_button.dart @@ -31,7 +31,7 @@ class QrScannerButton extends ConsumerWidget { if (qrCode != null) ref.read(tokenProvider.notifier).handleQrCode(qrCode); }); }, - tooltip: AppLocalizations.of(context)?.scanQrCode ?? '', + tooltip: AppLocalizations.of(context)!.scanQrCode, child: const Icon(Icons.qr_code_scanner_outlined), ); } diff --git a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart index 1d975b0cd..7327fb948 100644 --- a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart +++ b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart @@ -5,13 +5,15 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../model/mixins/sortable_mixin.dart'; import '../../../model/token_folder.dart'; import '../../../model/tokens/push_token.dart'; +import '../../../model/tokens/token.dart'; +import '../../../utils/push_provider.dart'; import '../../../utils/riverpod_providers.dart'; import '../../../widgets/deactivateable_refresh_indicator.dart'; import '../../../widgets/drag_item_scroller.dart'; import '../../../widgets/introduction_widgets/token_introduction.dart'; import 'drag_target_divider.dart'; +import 'loading_indicator.dart'; import 'no_token_screen.dart'; -import 'poll_loading_indicator.dart'; import 'sortable_widget_builder.dart'; class MainViewTokensList extends ConsumerStatefulWidget { @@ -21,6 +23,28 @@ class MainViewTokensList extends ConsumerStatefulWidget { @override ConsumerState createState() => _MainViewTokensListState(); + + static List buildSortableWidgets(List sortables, SortableMixin? draggingSortable) { + List widgets = []; + if (sortables.isEmpty) return []; + sortables.sort((a, b) => a.compareTo(b)); + for (var i = 0; i < sortables.length; i++) { + final isFirst = i == 0; + final isDraggingTheCurrent = draggingSortable == sortables[i]; + final previousWasExpandedFolder = i > 0 && sortables[i - 1] is TokenFolder && (sortables[i - 1] as TokenFolder).isExpanded; + // 1. Add a divider if the current sortable is not the one which is dragged + // 2. Dont add a divider if the current sortable is the first + // 3. Dont add a divider if the previous sortable was an expanded folder + // 4. Ignore 2. and 3. if there is a sortable that is dragged + // 1 2 3 4 + if (!isDraggingTheCurrent && ((!isFirst && !previousWasExpandedFolder) || draggingSortable != null)) { + widgets.add(DragTargetDivider(dependingFolder: null, previousSortable: sortables.last, nextSortable: sortables[i])); + } + widgets.add(SortableWidgetBuilder.fromSortable(sortables[i])); + } + + return widgets; + } } class _MainViewTokensListState extends ConsumerState { @@ -31,22 +55,33 @@ class _MainViewTokensListState extends ConsumerState { @override Widget build(BuildContext context) { - final tokenFolders = ref.watch(tokenFolderProvider).folders; - final tokenState = ref.watch(tokenProvider); - final allowToRefresh = tokenState.hasPushTokens; final draggingSortable = ref.watch(draggingSortableProvider); - bool filterPushTokens = ref.watch(settingsProvider).hidePushTokens && tokenState.hasOTPTokens; + final allSortables = ref.watch(sortableProvider); + final allowToRefresh = allSortables.any((element) => element is PushToken); + bool filterPushTokens = ref.watch(settingsProvider).hidePushTokens && allowToRefresh; - final tokenStateWithNoFolder = tokenState.tokensWithoutFolder(exclude: filterPushTokens ? [PushToken] : []); + final sortables = []; - List sortables = [...tokenFolders, ...tokenStateWithNoFolder]; + for (var element in allSortables) { + if (element is! Token) { + sortables.add(element); + continue; + } + if (filterPushTokens == false && element.folderId == null) { + sortables.add(element); + continue; + } + } + // final List tokens = allSortables.whereType().toList(); + // final tokensWithNoFolder = tokens.withoutFolder(exclude: filterPushTokens ? [PushToken] : []); + // List sortables = [...tokenFolders, ...tokensWithNoFolder]; return Stack( children: [ if (sortables.isEmpty) const NoTokenScreen(), DeactivateableRefreshIndicator( allowToRefresh: allowToRefresh, - onRefresh: () async => PollLoadingIndicator.pollForChallenges(context), + onRefresh: () async => LoadingIndicator.show(context, () async => PushProvider.instance?.pollForChallenges(isManually: true)), child: SlidableAutoCloseBehavior( child: DragItemScroller( listViewKey: listViewKey, @@ -64,17 +99,24 @@ class _MainViewTokensListState extends ConsumerState { TokenIntroduction( child: Column( children: [ - ..._buildSortableWidgets(sortables, draggingSortable), + ...MainViewTokensList.buildSortableWidgets(sortables, draggingSortable), ], ), ), ...(draggingSortable != null) ? [ - const DragTargetDivider(dependingFolder: null, nextSortable: null, isLastDivider: true, bottomPaddingIfLast: 80), - const Expanded( + DragTargetDivider( + dependingFolder: null, previousSortable: sortables.last, nextSortable: null, isLastDivider: true, bottomPaddingIfLast: 80), + Expanded( child: Opacity( opacity: 0, - child: DragTargetDivider(dependingFolder: null, nextSortable: null, isLastDivider: true, bottomPaddingIfLast: 80)), + child: DragTargetDivider( + dependingFolder: null, + previousSortable: sortables.last, + nextSortable: null, + isLastDivider: true, + bottomPaddingIfLast: 0, + )), ) ] : [const SizedBox(height: 80)] @@ -89,26 +131,4 @@ class _MainViewTokensListState extends ConsumerState { ], ); } - - List _buildSortableWidgets(List sortables, SortableMixin? draggingSortable) { - List widgets = []; - if (sortables.isEmpty) return []; - sortables.sort((a, b) => a.compareTo(b)); - for (var i = 0; i < sortables.length; i++) { - final isFirst = i == 0; - final isDraggingTheCurrent = draggingSortable == sortables[i]; - final previousWasExpandedFolder = i > 0 && sortables[i - 1] is TokenFolder && (sortables[i - 1] as TokenFolder).isExpanded; - // 1. Add a divider if the current sortable is not the one which is dragged - // 2. Dont add a divider if the current sortable is the first - // 3. Dont add a divider if the previous sortable was an expanded folder - // 4. Ignore 2. and 3. if there is a sortable that is dragged - // 1 2 3 4 - if (!isDraggingTheCurrent && ((!isFirst && !previousWasExpandedFolder) || draggingSortable != null)) { - widgets.add(DragTargetDivider(dependingFolder: null, nextSortable: sortables[i])); - } - widgets.add(SortableWidgetBuilder.fromSortable(sortables[i])); - } - - return widgets; - } } diff --git a/lib/views/main_view/main_view_widgets/no_token_screen.dart b/lib/views/main_view/main_view_widgets/no_token_screen.dart index 56b54eab9..ba0635593 100644 --- a/lib/views/main_view/main_view_widgets/no_token_screen.dart +++ b/lib/views/main_view/main_view_widgets/no_token_screen.dart @@ -26,7 +26,10 @@ class NoTokenScreen extends StatelessWidget { overflow: TextOverflow.fade, softWrap: false, ), - const Icon(Icons.qr_code_scanner_outlined), + Tooltip( + message: AppLocalizations.of(context)!.scanQrCode, + child: const Icon(Icons.qr_code_scanner_outlined), + ), Text( AppLocalizations.of(context)!.noResultText2, style: Theme.of(context).textTheme.titleMedium, diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart index aa1ca534d..1daf7cff2 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart @@ -6,11 +6,10 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../../l10n/app_localizations.dart'; import '../../../../../../model/enums/introduction.dart'; import '../../../../../../model/tokens/day_password_token.dart'; -import '../../../../../../utils/app_customizer.dart'; +import '../../../../../../utils/customization/action_theme.dart'; import '../../../../../../utils/globals.dart'; import '../../../../../../utils/lock_auth.dart'; import '../../../../../../utils/riverpod_providers.dart'; -import '../../../../../../utils/utils.dart'; import '../../../../../../widgets/dialog_widgets/default_dialog.dart'; import '../../../../../../widgets/focused_item_as_overlay.dart'; import '../../token_action.dart'; @@ -124,7 +123,7 @@ class EditDayPassowrdTokenAction extends TokenAction { }, ), TextFormField( - initialValue: enumAsString(algorithm), + initialValue: algorithm.name, decoration: InputDecoration(labelText: AppLocalizations.of(context)!.algorithm), enabled: false, ), @@ -133,6 +132,17 @@ class EditDayPassowrdTokenAction extends TokenAction { decoration: InputDecoration(labelText: AppLocalizations.of(context)!.period), enabled: false, ), + if (token.origin != null) + TextFormField( + initialValue: token.origin!.appName, + decoration: const InputDecoration(labelText: 'Origin'), + enabled: false, + ), + TextFormField( + initialValue: token.isPrivacyIdeaToken == false ? 'Yes' : 'No', + decoration: const InputDecoration(labelText: 'Is exportable?'), + enabled: false, + ), ], ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart index c201ed460..4bc649f67 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart @@ -2,22 +2,23 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import 'package:privacyidea_authenticator/widgets/custom_trailing.dart'; import '../../../../../l10n/app_localizations.dart'; -import '../../../../../model/enums/day_passoword_token_view_mode.dart'; +import '../../../../../model/enums/day_password_token_view_mode.dart'; import '../../../../../model/tokens/day_password_token.dart'; import '../../../../../utils/riverpod_providers.dart'; import '../../../../../utils/utils.dart'; import '../../../../../widgets/custom_texts.dart'; +import '../../../../../widgets/custom_trailing.dart'; +import '../../../../../widgets/hideable_widget_.dart'; import '../token_widget_tile.dart'; class DayPasswordTokenWidgetTile extends ConsumerStatefulWidget { final DayPasswordToken token; - const DayPasswordTokenWidgetTile(this.token, {super.key}); + final bool isPreview; + const DayPasswordTokenWidgetTile(this.token, {this.isPreview = false, super.key}); @override ConsumerState createState() => _DayPasswordTokenWidgetTileState(); @@ -78,70 +79,95 @@ class _DayPasswordTokenWidgetTileState extends ConsumerState await ref.read(tokenProvider.notifier).showToken(widget.token) : _copyOtpValue, - child: HideableText( - text: insertCharAt(widget.token.otpValue, ' ', widget.token.digits ~/ 2), - textScaleFactor: 1.9, - enabled: widget.token.isLocked, - isHidden: widget.token.isHidden), + child: Tooltip( + message: widget.token.isHidden ? AppLocalizations.of(context)!.authenticateToShowOtp : AppLocalizations.of(context)!.copyOTPToClipboard, + child: InkWell( + onTap: widget.isPreview + ? null + : widget.token.isLocked && widget.token.isHidden + ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) + : _copyOtpValue, + child: HideableText( + text: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), + textScaleFactor: 1.9, + enabled: widget.token.isLocked, + isHidden: widget.token.isHidden), + ), ), ), - subtitles: [ - if (widget.token.label.isNotEmpty) widget.token.label, - if (widget.token.issuer.isNotEmpty) widget.token.issuer, - ], - trailing: CustomTrailing( - padding: const EdgeInsets.all(0), - fit: BoxFit.none, - child: GestureDetector( - behavior: HitTestBehavior.deferToChild, - onTap: () { - if (widget.token.viewMode == DayPasswordTokenViewMode.VALIDFOR) { - globalRef?.read(tokenProvider.notifier).updateToken(widget.token, (p0) => p0.copyWith(viewMode: DayPasswordTokenViewMode.VALIDUNTIL)); - return; - } - if (widget.token.viewMode == DayPasswordTokenViewMode.VALIDUNTIL) { - globalRef?.read(tokenProvider.notifier).updateToken(widget.token, (p0) => p0.copyWith(viewMode: DayPasswordTokenViewMode.VALIDFOR)); - return; - } - }, - child: SizedBox( - height: Theme.of(context).textTheme.bodyLarge!.fontSize! * (Theme.of(context).textTheme.bodyLarge?.height ?? 1.2) * 3.1, - child: Column( - children: [ - Expanded( - child: Text( - switch (widget.token.viewMode) { - DayPasswordTokenViewMode.VALIDFOR => '${AppLocalizations.of(context)!.validFor}:', - DayPasswordTokenViewMode.VALIDUNTIL => '${AppLocalizations.of(context)!.validUntil}:', + subtitles: widget.isPreview + ? [ + (widget.token.label.isNotEmpty && widget.token.issuer.isNotEmpty) + ? '${widget.token.issuer}: ${widget.token.label}' + : widget.token.issuer + widget.token.label, + 'Algorithm: ${widget.token.algorithm.name}', + 'Period: ${widget.token.period.toString().split('.').first}', + ] + : [ + if (widget.token.label.isNotEmpty) widget.token.label, + if (widget.token.issuer.isNotEmpty) widget.token.issuer, + ], + trailing: SizedBox( + height: double.maxFinite, + child: CustomTrailing( + padding: const EdgeInsets.all(0), + fit: BoxFit.contain, + child: HideableWidget( + isHidden: widget.token.isHidden && !widget.isPreview, + token: widget.token, + child: GestureDetector( + behavior: HitTestBehavior.deferToChild, + onTap: widget.isPreview + ? null + : () { + if (widget.token.viewMode == DayPasswordTokenViewMode.VALIDFOR) { + globalRef?.read(tokenProvider.notifier).updateToken(widget.token, (p0) => p0.copyWith(viewMode: DayPasswordTokenViewMode.VALIDUNTIL)); + return; + } + if (widget.token.viewMode == DayPasswordTokenViewMode.VALIDUNTIL) { + globalRef?.read(tokenProvider.notifier).updateToken(widget.token, (p0) => p0.copyWith(viewMode: DayPasswordTokenViewMode.VALIDFOR)); + return; + } }, - style: Theme.of(context).listTileTheme.subtitleTextStyle, - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: false, - ), - ), - Expanded( - flex: 2, - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - switch (widget.token.viewMode) { - DayPasswordTokenViewMode.VALIDFOR => durationString, - DayPasswordTokenViewMode.VALIDUNTIL => '$yMdString\n$ejmString', - }, - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 2, + child: SizedBox( + height: Theme.of(context).textTheme.bodyLarge!.fontSize! * (Theme.of(context).textTheme.bodyLarge?.height ?? 1.2) * 3.1, + child: Column( + children: [ + Expanded( + child: Text( + switch (widget.token.viewMode) { + DayPasswordTokenViewMode.VALIDFOR => '${AppLocalizations.of(context)!.validFor}:', + DayPasswordTokenViewMode.VALIDUNTIL => '${AppLocalizations.of(context)!.validUntil}:', + }, + style: Theme.of(context).listTileTheme.subtitleTextStyle, + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + softWrap: false, + ), + ), + Expanded( + flex: 2, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + switch (widget.token.viewMode) { + DayPasswordTokenViewMode.VALIDFOR => durationString, + DayPasswordTokenViewMode.VALIDUNTIL => '$yMdString\n$ejmString', + }, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 2, + ), + ), ), - ), + ], ), - ], + ), ), ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart index ef5180c7b..730d20408 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart @@ -3,11 +3,12 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/tokens/token.dart'; -import '../../../../../utils/app_customizer.dart'; +import '../../../../../utils/customization/action_theme.dart'; import '../../../../../utils/globals.dart'; import '../../../../../utils/lock_auth.dart'; import '../../../../../utils/riverpod_providers.dart'; import '../../../../../widgets/dialog_widgets/default_dialog.dart'; +import '../../loading_indicator.dart'; import '../token_action.dart'; class DefaultDeleteAction extends TokenAction { @@ -21,7 +22,7 @@ class DefaultDeleteAction extends TokenAction { backgroundColor: Theme.of(context).extension()!.deleteColor, foregroundColor: Theme.of(context).extension()!.foregroundColor, onPressed: (_) async { - if (token.isLocked && await lockAuth(localizedReason: AppLocalizations.of(context)?.deleteLockedToken ?? '') == false) { + if (token.isLocked && await lockAuth(localizedReason: AppLocalizations.of(context)!.deleteLockedToken) == false) { return; } _showDialog(); @@ -74,7 +75,7 @@ class DefaultDeleteAction extends TokenAction { ), TextButton( onPressed: () { - globalRef?.read(tokenProvider.notifier).removeToken(token); + LoadingIndicator.show(context, () async => globalRef?.read(tokenProvider.notifier).removeToken(token)); Navigator.of(context).pop(); }, child: Text( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart index 184529d9b..eceac546e 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart @@ -5,7 +5,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/enums/introduction.dart'; import '../../../../../model/tokens/token.dart'; -import '../../../../../utils/app_customizer.dart'; +import '../../../../../utils/customization/action_theme.dart'; import '../../../../../utils/globals.dart'; import '../../../../../utils/lock_auth.dart'; import '../../../../../utils/logger.dart'; diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart index d4a604e5c..3fcae56e9 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart @@ -4,7 +4,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/enums/introduction.dart'; import '../../../../../model/tokens/token.dart'; -import '../../../../../utils/app_customizer.dart'; +import '../../../../../utils/customization/action_theme.dart'; import '../../../../../utils/lock_auth.dart'; import '../../../../../utils/logger.dart'; import '../../../../../utils/riverpod_providers.dart'; diff --git a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart index 8c5e8c7a8..fc1a65bf1 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart @@ -7,11 +7,10 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../../l10n/app_localizations.dart'; import '../../../../../../model/enums/introduction.dart'; import '../../../../../../model/tokens/hotp_token.dart'; -import '../../../../../../utils/app_customizer.dart'; +import '../../../../../../utils/customization/action_theme.dart'; import '../../../../../../utils/globals.dart'; import '../../../../../../utils/lock_auth.dart'; import '../../../../../../utils/riverpod_providers.dart'; -import '../../../../../../utils/utils.dart'; import '../../../../../../widgets/dialog_widgets/default_dialog.dart'; import '../../../../../../widgets/focused_item_as_overlay.dart'; import '../../token_action.dart'; @@ -120,10 +119,21 @@ class EditHOTPTokenAction extends TokenAction { }, ), TextFormField( - initialValue: enumAsString(algorithm), + initialValue: algorithm.name, decoration: InputDecoration(labelText: AppLocalizations.of(context)!.algorithm), enabled: false, ), + if (token.origin != null) + TextFormField( + initialValue: token.origin!.appName, + decoration: const InputDecoration(labelText: 'Origin'), + enabled: false, + ), + TextFormField( + initialValue: token.isPrivacyIdeaToken == false ? 'Yes' : 'No', + decoration: const InputDecoration(labelText: 'Is exportable?'), + enabled: false, + ), ], ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart index d25eddac8..758023c3a 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../../../widgets/custom_trailing.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/tokens/hotp_token.dart'; import '../../../../../utils/riverpod_providers.dart'; import '../../../../../utils/utils.dart'; import '../../../../../widgets/custom_texts.dart'; +import '../../../../../widgets/custom_trailing.dart'; import '../../../../../widgets/hideable_widget_.dart'; import '../token_widget_tile.dart'; @@ -68,17 +68,20 @@ class _HOTPTokenWidgetTileState extends ConsumerState { isPreview: widget.isPreview, title: Align( alignment: Alignment.centerLeft, - child: InkWell( - onTap: widget.isPreview - ? null - : widget.token.isLocked && widget.token.isHidden - ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) - : _copyOtpValue, - child: HideableText( - textScaleFactor: 1.9, - isHidden: widget.token.isHidden, - text: insertCharAt(widget.token.otpValue, ' ', widget.token.digits ~/ 2), - enabled: widget.token.isLocked, + child: Tooltip( + message: widget.token.isHidden ? AppLocalizations.of(context)!.authenticateToShowOtp : AppLocalizations.of(context)!.copyOTPToClipboard, + child: InkWell( + onTap: widget.isPreview + ? null + : widget.token.isLocked && widget.token.isHidden + ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) + : _copyOtpValue, + child: HideableText( + textScaleFactor: 1.9, + isHidden: widget.token.isHidden, + text: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), + enabled: widget.token.isLocked, + ), ), ), ), @@ -87,7 +90,7 @@ class _HOTPTokenWidgetTileState extends ConsumerState { (widget.token.label.isNotEmpty && widget.token.issuer.isNotEmpty) ? '${widget.token.issuer}: ${widget.token.label}' : '${widget.token.issuer}${widget.token.label}', - 'Algorithm: ${enumAsString(widget.token.algorithm)}', + 'Algorithm: ${widget.token.algorithm.name}', 'Counter: ${widget.token.counter}', ] : [ @@ -96,22 +99,20 @@ class _HOTPTokenWidgetTileState extends ConsumerState { ], trailing: CustomTrailing( child: widget.isPreview - ? const Icon( - size: 100, - Icons.replay, + ? const FittedBox( + fit: BoxFit.contain, + child: Icon(size: 100, Icons.replay), ) : HideableWidget( token: widget.token, isHidden: widget.token.isHidden, child: IconButton( + tooltip: AppLocalizations.of(context)!.increaseCounter, padding: const EdgeInsets.all(0), onPressed: disableTrailingButton ? null : () => _updateOtpValue(), icon: const FittedBox( fit: BoxFit.contain, - child: Icon( - size: 100, - Icons.replay, - ), + child: Icon(size: 100, Icons.replay), ), ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart index a73ab86c0..ee6f49281 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart @@ -5,8 +5,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../../l10n/app_localizations.dart'; import '../../../../../../model/enums/introduction.dart'; import '../../../../../../model/tokens/push_token.dart'; -import '../../../../../../repo/secure_token_repository.dart'; -import '../../../../../../utils/app_customizer.dart'; +import '../../../../../../utils/customization/action_theme.dart'; import '../../../../../../utils/globals.dart'; import '../../../../../../utils/lock_auth.dart'; import '../../../../../../utils/riverpod_providers.dart'; @@ -160,25 +159,23 @@ class EditPushTokenAction extends TokenAction { softWrap: false, ), children: [ - FutureBuilder( - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text( - snapshot.data != null ? snapshot.data.toString() : AppLocalizations.of(context)!.noFbToken, - style: Theme.of(context).textTheme.bodyMedium, - ); - } else { - return const Text( - '', - overflow: TextOverflow.fade, - softWrap: false, - ); - } - }, - future: SecureTokenRepository.getCurrentFirebaseToken(), - ), + Text( + token.fbToken != null ? token.fbToken.toString() : AppLocalizations.of(context)!.noFbToken, + style: Theme.of(context).textTheme.bodyMedium, + ) ], ), + if (token.origin != null) + TextFormField( + initialValue: token.origin!.appName, + decoration: const InputDecoration(labelText: 'Origin'), + enabled: false, + ), + TextFormField( + initialValue: token.isPrivacyIdeaToken == false ? 'Yes' : 'No', + decoration: const InputDecoration(labelText: 'Is exportable?'), + enabled: false, + ), ], ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart index 5358fc50d..a6d8c1fb4 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../../../widgets/custom_trailing.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/enums/introduction.dart'; import '../../../../../model/tokens/push_token.dart'; import '../../../../../utils/riverpod_providers.dart'; +import '../../../../../widgets/custom_trailing.dart'; import '../../../../../widgets/focused_item_as_overlay.dart'; import '../token_widget_tile.dart'; class PushTokenWidgetTile extends ConsumerWidget { final PushToken token; - const PushTokenWidgetTile(this.token, {super.key}); + final bool isPreview; + const PushTokenWidgetTile(this.token, {this.isPreview = false, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -19,6 +20,7 @@ class PushTokenWidgetTile extends ConsumerWidget { key: Key('${token.hashCode}TokenWidgetTile'), tokenIsLocked: token.isLocked, tokenImage: token.tokenImage, + isPreview: isPreview, title: Text( token.label, textScaler: const TextScaler.linear(1.9), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart index c9f8df4eb..1d8109319 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../../../../l10n/app_localizations.dart'; -import '../../../../../model/enums/push_token_rollout_state.dart'; +import '../../../../../model/extensions/enums/push_token_rollout_state_extension.dart'; import '../../../../../model/tokens/push_token.dart'; import '../../../../../utils/globals.dart'; import '../../../../../utils/riverpod_providers.dart'; @@ -16,6 +16,7 @@ class RolloutFailedWidget extends StatelessWidget { @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; + final localizations = AppLocalizations.of(context)!; return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -24,7 +25,7 @@ class RolloutFailedWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 6.0), child: FittedBox( child: Text( - token.rolloutState.rolloutMsg(context), + token.rolloutState.rolloutMsg(localizations), style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center, ), @@ -38,7 +39,7 @@ class RolloutFailedWidget extends StatelessWidget { child: PressButton( onPressed: () => globalRef?.read(tokenProvider.notifier).rolloutPushToken(token), child: Text( - AppLocalizations.of(context)!.retryRollout, + localizations.retryRollout, style: Theme.of(context).textTheme.bodyMedium, overflow: TextOverflow.fade, softWrap: false, @@ -52,7 +53,7 @@ class RolloutFailedWidget extends StatelessWidget { style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.errorContainer)), onPressed: () => _showDialog(), child: Text( - AppLocalizations.of(context)!.delete, + localizations.delete, style: Theme.of(context).textTheme.bodyMedium, overflow: TextOverflow.fade, softWrap: false, @@ -70,17 +71,18 @@ class RolloutFailedWidget extends StatelessWidget { useRootNavigator: false, context: globalNavigatorKey.currentContext!, builder: (BuildContext context) { + final localizations = AppLocalizations.of(context)!; return DefaultDialog( scrollable: true, title: Text( - AppLocalizations.of(context)!.confirmDeletion, + localizations.confirmDeletion, ), - content: Text(AppLocalizations.of(context)!.confirmDeletionOf(token.label)), + content: Text(localizations.confirmDeletionOf(token.label)), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text( - AppLocalizations.of(context)!.cancel, + localizations.cancel, overflow: TextOverflow.fade, softWrap: false, ), @@ -91,7 +93,7 @@ class RolloutFailedWidget extends StatelessWidget { Navigator.of(context).pop(); }, child: Text( - AppLocalizations.of(context)!.delete, + localizations.delete, overflow: TextOverflow.fade, softWrap: false, ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart index 77c364540..bcc76fa5d 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../../../../../model/enums/push_token_rollout_state.dart'; +import '../../../../../l10n/app_localizations.dart'; +import '../../../../../model/extensions/enums/push_token_rollout_state_extension.dart'; import '../../../../../model/tokens/push_token.dart'; class RolloutWidget extends StatelessWidget { @@ -13,7 +14,7 @@ class RolloutWidget extends StatelessWidget { children: [ const CircularProgressIndicator(), Text( - token.rolloutState.rolloutMsg(context), + token.rolloutState.rolloutMsg(AppLocalizations.of(context)!), style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, overflow: TextOverflow.fade, diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart index ef5e706ac..0ab1ce383 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart @@ -48,15 +48,13 @@ class TokenWidgetBase extends ConsumerWidget { return draggingSortable == null ? LongPressDraggable( maxSimultaneousDrags: 1, - onDragStarted: () { - ref.read(draggingSortableProvider.notifier).state = token; - }, - onDragCompleted: () { - globalRef?.read(draggingSortableProvider.notifier).state = null; - }, - onDraggableCanceled: (velocity, offset) { + onDragStarted: () => ref.read(draggingSortableProvider.notifier).state = token, + onDragCompleted: () async { + await Future.delayed(const Duration(milliseconds: 50)); + // FIXME: The token may appear before reordering the list. (race condition) This results in a flickering effect. Waiting here is a workaround so the list is updated before the token visible again. We should find a better solution. globalRef?.read(draggingSortableProvider.notifier).state = null; }, + onDraggableCanceled: (velocity, offset) => globalRef?.read(draggingSortableProvider.notifier).state = null, dragAnchorStrategy: (Draggable d, BuildContext context, Offset point) { final textSize = textSizeOf(token.label, Theme.of(context).textTheme.titleLarge!); return Offset(max(textSize.width / 2, 30), textSize.height / 2 + 30); diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_builder.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_builder.dart index a914d9fee..2a401d859 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_builder.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_builder.dart @@ -3,29 +3,37 @@ import 'package:flutter/material.dart'; import '../../../../model/tokens/day_password_token.dart'; import '../../../../model/tokens/hotp_token.dart'; import '../../../../model/tokens/push_token.dart'; +import '../../../../model/tokens/steam_token.dart'; import '../../../../model/tokens/token.dart'; import '../../../../model/tokens/totp_token.dart'; import 'day_password_token_widgets/day_password_token_widget.dart'; +import 'day_password_token_widgets/day_password_token_widget_tile.dart'; import 'hotp_token_widgets/hotp_token_widget.dart'; import 'hotp_token_widgets/hotp_token_widget_tile.dart'; import 'push_token_widgets/push_token_widget.dart'; +import 'push_token_widgets/push_token_widget_tile.dart'; import 'token_widget.dart'; import 'totp_token_widgets/totp_token_widget.dart'; import 'totp_token_widgets/totp_token_widget_tile.dart'; abstract class TokenWidgetBuilder { - static TokenWidget fromToken(Token token, {Key? key}) => switch (token.runtimeType) { - const (TOTPToken) => TOTPTokenWidget(token as TOTPToken, key: key), - const (HOTPToken) => HOTPTokenWidget(token as HOTPToken, key: key), - const (PushToken) => PushTokenWidget(token as PushToken, key: key), - const (DayPasswordToken) => DayPasswordTokenWidget(token as DayPasswordToken, key: key), - _ => throw UnimplementedError('Token type (${token.runtimeType}) not supported in this Version of the App') - }; + static TokenWidget fromToken(Token token, {Key? key}) { + return switch (token.runtimeType) { + const (TOTPToken) => TOTPTokenWidget(token as TOTPToken, key: key), + const (HOTPToken) => HOTPTokenWidget(token as HOTPToken, key: key), + const (PushToken) => PushTokenWidget(token as PushToken, key: key), + const (DayPasswordToken) => DayPasswordTokenWidget(token as DayPasswordToken, key: key), + const (SteamToken) => TOTPTokenWidget(token as SteamToken, key: key), + _ => throw UnimplementedError('Token type (${token.runtimeType}) not supported in this Version of the App') + }; + } static Widget previewFromToken(Token token, {Key? key}) => switch (token.runtimeType) { const (TOTPToken) => TOTPTokenWidgetTile(token as TOTPToken, key: key, isPreview: true), const (HOTPToken) => HOTPTokenWidgetTile(token as HOTPToken, key: key, isPreview: true), - // PA tokens are not exportable, so there is no need for an import preview for Push Tokens and Day Password Tokens now. + const (PushToken) => PushTokenWidgetTile(token as PushToken, key: key, isPreview: true), + const (DayPasswordToken) => DayPasswordTokenWidgetTile(token as DayPasswordToken, key: key, isPreview: true), + const (SteamToken) => TOTPTokenWidgetTile(token as SteamToken, key: key, isPreview: true), _ => throw UnimplementedError('Preview for token type (${token.runtimeType}) not supported in this Version of the App') }; } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart index 94064cb7b..81b31d1b8 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart @@ -4,11 +4,10 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../../../../l10n/app_localizations.dart'; import '../../../../../../model/enums/introduction.dart'; import '../../../../../../model/tokens/totp_token.dart'; -import '../../../../../../utils/app_customizer.dart'; +import '../../../../../../utils/customization/action_theme.dart'; import '../../../../../../utils/globals.dart'; import '../../../../../../utils/lock_auth.dart'; import '../../../../../../utils/riverpod_providers.dart'; -import '../../../../../../utils/utils.dart'; import '../../../../../../widgets/dialog_widgets/default_dialog.dart'; import '../../../../../../widgets/focused_item_as_overlay.dart'; import '../../token_action.dart'; @@ -120,7 +119,7 @@ class EditTOTPTokenAction extends TokenAction { }, ), TextFormField( - initialValue: enumAsString(algorithm), + initialValue: algorithm.name, decoration: InputDecoration(labelText: AppLocalizations.of(context)!.algorithm), enabled: false, ), @@ -129,6 +128,17 @@ class EditTOTPTokenAction extends TokenAction { decoration: InputDecoration(labelText: AppLocalizations.of(context)!.period), enabled: false, ), + if (token.origin != null) + TextFormField( + initialValue: token.origin!.appName, + decoration: const InputDecoration(labelText: 'Origin'), + enabled: false, + ), + TextFormField( + initialValue: token.isPrivacyIdeaToken == false ? 'Yes' : 'No', + decoration: const InputDecoration(labelText: 'Is exportable?'), + enabled: false, + ), ], ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart index 41de59315..dcd1a6242 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../../../widgets/custom_trailing.dart'; +import 'package:flutterlifecyclehooks/flutterlifecyclehooks.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/tokens/totp_token.dart'; import '../../../../../utils/riverpod_providers.dart'; import '../../../../../utils/utils.dart'; import '../../../../../widgets/custom_texts.dart'; +import '../../../../../widgets/custom_trailing.dart'; import '../../../../../widgets/hideable_widget_.dart'; import '../token_widget_tile.dart'; @@ -21,7 +22,7 @@ class TOTPTokenWidgetTile extends ConsumerStatefulWidget { ConsumerState createState() => _TOTPTokenWidgetTileState(); } -class _TOTPTokenWidgetTileState extends ConsumerState with SingleTickerProviderStateMixin { +class _TOTPTokenWidgetTileState extends ConsumerState with SingleTickerProviderStateMixin, LifecycleMixin { double secondsLeft = 0; late AnimationController animation; late DateTime lastCount; @@ -54,19 +55,6 @@ class _TOTPTokenWidgetTileState extends ConsumerState with _startCountDown(); } - void _onAppStateChange(AppLifecycleState? state) { - if (!mounted) return; - if (state == AppLifecycleState.resumed) { - setState(() => secondsLeft = widget.token.secondsUntilNextOTP); - animation.forward(from: 1 - secondsLeft / widget.token.period); - return; - } - if (state == AppLifecycleState.paused) { - animation.stop(); - return; - } - } - @override dispose() { animation.dispose(); @@ -89,10 +77,23 @@ class _TOTPTokenWidgetTileState extends ConsumerState with Future.delayed(Duration(milliseconds: msUntilNextSecond), () => _startCountDown()); } + @override + void onAppPause() { + if (!mounted) return; + animation.stop(); + } + + @override + void onAppResume() { + if (!mounted) return; + setState(() { + secondsLeft = widget.token.secondsUntilNextOTP; + animation.forward(from: 1 - secondsLeft / widget.token.period); + }); + } + @override Widget build(BuildContext context) { - final appstate = ref.watch(appStateProvider); - _onAppStateChange(appstate); return TokenWidgetTile( isPreview: widget.isPreview, key: Key('${widget.token.hashCode}TokenWidgetTile'), @@ -100,18 +101,21 @@ class _TOTPTokenWidgetTileState extends ConsumerState with tokenIsLocked: widget.token.isLocked, title: Align( alignment: Alignment.centerLeft, - child: InkWell( - onTap: widget.isPreview - ? null - : widget.token.isLocked && widget.token.isHidden - ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) - : _copyOtpValue, - child: HideableText( - key: Key(widget.token.hashCode.toString()), - text: insertCharAt(widget.token.otpValue, ' ', widget.token.digits ~/ 2), - textScaleFactor: 1.9, - enabled: widget.token.isLocked, - isHidden: widget.token.isHidden, + child: Tooltip( + message: widget.token.isHidden ? AppLocalizations.of(context)!.authenticateToShowOtp : AppLocalizations.of(context)!.copyOTPToClipboard, + child: InkWell( + onTap: widget.isPreview + ? null + : widget.token.isLocked && widget.token.isHidden + ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) + : _copyOtpValue, + child: HideableText( + key: Key(widget.token.hashCode.toString()), + text: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), + textScaleFactor: 1.9, + enabled: widget.token.isLocked, + isHidden: widget.token.isHidden, + ), ), ), ), @@ -120,7 +124,7 @@ class _TOTPTokenWidgetTileState extends ConsumerState with (widget.token.label.isNotEmpty && widget.token.issuer.isNotEmpty) ? '${widget.token.issuer}: ${widget.token.label}' : widget.token.issuer + widget.token.label, - 'Algorithm: ${enumAsString(widget.token.algorithm)}', + 'Algorithm: ${widget.token.algorithm.name}', 'Period: ${widget.token.period} seconds', ] : [ @@ -130,7 +134,7 @@ class _TOTPTokenWidgetTileState extends ConsumerState with trailing: CustomTrailing( child: HideableWidget( token: widget.token, - isHidden: widget.token.isHidden, + isHidden: widget.token.isHidden && !widget.isPreview, child: Stack( alignment: Alignment.center, children: [ diff --git a/lib/views/onboarding_view/onboading_view_widgets/onboarding_page.dart b/lib/views/onboarding_view/onboading_view_widgets/onboarding_page.dart deleted file mode 100644 index d18515677..000000000 --- a/lib/views/onboarding_view/onboading_view_widgets/onboarding_page.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; - -class OnboardingPage extends StatelessWidget { - const OnboardingPage({super.key, required, required this.title, required this.subtitle, this.onPressed, this.buttonTitle}); - - final String title; - final String subtitle; - final VoidCallback? onPressed; - final String? buttonTitle; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(24.0), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - title, - style: TextStyle( - fontSize: 27.0, - fontWeight: FontWeight.bold, - color: Theme.of(context).textTheme.titleMedium?.color, - ), - ), - ), - ), - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - alignment: WrapAlignment.center, - children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - subtitle, - style: TextStyle( - fontSize: 17.0, - color: Theme.of(context).textTheme.titleMedium?.color, - ), - textAlign: TextAlign.center, - ), - ), - if (onPressed != null && buttonTitle != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: OutlinedButton( - onPressed: onPressed, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - buttonTitle!, - overflow: TextOverflow.fade, - softWrap: false, - ), - ), - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/views/onboarding_view/onboarding_view.dart b/lib/views/onboarding_view/onboarding_view.dart deleted file mode 100644 index c52cc7a77..000000000 --- a/lib/views/onboarding_view/onboarding_view.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lottie/lottie.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../../l10n/app_localizations.dart'; -import '../../model/enums/introduction.dart'; -import '../../utils/riverpod_providers.dart'; -import '../../widgets/dot_indicator.dart'; -import '../main_view/main_view.dart'; -import '../view_interface.dart'; -import 'onboading_view_widgets/onboarding_page.dart'; - -class LottieFiles { - final String lottieFile; - - LottieFiles(this.lottieFile); -} - -List lottieFiles = [ - LottieFiles( - 'res/lottie/onboarding_secure_animation.json', - ), - LottieFiles( - 'res/lottie/lock_shield.json', - ), - LottieFiles( - 'res/lottie/github-logo.json', - ), -]; - -class OnboardingView extends ConsumerStatefulView { - static const String routeName = '/onboarding'; - @override - RouteSettings get routeSettings => const RouteSettings(name: routeName); - - final String appName; - - const OnboardingView({required this.appName, super.key}); - - @override - ConsumerState createState() => _OnboardingViewState(); -} - -class _OnboardingViewState extends ConsumerState { - int _currentIndex = 0; - final PageController _pageController = PageController(); - - @override - Widget build(BuildContext context) => Scaffold( - resizeToAvoidBottomInset: false, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - body: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Expanded(child: SizedBox()), - Expanded( - flex: 3, - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Lottie.asset( - lottieFiles[_currentIndex].lottieFile, - alignment: Alignment.topCenter, - ), - ), - ), - ), - Expanded( - flex: 2, - child: Column( - children: [ - Flexible( - child: PageView.builder( - controller: _pageController, - itemCount: lottieFiles.length, - itemBuilder: (BuildContext context, int index) { - if (_currentIndex == 0) { - return OnboardingPage( - title: AppLocalizations.of(context)!.onBoardingTitle1(widget.appName), - subtitle: AppLocalizations.of(context)!.onBoardingText1, - ); - } - if (_currentIndex == 1) { - return OnboardingPage( - title: AppLocalizations.of(context)!.onBoardingTitle2, - subtitle: AppLocalizations.of(context)!.onBoardingText2, - ); - } - if (_currentIndex == 2) { - return OnboardingPage( - title: AppLocalizations.of(context)!.onBoardingTitle3, - subtitle: AppLocalizations.of(context)!.onBoardingText3, - buttonTitle: 'Github', - onPressed: () async { - Uri uri = Uri.parse("https://github.com/privacyidea/pi-authenticator"); - if (!await launchUrl(uri)) { - throw Exception('Could not launch $uri'); - } - }, - ); - } - - return Container(); - }, - onPageChanged: (value) { - setState(() { - _currentIndex = value; - }); - }, - ), - ), - ], - ), - ), - Expanded( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int index = 0; index < lottieFiles.length; index++) DotIndicator(isSelected: index == _currentIndex), - ], - ), - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - if (_currentIndex == lottieFiles.length - 1) { - ref.read(settingsProvider.notifier).setFirstRun(false); - ref.read(introductionProvider.notifier).complete(Introduction.introductionScreen); - Navigator.of(context).pushReplacementNamed(MainView.routeName); - return; - } - _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.ease, - ); - }, - backgroundColor: Theme.of(context).colorScheme.background, - child: Icon( - Icons.arrow_forward_ios_outlined, - color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black, - ), - ), - ); -} diff --git a/lib/views/push_token_view/widgets/push_tokens_view_list.dart b/lib/views/push_token_view/widgets/push_tokens_view_list.dart index 16ad2c547..508192dd7 100644 --- a/lib/views/push_token_view/widgets/push_tokens_view_list.dart +++ b/lib/views/push_token_view/widgets/push_tokens_view_list.dart @@ -3,13 +3,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../model/mixins/sortable_mixin.dart'; -import '../../../model/token_folder.dart'; +import '../../../utils/push_provider.dart'; import '../../../utils/riverpod_providers.dart'; import '../../../widgets/deactivateable_refresh_indicator.dart'; import '../../../widgets/drag_item_scroller.dart'; -import '../../main_view/main_view_widgets/drag_target_divider.dart'; -import '../../main_view/main_view_widgets/poll_loading_indicator.dart'; -import '../../main_view/main_view_widgets/sortable_widget_builder.dart'; +import '../../main_view/main_view_widgets/loading_indicator.dart'; +import '../../main_view/main_view_widgets/main_view_tokens_list.dart'; class PushTokensViwList extends ConsumerStatefulWidget { const PushTokensViwList({super.key}); @@ -36,7 +35,7 @@ class _PushTokensViwListState extends ConsumerState { children: [ DeactivateableRefreshIndicator( allowToRefresh: allowToRefresh, - onRefresh: () async => PollLoadingIndicator.pollForChallenges(context), + onRefresh: () async => LoadingIndicator.show(context, () async => PushProvider.instance?.pollForChallenges(isManually: true)), child: SlidableAutoCloseBehavior( child: DragItemScroller( listViewKey: listViewKey, @@ -49,7 +48,7 @@ class _PushTokensViwListState extends ConsumerState { slivers: [ SliverList( delegate: SliverChildListDelegate( - [..._buildSortableWidgets(sortables, draggingSortable)], + [...MainViewTokensList.buildSortableWidgets(sortables, draggingSortable)], ), ), ], @@ -61,30 +60,3 @@ class _PushTokensViwListState extends ConsumerState { ); } } - -List _buildSortableWidgets(List sortables, SortableMixin? draggingSortable) { - List widgets = []; - if (sortables.isEmpty) return widgets; - sortables.sort((a, b) => a.compareTo(b)); - for (var i = 0; i < sortables.length; i++) { - final isFirst = i == 0; - final isDraggingTheCurrent = draggingSortable == sortables[i]; - final previousWasExpandedFolder = i > 0 && sortables[i - 1] is TokenFolder && (sortables[i - 1] as TokenFolder).isExpanded; - // 1. Add a divider if the current sortable is not the one which is dragged - // 2. Dont add a divider if the current sortable is the first - // 3. Dont add a divider if the previous sortable was an expanded folder - // 4. Ignore 2. and 3. if there is a sortable that is dragged - // 1 2 3 4 - if (!isDraggingTheCurrent && ((!isFirst && !previousWasExpandedFolder) || draggingSortable != null)) { - widgets.add( - DragTargetDivider(dependingFolder: null, nextSortable: sortables[i], ignoreFolderId: true), - ); - } - widgets.add(SortableWidgetBuilder.fromSortable(sortables[i])); - } - if (draggingSortable != null) { - widgets.add(const DragTargetDivider(dependingFolder: null, nextSortable: null, isLastDivider: true, ignoreFolderId: true)); - } - widgets.add(const SizedBox(height: 80)); - return widgets; -} diff --git a/lib/views/qr_scanner_view/qr_scanner_view_widgets/qr_scanner_widget.dart b/lib/views/qr_scanner_view/qr_scanner_view_widgets/qr_scanner_widget.dart index 51f97410c..587a79a62 100644 --- a/lib/views/qr_scanner_view/qr_scanner_view_widgets/qr_scanner_widget.dart +++ b/lib/views/qr_scanner_view/qr_scanner_view_widgets/qr_scanner_widget.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:isolate'; import 'dart:math'; + import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart new file mode 100644 index 000000000..eedc6a34a --- /dev/null +++ b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart @@ -0,0 +1,160 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../../../../../l10n/app_localizations.dart'; +import '../../../../../mains/main_netknights.dart'; +import '../../../../../model/encryption/token_encryption.dart'; +import '../../../../../model/tokens/token.dart'; +import '../../../../../utils/riverpod_providers.dart'; +import '../../../../../widgets/dialog_widgets/default_dialog.dart'; + +class ExportTokensToFileDialog extends ConsumerStatefulWidget { + final Iterable tokens; + const ExportTokensToFileDialog({super.key, required this.tokens}); + + @override + ConsumerState createState() => _ExportTokensToFileDialogState(); +} + +class _ExportTokensToFileDialogState extends ConsumerState { + final passwordTextController = TextEditingController(); + bool passwordHidden = true; + final confirmTextController = TextEditingController(); + bool confirmHidden = true; + + bool exportPressed = false; + @override + Widget build(BuildContext context) => DefaultDialog( + title: Text(AppLocalizations.of(context)!.exportTokens), + content: Column( + mainAxisSize: MainAxisSize.min, + children: (!exportPressed) + ? [ + Text(AppLocalizations.of(context)!.enterPasswordToEncrypt), + Row( + children: [ + Expanded( + child: TextField( + controller: passwordTextController, + obscureText: passwordHidden, + onChanged: (value) => setState(() {}), + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.password, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: GestureDetector( + onTapDown: (_) => setState(() => passwordHidden = false), + onTapUp: (_) => setState(() => passwordHidden = true), + onTapCancel: () => setState(() => passwordHidden = true), + child: const Icon(Icons.visibility), + ), + ) + ], + ), + Row( + children: [ + Expanded( + child: TextField( + controller: confirmTextController, + obscureText: confirmHidden, + onChanged: (value) => setState(() {}), + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.confirmPassword, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: GestureDetector( + onTapDown: (_) => setState(() => confirmHidden = false), + onTapUp: (_) => setState(() => confirmHidden = true), + onTapCancel: () => setState(() => confirmHidden = true), + child: const Icon(Icons.visibility), + ), + ) + ], + ), + ] + : [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(AppLocalizations.of(context)!.exportingTokens), + ), + const Padding( + padding: EdgeInsets.all(8.0), + child: LinearProgressIndicator(), + ), + ], + ), + actions: exportPressed == false + ? [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(AppLocalizations.of(context)!.cancel), + ), + TextButton( + onPressed: passwordTextController.text.isNotEmpty && passwordTextController.text == confirmTextController.text + ? () async { + if (passwordTextController.text.isEmpty || passwordTextController.text != confirmTextController.text) { + return; + } + setState(() => exportPressed = true); + final tokensToEncrypt = widget.tokens.map((e) => e.copyWith(folderId: () => null)); + _saveToFile(await TokenEncryption.encrypt(tokens: tokensToEncrypt, password: passwordTextController.text)); + } + : null, + child: Text(AppLocalizations.of(context)!.export)), + ] + : [], + ); + void _saveToFile(String encryptedTokens) async { + if (kIsWeb) return; + bool isExported = false; + if (Platform.isAndroid && mounted) isExported = await _saveToFileAndroid(context, encryptedTokens); + if (Platform.isIOS && mounted) isExported = await _saveToFileIOS(context, encryptedTokens); + + if (mounted && isExported) Navigator.of(context).pop(isExported); + } + + Future _saveToFileAndroid(BuildContext context, String encryptedTokens) async { + try { + final path = 'storage/emulated/0/Download/${_getFileName()}'.replaceAll(RegExp(r'\s'), '-'); + final file = File(path); + await file.writeAsString(encryptedTokens); + + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedToDownloadsFolder))); + return true; + } catch (e) { + if (context.mounted) ref.read(statusMessageProvider.notifier).state = (AppLocalizations.of(context)!.errorSavingFile, null); + setState(() => exportPressed = false); + return false; + } + } + + Future _saveToFileIOS(BuildContext context, String encryptedTokens) async { + final Directory downloadsDir = await getApplicationDocumentsDirectory(); + final file = File('${downloadsDir.path}/${_getFileName()}'); + try { + await file.writeAsString(encryptedTokens); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedToDownloadsFolder))); + return true; + } catch (e) { + if (context.mounted) ref.read(statusMessageProvider.notifier).state = (AppLocalizations.of(context)!.errorSavingFile, null); + setState(() => exportPressed = false); + return false; + } + } + + String _getFileName() { + final time = DateTime.now(); + final appName = PrivacyIDEAAuthenticator.currentCustomization!.appName; + return '${appName}_backup_${time.year}-${time.month}-${time.day}_${time.hour.toString().padLeft(2, '0')}${time.minute.toString().padLeft(2, '0')}${time.second.toString().padLeft(2, '0')}.json'; + } +} diff --git a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_export_type_dialog.dart b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_export_type_dialog.dart new file mode 100644 index 000000000..556e37fb0 --- /dev/null +++ b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_export_type_dialog.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +import '../../../../../l10n/app_localizations.dart'; +import '../../../../../widgets/dialog_widgets/default_dialog.dart'; +import '../../../settings_view_widgets/settings_list_tile_button.dart'; +import 'export_tokens_to_file_dialog.dart'; +import 'select_tokens_dialog.dart'; +import 'show_qr_code_dialog.dart'; + +class SelectExportTypeDialog extends StatelessWidget { + const SelectExportTypeDialog({super.key}); + + @override + Widget build(BuildContext context) => DefaultDialog( + title: Text(AppLocalizations.of(context)!.exportTokens), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SettingsListTileButton( + title: Text(AppLocalizations.of(context)!.toFile, style: Theme.of(context).textTheme.titleMedium), + onPressed: () async => _selectTokensDialog(context), + icon: const Icon(Icons.file_present, size: 24), + ), + SettingsListTileButton( + title: Text(AppLocalizations.of(context)!.asQrCode, style: Theme.of(context).textTheme.titleMedium), + onPressed: () async => _selectTokenDialog(context), + icon: const Icon(Icons.qr_code, size: 24)), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(null), + child: Text(AppLocalizations.of(context)!.cancel), + ), + ], + ); + + void _selectTokensDialog(BuildContext context) async { + final isExported = await showDialog( + context: context, + builder: (context) => SelectTokensDialog( + exportDialogBuilder: (tokens) => ExportTokensToFileDialog(tokens: tokens), + ), + ); + if (isExported == true && context.mounted) Navigator.of(context).pop(isExported); + } + + void _selectTokenDialog(BuildContext context) async { + final isExported = await showDialog( + context: context, + builder: (context) => SelectTokensDialog( + multiSelect: false, + exportDialogBuilder: (tokens) => ShowQrCodeDialog(token: tokens.first), + ), + ); + if (isExported == true && context.mounted) Navigator.of(context).pop(isExported); + } +} diff --git a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart new file mode 100644 index 000000000..4351536f6 --- /dev/null +++ b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../../l10n/app_localizations.dart'; +import '../../../../../model/tokens/token.dart'; +import '../../../../../utils/lock_auth.dart'; +import '../../../../../utils/riverpod_providers.dart'; +import '../../../../../widgets/dialog_widgets/default_dialog.dart'; +import '../../../../main_view/main_view_widgets/token_widgets/token_widget_builder.dart'; + +class SelectTokensDialog extends ConsumerStatefulWidget { + final bool multiSelect; + final Widget Function(Iterable tokens) exportDialogBuilder; + const SelectTokensDialog({this.multiSelect = true, required this.exportDialogBuilder, super.key}); + + @override + ConsumerState createState() => _SelectTokensDialogState(); +} + +class _SelectTokensDialogState extends ConsumerState { + Set _selectedTokens = {}; + @override + Widget build(BuildContext context) { + final tokens = ref.read(tokenProvider).nonPiTokens; + final exportEveryToken = tokens.length == _selectedTokens.length && _selectedTokens.containsAll(tokens); + return DefaultDialog( + title: Text(AppLocalizations.of(context)!.selectTokensToExport(widget.multiSelect ? 2 : 1)), + content: SizedBox( + width: ref.watch(appConstraintsProvider)!.maxWidth * 0.8, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: (tokens.isEmpty) + ? Text( + AppLocalizations.of(context)!.noTokensToExport, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.secondary), + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...[ + if (widget.multiSelect) + InkWell( + onTap: () { + setState(() { + if (exportEveryToken) { + _selectedTokens.clear(); + } else { + _selectedTokens = tokens.toSet(); + } + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + AppLocalizations.of(context)!.exportAllTokens, + textAlign: TextAlign.right, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Checkbox( + value: exportEveryToken, + onChanged: ((value) => setState(() => value == true ? _selectedTokens = tokens.toSet() : _selectedTokens.clear()))), + ), + ], + ), + ), + Flexible( + child: SingleChildScrollView( + child: Column( + children: [ + for (final token in tokens) + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: TextButton( + style: _selectedTokens.contains(token) + ? ButtonStyle( + backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.secondary.withAlpha(80)), + ) + : null, + onPressed: () async { + if (!widget.multiSelect) { + _showExportDialog([token]); + return; + } + setState(() { + if (_selectedTokens.contains(token)) { + _selectedTokens.remove(token); + } else { + if (widget.multiSelect || _selectedTokens.isEmpty) { + _selectedTokens.add(token); + } + } + }); + }, + child: TokenWidgetBuilder.previewFromToken(token), + ), + ), + ], + ), + ), + ), + ] + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(AppLocalizations.of(context)!.cancel), + ), + if (widget.multiSelect) + TextButton( + onPressed: _selectedTokens.isNotEmpty + ? () { + _showExportDialog(_selectedTokens); + } + : null, + child: Text(AppLocalizations.of(context)!.export), + ), + ], + ); + } + + void _showExportDialog(Iterable tokens) async { + if (tokens.isEmpty) return; + final tokenFolder = ref.read(tokenFolderProvider).folders.where((folder) => folder.isLocked).toList(); + final containsLocked = tokens.any((token) => token.isLocked || tokenFolder.any((folder) => folder.folderId == token.folderId)); + final authenticated = (!containsLocked || await lockAuth(localizedReason: AppLocalizations.of(context)!.exportLockedTokenReason)); + if (!authenticated || !mounted) return; + final isExported = await showDialog( + context: context, + builder: (context) => widget.exportDialogBuilder(tokens), + ); + if (isExported == true && mounted) Navigator.of(context).pop(isExported); + } +} diff --git a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/show_qr_code_dialog.dart b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/show_qr_code_dialog.dart new file mode 100644 index 000000000..746063f9f --- /dev/null +++ b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/show_qr_code_dialog.dart @@ -0,0 +1,97 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image/image.dart' as img; +import 'package:zxing2/qrcode.dart'; + +import '../../../../../l10n/app_localizations.dart'; +import '../../../../../model/encryption/token_encryption.dart'; +import '../../../../../model/tokens/token.dart'; +import '../../../../../utils/riverpod_providers.dart'; +import '../../../../../widgets/dialog_widgets/default_dialog.dart'; + +class ShowQrCodeDialog extends ConsumerWidget { + final Token token; + const ShowQrCodeDialog({required this.token, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appConstraits = ref.watch(appConstraintsProvider)!; + final qrSize = min(appConstraits.maxWidth, appConstraits.maxHeight) * 0.8; + final qrImage = Image.memory(_generateQrCodeImage(data: TokenEncryption.generateQrCodeUri(token: token).toString())); + return DefaultDialog( + title: Text(AppLocalizations.of(context)!.asQrCode), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(AppLocalizations.of(context)!.scanThisQrWithNewDevice), + ConstrainedBox( + constraints: BoxConstraints(maxWidth: qrSize, maxHeight: qrSize, minHeight: qrSize, minWidth: qrSize), + child: GestureDetector( + onTap: () => _showQrMaximized(context, qrImage), + child: Image.memory(_generateQrCodeImage(data: TokenEncryption.generateQrCodeUri(token: token).toString())), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context)!.oneMore), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(AppLocalizations.of(context)!.done), + ), + ], + ); + } + + void _showQrMaximized(BuildContext context, Image qrImage) { + showDialog( + context: context, + builder: (context) => GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Center(child: qrImage), + ), + ); + } + + static Uint8List _generateQrCodeImage({required String data}) { + final qrcode = Encoder.encode( + data, + ErrorCorrectionLevel.q, + hints: EncodeHints()..put(EncodeHintType.characterSet, CharacterSetECI.ASCII), + ); + final matrix = qrcode.matrix!; + const scale = 4; + const padding = 1; + + var image = img.Image( + width: (matrix.width + padding + padding) * scale, + height: (matrix.height + padding + padding) * scale, + numChannels: 4, + ); + img.fill(image, color: img.ColorRgba8(0xFF, 0xFF, 0xFF, 0xFF)); + + for (var x = 0; x < matrix.width; x++) { + for (var y = 0; y < matrix.height; y++) { + if (matrix.get(x, y) == 1) { + img.fillRect( + image, + x1: (x + padding) * scale, + y1: (y + padding) * scale, + x2: (x + padding) * scale + scale - 1, + y2: (y + padding) * scale + scale - 1, + color: img.ColorRgba8(0, 0, 0, 0xFF), + ); + } + } + } + return img.encodePng(image); + } +} diff --git a/lib/views/settings_view/settings_groups/settings_group_error_log.dart b/lib/views/settings_view/settings_groups/settings_group_error_log.dart new file mode 100644 index 000000000..8d7e1a9f3 --- /dev/null +++ b/lib/views/settings_view/settings_groups/settings_group_error_log.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import '../../../l10n/app_localizations.dart'; +import '../settings_view_widgets/logging_menu.dart'; +import '../settings_view_widgets/settings_groups.dart'; + +class SettingsGroupErrorLog extends StatelessWidget { + const SettingsGroupErrorLog({super.key}); + + @override + Widget build(BuildContext context) => SettingsGroup( + title: AppLocalizations.of(context)!.errorLogTitle, + children: [ + ListTile( + title: Text( + AppLocalizations.of(context)!.logMenu, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + style: ListTileStyle.list, + trailing: ElevatedButton( + child: Text( + AppLocalizations.of(context)!.open, + overflow: TextOverflow.fade, + softWrap: false, + ), + onPressed: () => showDialog( + context: context, + builder: (context) => const LoggingMenu(), + useRootNavigator: false, + ), + ), + ), + ], + ); +} diff --git a/lib/views/settings_view/settings_groups/settings_group_general.dart b/lib/views/settings_view/settings_groups/settings_group_general.dart new file mode 100644 index 000000000..bf37cf33d --- /dev/null +++ b/lib/views/settings_view/settings_groups/settings_group_general.dart @@ -0,0 +1,70 @@ +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:simple_icons/simple_icons.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../l10n/app_localizations.dart'; +import '../../../utils/globals.dart'; +import '../../feedback_view/feedback_view.dart'; +import '../../license_view/license_view.dart'; +import '../settings_view_widgets/settings_groups.dart'; +import '../settings_view_widgets/settings_list_tile_button.dart'; + +class SettingsGroupGeneral extends StatelessWidget { + const SettingsGroupGeneral({super.key}); + + @override + Widget build(BuildContext context) { + return SettingsGroup( + title: AppLocalizations.of(context)!.settingsGroupGeneral, + children: [ + SettingsListTileButton( + onPressed: () async { + if (!await launchUrl(policyStatementUri)) { + throw Exception('Could not launch $policyStatementUri'); + } + }, + title: Text( + AppLocalizations.of(context)!.privacyPolicy, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + ), + SettingsListTileButton( + onPressed: () { + Navigator.pushNamed(context, LicenseView.routeName); + }, + title: Text( + AppLocalizations.of(context)!.licensesAndVersion, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + ), + SettingsListTileButton( + onPressed: () => launchUrl(piAuthenticatorGitHubUri), + title: Text( + AppLocalizations.of(context)!.thisAppIsOpenSource, + //'This Application is a Open Source Project. Visit us on GitHub.', + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + ), + icon: const Icon(SimpleIcons.github), + ), + SettingsListTileButton( + onPressed: () { + Navigator.pushNamed(context, FeedbackView.routeName); + }, + title: Text( + 'Feedback', + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + icon: const Icon(FluentIcons.chat_32_regular), + ), + ], + ); + } +} diff --git a/lib/views/settings_view/settings_groups/settings_group_import_export_tokens.dart b/lib/views/settings_view/settings_groups/settings_group_import_export_tokens.dart new file mode 100644 index 000000000..b2ae2b802 --- /dev/null +++ b/lib/views/settings_view/settings_groups/settings_group_import_export_tokens.dart @@ -0,0 +1,63 @@ +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../l10n/app_localizations.dart'; +import '../../import_tokens_view/import_tokens_view.dart'; +import '../settings_view_widgets/settings_groups.dart'; +import '../settings_view_widgets/settings_list_tile_button.dart'; +import 'import_export_tokens_widgets/dialogs/select_export_type_dialog.dart'; + +class SettingsGroupImportExportTokens extends ConsumerStatefulWidget { + const SettingsGroupImportExportTokens({super.key}); + + @override + ConsumerState createState() => _SettingsGroupImportExportTokensState(); +} + +class _SettingsGroupImportExportTokensState extends ConsumerState { + @override + Widget build(BuildContext context) { + return SettingsGroup( + title: AppLocalizations.of(context)!.importExportTokens, + children: [ + SettingsListTileButton( + title: Text( + AppLocalizations.of(context)!.exportNonPrivacyIDEATokens, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + overflow: TextOverflow.fade, + ), + icon: const RotatedBox( + quarterTurns: 3, + child: Icon(FluentIcons.arrow_exit_20_filled), + ), + onPressed: () => _selectExportTypeDialog(), + ), + SettingsListTileButton( + onPressed: () { + Navigator.pushNamed(context, ImportTokensView.routeName); + }, + title: Text( + AppLocalizations.of(context)!.importTokens, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + icon: const RotatedBox( + quarterTurns: 1, + child: Icon(FluentIcons.arrow_enter_20_filled), + ), + ), + ], + ); + } + + void _selectExportTypeDialog() async { + final isExported = await showDialog( + context: context, + builder: (context) => const SelectExportTypeDialog(), + ); + if (isExported == true && mounted) Navigator.of(context).pop(isExported); + } +} diff --git a/lib/views/settings_view/settings_groups/settings_group_language.dart b/lib/views/settings_view/settings_groups/settings_group_language.dart new file mode 100644 index 000000000..c6d6e0149 --- /dev/null +++ b/lib/views/settings_view/settings_groups/settings_group_language.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../l10n/app_localizations.dart'; +import '../../../utils/riverpod_providers.dart'; +import '../settings_view_widgets/settings_groups.dart'; + +class SettingsGroupLanguage extends ConsumerWidget { + const SettingsGroupLanguage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => SettingsGroup( + title: AppLocalizations.of(context)!.language, + children: [ + SwitchListTile( + title: Text( + AppLocalizations.of(context)!.useDeviceLocaleTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + AppLocalizations.of(context)!.useDeviceLocaleDescription, + overflow: TextOverflow.fade, + ), + value: ref.watch(settingsProvider).useSystemLocale, + onChanged: (value) => ref.read(settingsProvider.notifier).setUseSystemLocale(value)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: DropdownButton( + disabledHint: Text( + '${ref.watch(settingsProvider).currentLocale}', + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.grey), + overflow: TextOverflow.fade, + softWrap: false, + ), + isExpanded: true, + value: ref.watch(settingsProvider).currentLocale, + items: AppLocalizations.supportedLocales.map>((Locale itemLocale) { + return DropdownMenuItem( + value: itemLocale, + child: Text( + '$itemLocale', + overflow: TextOverflow.fade, + softWrap: false, + ), + ); + }).toList(), + onChanged: ref.watch(settingsProvider).useSystemLocale ? null : (value) => ref.read(settingsProvider.notifier).setLocalePreference(value!), + ), + ), + ], + ); +} diff --git a/lib/views/settings_view/settings_groups/settings_group_push_token.dart b/lib/views/settings_view/settings_groups/settings_group_push_token.dart new file mode 100644 index 000000000..4ca61c79f --- /dev/null +++ b/lib/views/settings_view/settings_groups/settings_group_push_token.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../l10n/app_localizations.dart'; +import '../../../model/tokens/push_token.dart'; +import '../../../utils/riverpod_providers.dart'; +import '../settings_view_widgets/settings_groups.dart'; +import '../settings_view_widgets/update_firebase_token_dialog.dart'; + +class SettingsGroupPushToken extends ConsumerWidget { + final bool enablePushSettingsGroup; + final List unsupportedPushTokens; + const SettingsGroupPushToken({ + required this.enablePushSettingsGroup, + required this.unsupportedPushTokens, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) => SettingsGroup( + isActive: enablePushSettingsGroup, + title: AppLocalizations.of(context)!.pushToken, + children: [ + ListTile( + title: Text( + AppLocalizations.of(context)!.synchronizePushTokens, + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + AppLocalizations.of(context)!.synchronizesTokensWithServer, + overflow: TextOverflow.fade, + ), + trailing: ElevatedButton( + onPressed: enablePushSettingsGroup + ? () { + showDialog( + useRootNavigator: false, + context: context, + barrierDismissible: false, + builder: (context) => const UpdateFirebaseTokenDialog(), + ); + } + : null, + child: Text( + AppLocalizations.of(context)!.sync, + overflow: TextOverflow.fade, + softWrap: false, + ), + ), + ), + ListTile( + title: RichText( + text: TextSpan( + children: [ + TextSpan( + text: AppLocalizations.of(context)!.enablePolling, + style: Theme.of(context).textTheme.titleMedium, + ), + // Add clickable icon to inform user of unsupported push tokens (for polling) + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: unsupportedPushTokens.isNotEmpty && enablePushSettingsGroup + ? GestureDetector( + onTap: () => _showPollingInfo(context, unsupportedPushTokens), + child: const Icon( + Icons.info_outline, + color: Colors.red, + ), + ) + : null, + ), + ), + ], + ), + ), + subtitle: Text( + AppLocalizations.of(context)!.requestPushChallengesPeriodically, + overflow: TextOverflow.fade, + ), + trailing: Switch( + value: ref.watch(settingsProvider).enablePolling, + onChanged: enablePushSettingsGroup ? (value) => ref.read(settingsProvider.notifier).setPolling(value) : null, + ), + ), + ListTile( + title: RichText( + text: TextSpan( + children: [ + TextSpan( + text: AppLocalizations.of(context)!.hidePushTokens, + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + ), + subtitle: Text( + AppLocalizations.of(context)!.hidePushTokensDescription, + overflow: TextOverflow.fade, + ), + trailing: Switch( + value: ref.watch(settingsProvider).hidePushTokens, + onChanged: enablePushSettingsGroup && ref.watch(tokenProvider).hasOTPTokens + ? (value) => ref.read(settingsProvider.notifier).setHidePushTokens(value) + : null, + ), + ) + ], + ); + + /// Shows a dialog to the user that displays all push tokens that do not + /// support polling. + void _showPollingInfo(BuildContext context, List unsupported) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('${AppLocalizations.of(context)!.someTokensDoNotSupportPolling}:'), + content: Scrollbar( + child: ListView.separated( + shrinkWrap: true, + itemCount: unsupported.length, + itemBuilder: (context, index) => Text('${unsupported[index].label}'), + separatorBuilder: (context, index) => const Divider(), + ), + ), + actions: [ + TextButton( + child: Text( + AppLocalizations.of(context)!.dismiss, + style: Theme.of(context).textTheme.titleLarge, + ), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + }); + } +} diff --git a/lib/views/settings_view/settings_groups/settings_group_theme.dart b/lib/views/settings_view/settings_groups/settings_group_theme.dart new file mode 100644 index 000000000..aca058ea2 --- /dev/null +++ b/lib/views/settings_view/settings_groups/settings_group_theme.dart @@ -0,0 +1,60 @@ +import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; +import 'package:flutter/material.dart'; + +import '../../../l10n/app_localizations.dart'; +import '../../../utils/home_widget_utils.dart'; +import '../settings_view_widgets/settings_groups.dart'; + +class SettingsGroupTheme extends StatelessWidget { + const SettingsGroupTheme({super.key}); + + @override + Widget build(BuildContext context) => SettingsGroup( + title: AppLocalizations.of(context)!.theme, + children: [ + RadioListTile( + title: Text( + AppLocalizations.of(context)!.lightTheme, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + value: ThemeMode.light, + groupValue: EasyDynamicTheme.of(context).themeMode, + controlAffinity: ListTileControlAffinity.trailing, + onChanged: (dynamic value) { + EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: false); + HomeWidgetUtils().setCurrentThemeMode(ThemeMode.light); + }, + ), + RadioListTile( + title: Text( + AppLocalizations.of(context)!.darkTheme, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + value: ThemeMode.dark, + groupValue: EasyDynamicTheme.of(context).themeMode, + controlAffinity: ListTileControlAffinity.trailing, + onChanged: (dynamic value) { + EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: true); + HomeWidgetUtils().setCurrentThemeMode(ThemeMode.dark); + }, + ), + RadioListTile( + title: Text( + AppLocalizations.of(context)!.systemTheme, + style: Theme.of(context).textTheme.titleMedium, + ), + value: ThemeMode.system, + groupValue: EasyDynamicTheme.of(context).themeMode, + controlAffinity: ListTileControlAffinity.trailing, + onChanged: (dynamic value) { + EasyDynamicTheme.of(context).changeTheme(dynamic: true, dark: false); + HomeWidgetUtils().setCurrentThemeMode(ThemeMode.system); + }, + ), + ], + ); +} diff --git a/lib/views/settings_view/settings_view.dart b/lib/views/settings_view/settings_view.dart index 99d37527d..b2e1fd3b0 100644 --- a/lib/views/settings_view/settings_view.dart +++ b/lib/views/settings_view/settings_view.dart @@ -1,23 +1,17 @@ -import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; -import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../../l10n/app_localizations.dart'; import '../../model/tokens/push_token.dart'; -import '../../utils/globals.dart'; -import '../../utils/home_widget_utils.dart'; import '../../utils/riverpod_providers.dart'; import '../../widgets/push_request_listener.dart'; -import '../feedback_view/feedback_view.dart'; -import '../import_tokens_view/import_tokens_view.dart'; -import '../license_view/license_view.dart'; import '../view_interface.dart'; -import 'settings_view_widgets/logging_menu.dart'; -import 'settings_view_widgets/settings_groups.dart'; -import 'settings_view_widgets/settings_list_tile_button.dart'; -import 'settings_view_widgets/update_firebase_token_dialog.dart'; +import 'settings_groups/settings_group_error_log.dart'; +import 'settings_groups/settings_group_general.dart'; +import 'settings_groups/settings_group_import_export_tokens.dart'; +import 'settings_groups/settings_group_language.dart'; +import 'settings_groups/settings_group_push_token.dart'; +import 'settings_groups/settings_group_theme.dart'; class SettingsView extends ConsumerView { @override @@ -30,7 +24,7 @@ class SettingsView extends ConsumerView { Widget build(BuildContext context, WidgetRef ref) { final tokens = ref.watch(tokenProvider).tokens; final enrolledPushTokenList = tokens.whereType().where((e) => e.isRolledOut).toList(); - final unsupported = enrolledPushTokenList.where((e) => e.url == null).toList(); + final unsupportedPushTokens = enrolledPushTokenList.where((e) => e.url == null).toList(); final enablePushSettingsGroup = enrolledPushTokenList.isNotEmpty; return PushRequestListener( @@ -38,7 +32,6 @@ class SettingsView extends ConsumerView { appBar: AppBar( title: Text( AppLocalizations.of(context)!.settings, - overflow: TextOverflow.ellipsis, // maxLines: 2 only works like this. maxLines: 2, // Title can be shown on small screens too. ), @@ -48,270 +41,19 @@ class SettingsView extends ConsumerView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsGroup( - title: AppLocalizations.of(context)!.settingsGroupGeneral, - children: [ - SettingsListTileButton( - onPressed: () async { - if (!await launchUrl(policyStatementUri)) { - throw Exception('Could not launch $policyStatementUri'); - } - }, - title: Text( - AppLocalizations.of(context)!.privacyPolicy, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - ), - SettingsListTileButton( - onPressed: () { - Navigator.pushNamed(context, LicenseView.routeName); - }, - title: Text( - AppLocalizations.of(context)!.licensesAndVersion, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - ), - SettingsListTileButton( - onPressed: () { - Navigator.pushNamed(context, FeedbackView.routeName); - }, - title: Text( - 'Feedback', - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - icon: const Icon(FluentIcons.chat_32_regular), - ), - SettingsListTileButton( - onPressed: () { - Navigator.pushNamed(context, ImportTokensView.routeName); - }, - title: Text( - AppLocalizations.of(context)!.importTokens, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - icon: const RotatedBox( - quarterTurns: 1, - child: Icon(FluentIcons.arrow_enter_20_filled), - ), - ) - ], - ), + const SettingsGroupGeneral(), const Divider(), - SettingsGroup( - title: AppLocalizations.of(context)!.theme, - children: [ - RadioListTile( - title: Text( - AppLocalizations.of(context)!.lightTheme, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - value: ThemeMode.light, - groupValue: EasyDynamicTheme.of(context).themeMode, - controlAffinity: ListTileControlAffinity.trailing, - onChanged: (dynamic value) { - EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: false); - HomeWidgetUtils().setCurrentThemeMode(ThemeMode.light); - }, - ), - RadioListTile( - title: Text( - AppLocalizations.of(context)!.darkTheme, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - value: ThemeMode.dark, - groupValue: EasyDynamicTheme.of(context).themeMode, - controlAffinity: ListTileControlAffinity.trailing, - onChanged: (dynamic value) { - EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: true); - HomeWidgetUtils().setCurrentThemeMode(ThemeMode.dark); - }, - ), - RadioListTile( - title: Text( - AppLocalizations.of(context)!.systemTheme, - style: Theme.of(context).textTheme.titleMedium, - ), - value: ThemeMode.system, - groupValue: EasyDynamicTheme.of(context).themeMode, - controlAffinity: ListTileControlAffinity.trailing, - onChanged: (dynamic value) { - EasyDynamicTheme.of(context).changeTheme(dynamic: true, dark: false); - HomeWidgetUtils().setCurrentThemeMode(ThemeMode.system); - }, - ), - ], - ), + const SettingsGroupImportExportTokens(), const Divider(), - SettingsGroup( - title: AppLocalizations.of(context)!.language, - children: [ - SwitchListTile( - title: Text( - AppLocalizations.of(context)!.useDeviceLocaleTitle, - style: Theme.of(context).textTheme.titleMedium, - ), - subtitle: Text( - AppLocalizations.of(context)!.useDeviceLocaleDescription, - overflow: TextOverflow.fade, - ), - value: ref.watch(settingsProvider).useSystemLocale, - onChanged: (value) => ref.read(settingsProvider.notifier).setUseSystemLocale(value)), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: DropdownButton( - disabledHint: Text( - '${ref.watch(settingsProvider).currentLocale}', - style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.grey), - overflow: TextOverflow.fade, - softWrap: false, - ), - isExpanded: true, - value: ref.watch(settingsProvider).currentLocale, - items: AppLocalizations.supportedLocales.map>((Locale itemLocale) { - return DropdownMenuItem( - value: itemLocale, - child: Text( - '$itemLocale', - overflow: TextOverflow.fade, - softWrap: false, - ), - ); - }).toList(), - onChanged: - ref.watch(settingsProvider).useSystemLocale ? null : (value) => ref.read(settingsProvider.notifier).setLocalePreference(value!), - ), - ), - ], - ), - SettingsGroup( - isActive: enablePushSettingsGroup, - title: AppLocalizations.of(context)!.pushToken, - children: [ - ListTile( - title: Text( - AppLocalizations.of(context)!.synchronizePushTokens, - style: Theme.of(context).textTheme.titleMedium, - ), - subtitle: Text( - AppLocalizations.of(context)!.synchronizesTokensWithServer, - overflow: TextOverflow.fade, - ), - trailing: ElevatedButton( - onPressed: enablePushSettingsGroup - ? () { - showDialog( - useRootNavigator: false, - context: context, - barrierDismissible: false, - builder: (context) => const UpdateFirebaseTokenDialog(), - ); - } - : null, - child: Text( - AppLocalizations.of(context)!.sync, - overflow: TextOverflow.fade, - softWrap: false, - ), - ), - ), - ListTile( - title: RichText( - text: TextSpan( - children: [ - TextSpan( - text: AppLocalizations.of(context)!.enablePolling, - style: Theme.of(context).textTheme.titleMedium, - ), - // Add clickable icon to inform user of unsupported push tokens (for polling) - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(left: 10), - child: unsupported.isNotEmpty && enrolledPushTokenList.isNotEmpty - ? GestureDetector( - onTap: () {}, // () => _showPollingInfo(unsupported), - child: const Icon( - Icons.info_outline, - color: Colors.red, - ), - ) - : null, - ), - ), - ], - ), - ), - subtitle: Text( - AppLocalizations.of(context)!.requestPushChallengesPeriodically, - overflow: TextOverflow.fade, - ), - trailing: Switch( - value: ref.watch(settingsProvider).enablePolling, - onChanged: enablePushSettingsGroup ? (value) => ref.read(settingsProvider.notifier).setPolling(value) : null, - ), - ), - ListTile( - title: RichText( - text: TextSpan( - children: [ - TextSpan( - text: AppLocalizations.of(context)!.hidePushTokens, - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - ), - subtitle: Text( - AppLocalizations.of(context)!.hidePushTokensDescription, - overflow: TextOverflow.fade, - ), - trailing: Switch( - value: ref.watch(settingsProvider).hidePushTokens, - onChanged: enablePushSettingsGroup && ref.watch(tokenProvider).hasOTPTokens - ? (value) => ref.read(settingsProvider.notifier).setHidePushTokens(value) - : null, - ), - ) - ], - ), + const SettingsGroupTheme(), const Divider(), - SettingsGroup( - title: AppLocalizations.of(context)!.errorLogTitle, - children: [ - ListTile( - title: Text( - AppLocalizations.of(context)!.logMenu, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - style: ListTileStyle.list, - trailing: ElevatedButton( - child: Text( - AppLocalizations.of(context)!.open, - overflow: TextOverflow.fade, - softWrap: false, - ), - onPressed: () => showDialog( - context: context, - builder: (context) => const LoggingMenu(), - useRootNavigator: false, - ), - ), - ), - ], + const SettingsGroupLanguage(), + SettingsGroupPushToken( + enablePushSettingsGroup: enablePushSettingsGroup, + unsupportedPushTokens: unsupportedPushTokens, ), + const Divider(), + const SettingsGroupErrorLog(), ], ), ), diff --git a/lib/views/settings_view/settings_view_widgets/dialogs/ask_log_sended_dialog.dart b/lib/views/settings_view/settings_view_widgets/dialogs/ask_log_sended_dialog.dart new file mode 100644 index 000000000..7dafce8b7 --- /dev/null +++ b/lib/views/settings_view/settings_view_widgets/dialogs/ask_log_sended_dialog.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +import '../../../../l10n/app_localizations.dart'; +import '../../../../utils/logger.dart'; +import '../../../../widgets/dialog_widgets/default_dialog.dart'; + +/// A dialog that asks the user if they sended the log. Clear logs if he did. +class AskLogSendedDialog extends StatelessWidget { + const AskLogSendedDialog({super.key}); + + @override + Widget build(BuildContext context) { + return DefaultDialog( + title: Text( + AppLocalizations.of(context)!.confirmation, + overflow: TextOverflow.fade, + softWrap: false, + ), + content: SingleChildScrollView( + controller: ScrollController(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Text( + AppLocalizations.of(context)!.askLogSendedDescription, + ), + ), + ], + ), + ), + actions: [ + TextButton( + child: Text( + AppLocalizations.of(context)!.no, + overflow: TextOverflow.fade, + softWrap: false, + ), + onPressed: () => Navigator.pop(context), + ), + TextButton( + onPressed: () { + Navigator.pop(context, true); + Logger.clearErrorLog(); + }, + child: Text( + AppLocalizations.of(context)!.yes, + overflow: TextOverflow.fade, + softWrap: false, + ), + ) + ], + ); + } +} diff --git a/lib/views/settings_view/settings_view_widgets/send_error_dialog.dart b/lib/views/settings_view/settings_view_widgets/send_error_dialog.dart index 4c138b574..cd9ee5fb1 100644 --- a/lib/views/settings_view/settings_view_widgets/send_error_dialog.dart +++ b/lib/views/settings_view/settings_view_widgets/send_error_dialog.dart @@ -4,10 +4,19 @@ import 'package:url_launcher/url_launcher.dart'; import '../../../l10n/app_localizations.dart'; import '../../../utils/logger.dart'; import '../../../widgets/dialog_widgets/default_dialog.dart'; +import '../settings_view.dart'; +import 'dialogs/ask_log_sended_dialog.dart'; -class SendErrorDialog extends StatelessWidget { +class SendErrorDialog extends StatefulWidget { const SendErrorDialog({super.key}); + @override + State createState() => _SendErrorDialogState(); +} + +class _SendErrorDialogState extends State { + final TextEditingController _textController = TextEditingController(); + @override Widget build(BuildContext context) => DefaultDialog( title: Text( @@ -17,24 +26,34 @@ class SendErrorDialog extends StatelessWidget { ), content: SingleChildScrollView( controller: ScrollController(), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Text( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( AppLocalizations.of(context)!.sendErrorLogDescription, ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: TextButton( + TextButton( child: Text( AppLocalizations.of(context)!.showPrivacyPolicy, ), onPressed: () => launchUrl(Uri.parse('https://netknights.it/en/privacy-statement/'))), - ), - ], + const SizedBox(height: 8.0), + TextField( + controller: _textController, + decoration: InputDecoration( + border: const OutlineInputBorder(borderSide: BorderSide(width: 1.5)), + enabledBorder: const OutlineInputBorder(borderSide: BorderSide(width: 1.5)), + focusedBorder: const OutlineInputBorder(borderSide: BorderSide(width: 1.5)), + labelText: AppLocalizations.of(context)!.optionalMessage, + ), + maxLines: 5, + ), + ], + ), ), ), actions: [ @@ -44,14 +63,20 @@ class SendErrorDialog extends StatelessWidget { overflow: TextOverflow.fade, softWrap: false, ), - onPressed: () => Navigator.pop(context), + onPressed: () => _popDialogs(context), ), TextButton( - onPressed: () => Logger.sendErrorLog(), + onPressed: () { + Logger.sendErrorLog(_textController.text); + showDialog(context: context, builder: (context) => const AskLogSendedDialog()).then((value) => value == true ? _popDialogs(context) : null); + }, child: const Icon(Icons.email), ) ], ); + void _popDialogs(BuildContext context) { + Navigator.popUntil(context, (route) => SettingsView.routeName == route.settings.name); + } } class NoLogDialog extends StatelessWidget { diff --git a/lib/views/settings_view/settings_view_widgets/settings_list_tile_button.dart b/lib/views/settings_view/settings_view_widgets/settings_list_tile_button.dart index 94bd73536..1b46bd830 100644 --- a/lib/views/settings_view/settings_view_widgets/settings_list_tile_button.dart +++ b/lib/views/settings_view/settings_view_widgets/settings_list_tile_button.dart @@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; class SettingsListTileButton extends StatelessWidget { final void Function() onPressed; - final Widget? title; + final Widget title; final Widget? icon; + static const double tileHeight = 40; - const SettingsListTileButton({super.key, this.title, this.icon, required this.onPressed}); + const SettingsListTileButton({super.key, required this.title, this.icon, required this.onPressed}); @override Widget build(BuildContext context) => TextButton( @@ -15,14 +16,16 @@ class SettingsListTileButton extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: SizedBox( - height: 40, + height: tileHeight, child: Row( mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.end, children: [ - if (title != null) title!, + Expanded(child: title), if (icon != null) IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: tileHeight, minWidth: tileHeight), onPressed: onPressed, splashRadius: 26, icon: icon!, diff --git a/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart b/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart index 1cb69d36a..5419b74dd 100644 --- a/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart +++ b/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart @@ -19,23 +19,24 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; +import 'package:privacyidea_authenticator/utils/riverpod_providers.dart'; import 'package:privacyidea_authenticator/utils/view_utils.dart'; import '../../../model/tokens/push_token.dart'; import '../../../utils/globals.dart'; -import '../../../utils/push_provider.dart'; import '../../../widgets/dialog_widgets/default_dialog.dart'; -class UpdateFirebaseTokenDialog extends StatefulWidget { +class UpdateFirebaseTokenDialog extends ConsumerStatefulWidget { const UpdateFirebaseTokenDialog({super.key}); @override - State createState() => _UpdateFirebaseTokenDialogState(); + ConsumerState createState() => _UpdateFirebaseTokenDialogState(); } -class _UpdateFirebaseTokenDialogState extends State { +class _UpdateFirebaseTokenDialogState extends ConsumerState { Widget _content = const Row( mainAxisAlignment: MainAxisAlignment.center, children: [CircularProgressIndicator()], @@ -67,14 +68,13 @@ class _UpdateFirebaseTokenDialogState extends State { // TODO What to do with poll only tokens if google-services is used? - final tuple = await PushProvider.updateFirebaseToken(); + final tuple = await ref.read(tokenProvider.notifier).updateFirebaseToken(); if (tuple == null) { showMessage(message: AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorSynchronizationNoNetworkConnection); return; } - late List tokenWithFailedUpdate; - late List tokenWithOutUrl; - (tokenWithFailedUpdate, tokenWithOutUrl) = tuple; + + final (tokenWithFailedUpdate, tokenWithOutUrl) = tuple; if (tokenWithFailedUpdate.isEmpty && tokenWithOutUrl.isEmpty) { if (!mounted) return; diff --git a/lib/views/splash_screen/splash_screen.dart b/lib/views/splash_screen/splash_screen.dart index 804b7981d..c0b2545c5 100644 --- a/lib/views/splash_screen/splash_screen.dart +++ b/lib/views/splash_screen/splash_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../model/enums/app_feature.dart'; -import '../../utils/app_customizer.dart'; +import '../../utils/customization/application_customization.dart'; import '../../utils/app_info_utils.dart'; import '../../utils/home_widget_utils.dart'; import '../../utils/logger.dart'; @@ -57,7 +57,7 @@ class _SplashScreenState extends ConsumerState { [ Future.delayed(_splashScreenDuration), ref.read(settingsProvider.notifier).loadingRepo, - ref.read(tokenProvider.notifier).loadingRepo, + ref.read(tokenProvider.notifier).initState, ref.read(introductionProvider.notifier).loadingRepo, AppInfoUtils.init(), HomeWidgetUtils().homeWidgetInit(), diff --git a/lib/widgets/app_wrapper.dart b/lib/widgets/app_wrapper.dart index 91865ca00..893670bb1 100644 --- a/lib/widgets/app_wrapper.dart +++ b/lib/widgets/app_wrapper.dart @@ -1,7 +1,13 @@ +import 'dart:developer'; +import 'dart:ui'; + import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../utils/home_widget_utils.dart'; +import '../utils/logger.dart'; import '../utils/riverpod_providers.dart'; import '../utils/riverpod_state_listener.dart'; import 'app_wrappers/single_touch_recognizer.dart'; @@ -10,20 +16,84 @@ import 'app_wrappers/state_observer.dart'; class AppWrapper extends StatelessWidget { final Widget child; - const AppWrapper({super.key, required this.child}); + const AppWrapper({required this.child, super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope(child: _AppWrapper(key: key, child: child)); + } +} + +class _AppWrapper extends ConsumerStatefulWidget { + final Widget child; + const _AppWrapper({required this.child, super.key}); + + @override + ConsumerState<_AppWrapper> createState() => _AppWrapperState(); +} + +class _AppWrapperState extends ConsumerState<_AppWrapper> { + late final AppLifecycleListener _listener; + + @override + void initState() { + super.initState(); + _listener = AppLifecycleListener( + onResume: () async { + await ref.read(tokenProvider.notifier).loadStateFromRepo(); + Logger.info('Refreshed tokens on resume', name: 'tokenProvider#appStateProvider'); + + final prProvider = ref.read(pushRequestProvider.notifier); + await prProvider.loadStateFromRepo(); + await prProvider.pollForChallenges(isManually: false); + Logger.info('Polled for challenges on resume', name: 'pushRequestProvider#appStateProvider'); + + final hidden = await HomeWidgetUtils().hideAllOtps(); + if (hidden) Logger.info('Hid all HomeWidget OTPs on resume', name: 'tokenProvider#appStateProvider'); + + log('App resumed'); + }, + onInactive: () => log('App inactive'), + onHide: () async { + await ref.read(tokenProvider.notifier).saveStateOnMinimizeApp(); + Logger.info('Saved tokens on Hide', name: 'tokenProvider#appStateProvider'); + + await FlutterLocalNotificationsPlugin().cancelAll(); + Logger.info('Cancelled all notifications on Hide', name: 'tokenProvider#appStateProvider'); + + ref.read(tokenFolderProvider.notifier).collapseLockedFolders(); + Logger.info('Collapsed locked folders on Hide', name: 'tokenFolderProvider#appStateProvider'); + + log('App hidden'); + }, + onShow: () => log('App shown'), + onPause: () => log('App paused'), + onRestart: () => log('App restarted'), + onDetach: () => log('App detached'), + onExitRequested: () async { + log('App exit requested'); + return AppExitResponse.exit; + }, + ); + } + + @override + void dispose() { + _listener.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { return SingleTouchRecognizer( - child: ProviderScope( - child: StateObserver( - listeners: [ - NavigationDeepLinkListener(deeplinkProvider: deeplinkProvider), - HomeWidgetTokenStateListener(tokenProvider: tokenProvider), - ], - child: EasyDynamicThemeWidget( - child: child, - ), + child: StateObserver( + listeners: [ + NavigationDeepLinkListener(deeplinkProvider: deeplinkProvider), + HomeWidgetTokenStateListener(tokenProvider: tokenProvider), + // SortableListener(tokenProvider, tokenFolderProvider), + ], + child: EasyDynamicThemeWidget( + child: widget.child, ), ), ); diff --git a/lib/widgets/dialog_widgets/default_dialog.dart b/lib/widgets/dialog_widgets/default_dialog.dart index b339a3bf7..61fb76a2b 100644 --- a/lib/widgets/dialog_widgets/default_dialog.dart +++ b/lib/widgets/dialog_widgets/default_dialog.dart @@ -39,7 +39,7 @@ class DefaultDialog extends StatelessWidget { buttonPadding: const EdgeInsets.fromLTRB(8, 0, 8, 8), insetPadding: const EdgeInsets.fromLTRB(16, 32, 16, 12), titlePadding: const EdgeInsets.all(12), - contentPadding: const EdgeInsets.all(16), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), elevation: 2, title: Row( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/widgets/dialog_widgets/patch_notes_dialog.dart b/lib/widgets/dialog_widgets/patch_notes_dialog.dart index 3094c5742..288bbc726 100644 --- a/lib/widgets/dialog_widgets/patch_notes_dialog.dart +++ b/lib/widgets/dialog_widgets/patch_notes_dialog.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import '../../l10n/app_localizations.dart'; import '../../model/enums/patch_note_type.dart'; +import '../../model/extensions/enums/patch_note_type_extension.dart'; +import '../../model/version.dart'; import '../../utils/app_info_utils.dart'; import '../../utils/riverpod_providers.dart'; -import '../../utils/version.dart'; import 'default_dialog.dart'; class PatchNotesDialog extends StatelessWidget { @@ -47,7 +48,7 @@ class PatchNotesDialog extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - entry.key.getName(localizations), + entry.key.localizedName(localizations), style: Theme.of(context).textTheme.titleSmall?.copyWith(color: theme.primaryColor), ), const SizedBox(height: 8), diff --git a/lib/widgets/dialog_widgets/push_request_dialog.dart b/lib/widgets/dialog_widgets/push_request_dialog.dart index baca03212..f3a88453a 100644 --- a/lib/widgets/dialog_widgets/push_request_dialog.dart +++ b/lib/widgets/dialog_widgets/push_request_dialog.dart @@ -1,9 +1,9 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; -import 'package:privacyidea_authenticator/extensions/color_extension.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../l10n/app_localizations.dart'; +import '../../model/extensions/color_extension.dart'; +import '../../model/push_request.dart'; import '../../model/tokens/push_token.dart'; import '../../utils/globals.dart'; import '../../utils/lock_auth.dart'; @@ -11,16 +11,16 @@ import '../../utils/riverpod_providers.dart'; import '../press_button.dart'; import 'default_dialog.dart'; -class PushRequestDialog extends StatefulWidget { - final PushToken tokenWithPushRequest; +class PushRequestDialog extends ConsumerStatefulWidget { + final PushRequest pushRequest; - const PushRequestDialog(this.tokenWithPushRequest, {super.key}); + const PushRequestDialog(this.pushRequest, {super.key}); @override - State createState() => _PushRequestDialogState(); + ConsumerState createState() => _PushRequestDialogState(); } -class _PushRequestDialogState extends State { +class _PushRequestDialogState extends ConsumerState { static const titleScale = 1.35; static const questionScale = 1.1; double get lineHeight => Theme.of(context).textTheme.titleLarge?.fontSize ?? 16; @@ -45,8 +45,16 @@ class _PushRequestDialogState extends State { @override Widget build(BuildContext context) { final lineHeight = this.lineHeight; - final question = widget.tokenWithPushRequest.pushRequests.peek()?.question; - return isHandled + final question = widget.pushRequest.question; + final token = ref.watch(tokenProvider).getTokenBySerial(widget.pushRequest.serial); + if (token == null) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + ref.read(pushRequestProvider.notifier).remove(widget.pushRequest); + } + }); + } + return isHandled || token == null ? const SizedBox() : Container( color: Colors.transparent, @@ -63,15 +71,15 @@ class _PushRequestDialogState extends State { children: [ Text( AppLocalizations.of(context)!.requestInfo( - widget.tokenWithPushRequest.issuer, - widget.tokenWithPushRequest.label, + token.issuer, + token.label, ), style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: Theme.of(context).textTheme.titleMedium?.fontSize), textScaler: const TextScaler.linear(questionScale), textAlign: TextAlign.center, ), SizedBox(height: lineHeight), - if (question != null) ...[ + ...[ Text( question, style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: Theme.of(context).textTheme.titleMedium?.fontSize), @@ -85,9 +93,10 @@ class _PushRequestDialogState extends State { height: lineHeight * titleScale * 2 + 16, child: PressButton( onPressed: () async { - if (widget.tokenWithPushRequest.isLocked && - await lockAuth(localizedReason: AppLocalizations.of(context)!.authToAcceptPushRequest) == false) return; - globalRef?.read(pushRequestProvider.notifier).acceptPop(widget.tokenWithPushRequest); + if (token.isLocked && await lockAuth(localizedReason: AppLocalizations.of(context)!.authToAcceptPushRequest) == false) { + return; + } + globalRef?.read(pushRequestProvider.notifier).accept(token, widget.pushRequest); if (mounted) setState(() => isHandled = true); }, child: Row( @@ -117,12 +126,11 @@ class _PushRequestDialogState extends State { child: PressButton( style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.errorContainer)), onPressed: () async { - if (widget.tokenWithPushRequest.isLocked && - await lockAuth(localizedReason: AppLocalizations.of(context)!.authToDeclinePushRequest) == false) { + if (token.isLocked && await lockAuth(localizedReason: AppLocalizations.of(context)!.authToDeclinePushRequest) == false) { return; } dialogIsOpen = true; - await _showConfirmationDialog(widget.tokenWithPushRequest); + await _showConfirmationDialog(token); dialogIsOpen = false; }, child: Row( @@ -146,7 +154,7 @@ class _PushRequestDialogState extends State { ); } - Future _showConfirmationDialog(PushToken token) => showDialog( + Future _showConfirmationDialog(PushToken pushToken) => showDialog( useRootNavigator: false, context: globalNavigatorKey.currentContext!, builder: (BuildContext context) { @@ -178,7 +186,7 @@ class _PushRequestDialogState extends State { flex: 6, child: PressButton( onPressed: () { - globalRef?.read(pushRequestProvider.notifier).declinePop(token); + globalRef?.read(pushRequestProvider.notifier).decline(pushToken, widget.pushRequest); Navigator.of(context).pop(); if (mounted) setState(() => isHandled = true); }, @@ -213,7 +221,7 @@ class _PushRequestDialogState extends State { style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.errorContainer)), onPressed: () { //TODO: Notify issuer - globalRef?.read(pushRequestProvider.notifier).declinePop(token); + globalRef?.read(pushRequestProvider.notifier).decline(pushToken, widget.pushRequest); Navigator.of(context).pop(); if (mounted) setState(() => isHandled = true); }, diff --git a/lib/widgets/dialog_widgets/two_step_dialog.dart b/lib/widgets/dialog_widgets/two_step_dialog.dart index 28fc07284..d87b43d5e 100644 --- a/lib/widgets/dialog_widgets/two_step_dialog.dart +++ b/lib/widgets/dialog_widgets/two_step_dialog.dart @@ -24,11 +24,11 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; import 'package:privacyidea_authenticator/utils/crypto_utils.dart'; -import 'package:privacyidea_authenticator/utils/utils.dart'; import 'package:privacyidea_authenticator/utils/view_utils.dart'; import 'package:privacyidea_authenticator/widgets/dialog_widgets/default_dialog.dart'; import '../../utils/logger.dart'; +import '../../utils/utils.dart'; import '../widget_keys.dart'; class GenerateTwoStepDialog extends StatelessWidget { @@ -62,12 +62,8 @@ class GenerateTwoStepDialog extends StatelessWidget { } // 3. Show phone part if this widget is still mounted. - Navigator.of(context).pop(generatedSecret); - showAsyncDialog( - barrierDismissible: false, - builder: (context) => TwoStepDialog( - phoneChecksum: phoneChecksum, - )); + if (context.mounted) Navigator.of(context).pop(generatedSecret); + showAsyncDialog(barrierDismissible: false, builder: (context) => TwoStepDialog(phoneChecksum: phoneChecksum)); } @override @@ -119,7 +115,9 @@ class _TwoStepDialogState extends State { ), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + if (context.mounted) Navigator.of(context).pop(); + }, child: Text( AppLocalizations.of(context)!.dismiss, overflow: TextOverflow.fade, diff --git a/lib/widgets/focused_item_as_overlay.dart b/lib/widgets/focused_item_as_overlay.dart index 0abdee015..6f25a19ef 100644 --- a/lib/widgets/focused_item_as_overlay.dart +++ b/lib/widgets/focused_item_as_overlay.dart @@ -3,8 +3,9 @@ import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; -import '../utils/logger.dart'; +import '../l10n/app_localizations.dart'; +import '../utils/logger.dart'; import '../utils/utils.dart'; import 'pulse_icon.dart'; import 'tooltip_container.dart'; @@ -212,14 +213,17 @@ class _FocusedItemOverlayState extends State<_FocusedItemOverlay> { ), ), Positioned.fill( - child: GestureDetector( - onTapDown: (details) { - widget.onComplete?.call(); - }, - child: Container( - height: double.maxFinite, - width: double.maxFinite, - color: Colors.transparent, + child: Tooltip( + message: AppLocalizations.of(context)!.continueButton, + child: GestureDetector( + onTapDown: (details) { + widget.onComplete?.call(); + }, + child: Container( + height: double.maxFinite, + width: double.maxFinite, + color: Colors.transparent, + ), ), ), ), diff --git a/lib/widgets/hideable_widget_.dart b/lib/widgets/hideable_widget_.dart index 1937dd77c..217dc9ba5 100644 --- a/lib/widgets/hideable_widget_.dart +++ b/lib/widgets/hideable_widget_.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../model/tokens/token.dart'; +import '../l10n/app_localizations.dart'; +import '../model/tokens/otp_token.dart'; import '../utils/riverpod_providers.dart'; class HideableWidget extends ConsumerWidget { - final Token token; + final OTPToken token; final bool isHidden; final Widget child; const HideableWidget({ @@ -19,6 +20,7 @@ class HideableWidget extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return token.isLocked && isHidden ? IconButton( + tooltip: AppLocalizations.of(context)!.authenticateToShowOtp, onPressed: () async => ref.read(tokenProvider.notifier).showToken(token), icon: const Icon(Icons.remove_red_eye_outlined), ) diff --git a/lib/widgets/hideable_widget_trailing.dart b/lib/widgets/hideable_widget_trailing.dart deleted file mode 100644 index 3663a0807..000000000 --- a/lib/widgets/hideable_widget_trailing.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../l10n/app_localizations.dart'; -import '../model/tokens/token.dart'; -import '../utils/lock_auth.dart'; - -class HideableWidgetTrailing extends StatelessWidget { - final Token token; - final ValueNotifier isHiddenNotifier; - final Widget child; - const HideableWidgetTrailing({ - required this.child, - required this.token, - required this.isHiddenNotifier, - super.key, - }); - - @override - Widget build(BuildContext context) { - return token.isLocked && isHiddenNotifier.value - ? IconButton( - onPressed: () async { - if (await lockAuth(localizedReason: AppLocalizations.of(context)!.authenticateToShowOtp)) { - isHiddenNotifier.value = false; - } - }, - icon: const Icon(Icons.remove_red_eye_outlined), - ) - : child; - } -} diff --git a/lib/widgets/home_widgets/home_widget_action.dart b/lib/widgets/home_widgets/home_widget_action.dart index 62c315625..1b61b9c54 100644 --- a/lib/widgets/home_widgets/home_widget_action.dart +++ b/lib/widgets/home_widgets/home_widget_action.dart @@ -1,9 +1,9 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:privacyidea_authenticator/utils/home_widget_utils.dart'; -import '../../extensions/color_extension.dart'; +import '../../model/extensions/color_extension.dart'; +import '../../utils/home_widget_utils.dart'; import 'interfaces/flutter_home_widget_base.dart'; import 'interfaces/flutter_home_widget_builder.dart'; diff --git a/lib/widgets/home_widgets/home_widget_copied.dart b/lib/widgets/home_widgets/home_widget_copied.dart index faed851c8..6aa9ea7d9 100644 --- a/lib/widgets/home_widgets/home_widget_copied.dart +++ b/lib/widgets/home_widgets/home_widget_copied.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../utils/app_customizer.dart'; +import '../../utils/customization/extended_text_theme.dart'; import 'interfaces/flutter_home_widget_base.dart'; import 'interfaces/flutter_home_widget_builder.dart'; diff --git a/lib/widgets/home_widgets/home_widget_hidden.dart b/lib/widgets/home_widgets/home_widget_hidden.dart index 40074003f..256d3bdc6 100644 --- a/lib/widgets/home_widgets/home_widget_hidden.dart +++ b/lib/widgets/home_widgets/home_widget_hidden.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../utils/app_customizer.dart'; +import '../../utils/customization/extended_text_theme.dart'; import 'home_widget_otp.dart'; import 'interfaces/flutter_home_widget_base.dart'; import 'interfaces/flutter_home_widget_builder.dart'; diff --git a/lib/widgets/home_widgets/home_widget_otp.dart b/lib/widgets/home_widgets/home_widget_otp.dart index f0e690487..eee6d78bb 100644 --- a/lib/widgets/home_widgets/home_widget_otp.dart +++ b/lib/widgets/home_widgets/home_widget_otp.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../utils/app_customizer.dart'; +import '../../utils/customization/extended_text_theme.dart'; import '../../utils/utils.dart'; import 'interfaces/flutter_home_widget_base.dart'; import 'interfaces/flutter_home_widget_builder.dart'; @@ -48,7 +48,7 @@ class HomeWidgetOtp extends FlutterHomeWidgetBase { @override Widget build(BuildContext context) { - String text = otp.length > 10 ? insertCharAt(otp, '\n', otp.length ~/ 2) : insertCharAt(otp, ' ', otp.length ~/ 2); + String text = insertCharAt(otp, otp.length > 10 ? '\n' : ' ', (otp.length / 2).ceil()); return SizedBox( width: logicalSize.width, height: logicalSize.height, diff --git a/lib/widgets/home_widgets/home_widget_unlinked.dart b/lib/widgets/home_widgets/home_widget_unlinked.dart index b6687cb41..af3b17b0d 100644 --- a/lib/widgets/home_widgets/home_widget_unlinked.dart +++ b/lib/widgets/home_widgets/home_widget_unlinked.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../utils/app_customizer.dart'; +import '../../utils/customization/extended_text_theme.dart'; import 'interfaces/flutter_home_widget_base.dart'; import 'interfaces/flutter_home_widget_builder.dart'; diff --git a/lib/widgets/push_request_listener.dart b/lib/widgets/push_request_listener.dart index 6c7fae429..15aa0fda4 100644 --- a/lib/widgets/push_request_listener.dart +++ b/lib/widgets/push_request_listener.dart @@ -18,21 +18,20 @@ class _PushRequestListenerState extends ConsumerState { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { - PushProvider().pollForChallenges(isManually: false); + PushProvider.instance?.pollForChallenges(isManually: false); }); } @override Widget build(BuildContext context) { - final tokensWithPushRequest = ref.watch(tokenProvider).pushTokens.where((token) => token.pushRequests.isNotEmpty); - final tokenWithPushRequest = tokensWithPushRequest.isNotEmpty ? tokensWithPushRequest.first : null; + final pushRequest = ref.watch(pushRequestProvider).pushRequests.firstOrNull; return Stack( children: [ widget.child, - if (tokenWithPushRequest != null) + if (pushRequest != null) PushRequestDialog( - tokenWithPushRequest, - key: Key('${tokenWithPushRequest.pushRequests.peek().hashCode.toString()}#PushRequestDialog'), + pushRequest, + key: Key('${pushRequest.hashCode.toString()}#PushRequestDialog'), ), ], ); diff --git a/local_plugins/pi-authenticator-legacy/example/pubspec.lock b/local_plugins/pi-authenticator-legacy/example/pubspec.lock index 160000106..48e72786c 100644 --- a/local_plugins/pi-authenticator-legacy/example/pubspec.lock +++ b/local_plugins/pi-authenticator-legacy/example/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" fake_async: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index e4cc5b799..59b3a6f5b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "4eec93681221723a686ad580c2e7d960e1017cf1a4e0a263c2573c2c6b0bf5cd" + sha256: "3dee3db3468c5f4640a4e8aa9c1e22561c298976d8c39ed2fdd456a9a3db26e1" url: "https://pub.dev" source: hosted - version: "1.3.25" + version: "1.3.32" analyzer: dependency: transitive description: @@ -37,18 +37,18 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: "0763b45fa9294197a2885c8567927e2830ade852e5c896fd4ab7e0e348d0f373" url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.5.0" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" asn1lib: dependency: "direct main" description: @@ -117,10 +117,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.2" camera: dependency: "direct main" description: @@ -157,18 +157,18 @@ packages: dependency: transitive description: name: camera_android - sha256: "351429510121d179b9aac5a2e8cb525c3cd6c39f4d709c5f72dfb21726e52371" + sha256: "7b0aba6398afa8475e2bc9115d976efb49cf8db781e922572d443795c04a4f4f" url: "https://pub.dev" source: hosted - version: "0.10.8+16" + version: "0.10.9+1" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "8b113e43ee4434c9244c03c905432a0d5956cedaded3cd7381abaab89ce50297" + sha256: "7d021e8cd30d9b71b8b92b4ad669e80af432d722d18d6aac338572754a786c15" url: "https://pub.dev" source: hosted - version: "0.9.14+1" + version: "0.9.16" camera_platform_interface: dependency: transitive description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: camera_web - sha256: f18ccfb33b2a7c49a52ad5aa3f07330b7422faaecbdfd9b9fe8e51182f6ad67d + sha256: "9e9aba2fbab77ce2472924196ff8ac4dd8f9126c4f9a3096171cd1d870d6b26c" url: "https://pub.dev" source: hosted - version: "0.3.2+4" + version: "0.3.3" characters: dependency: transitive description: @@ -237,18 +237,18 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + sha256: db7a4e143dc72cc3cb2044ef9b052a7ebfe729513e6a82943bc3526f784365b8 url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.0.3" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "2.0.0" convert: dependency: transitive description: @@ -269,12 +269,12 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+1" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab @@ -293,10 +293,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: @@ -317,10 +317,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" + sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 url: "https://pub.dev" source: hosted - version: "9.1.2" + version: "10.1.0" device_info_plus_platform_interface: dependency: transitive description: @@ -397,10 +397,10 @@ packages: dependency: transitive description: name: file_selector_ios - sha256: b015154e6d9fddbc4d08916794df170b44531798c8dd709a026df162d07ad81d + sha256: "0a1196a9c5795858aa315332da2fb5c4bcfdcb312d8a4e27651f765b87904431" url: "https://pub.dev" source: hosted - version: "0.5.1+8" + version: "0.5.1+9" file_selector_linux: dependency: transitive description: @@ -429,10 +429,10 @@ packages: dependency: transitive description: name: file_selector_web - sha256: c0f025d460de3301b7bbbf837fc8d0759df85f182c635f1dd94934b4cdc92352 + sha256: "619e431b224711a3869e30dbd7d516f5f5a4f04b265013a50912f39e1abc88c8" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.4+1" file_selector_windows: dependency: transitive description: @@ -445,10 +445,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "53316975310c8af75a96e365f9fccb67d1c544ef0acdbf0d88bbe30eedd1c4f9" + sha256: "4aef2a23d0f3265545807d68fbc2f76a6b994ca3c778d88453b99325abd63284" url: "https://pub.dev" source: hosted - version: "2.27.0" + version: "2.30.1" firebase_core_platform_interface: dependency: transitive description: @@ -461,34 +461,34 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: c8e1d59385eee98de63c92f961d2a7062c5d9a65e7f45bdc7f1b0b205aab2492 + sha256: "67f2fcc600fc78c2f731c370a3a5e6c87ee862e3a2fba6f951eca6d5dafe5c29" url: "https://pub.dev" source: hosted - version: "2.11.5" + version: "2.16.0" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: e41586e0fd04fe9a40424f8b0053d0832e6d04f49e020cdaf9919209a28497e9 + sha256: "73a43445a7f8c6e6327f0ec3922b1c99a9f4a0e4896197bfe10a88259f775aad" url: "https://pub.dev" source: hosted - version: "14.7.19" + version: "14.9.1" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: f7a9d74ff7fc588a924f6b2eaeaa148b0db521b13a9db55f6ad45864fa98c06e + sha256: "675527aadccb679c9dfd43a4558690427123ac1e99f03eef5bbce9dc216edc91" url: "https://pub.dev" source: hosted - version: "4.5.27" + version: "4.5.34" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: fc21e771166860c55b103701c5ac7cdb2eec28897b97c42e6e5703cbedf9e02e + sha256: "66deff69307f54fc7a20732b4278af327ae378b86607d5ac877d8f2201fe881e" url: "https://pub.dev" source: hosted - version: "3.6.8" + version: "3.8.4" fixnum: dependency: transitive description: @@ -501,10 +501,10 @@ packages: dependency: "direct main" description: name: fluentui_system_icons - sha256: "14c52f5e66620e12803cf7e1514d695474282f0821c9fbae60e6721036dbe1a7" + sha256: "9bb6c46ff0351f0b20a79bbe5afcaf533af5f06fd8ac22407b57eaee8025004c" url: "https://pub.dev" source: hosted - version: "1.1.231" + version: "1.1.236" flutter: dependency: "direct main" description: flutter @@ -519,18 +519,18 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1 + sha256: "8cdc719114ab1c86c64bb7a86d3a679674c3637edd229e3a994797d4a1504ce4" url: "https://pub.dev" source: hosted - version: "17.0.0" + version: "17.1.0" flutter_local_notifications_linux: dependency: transitive description: @@ -543,10 +543,10 @@ packages: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" url: "https://pub.dev" source: hosted - version: "7.0.0+1" + version: "7.1.0" flutter_localizations: dependency: "direct main" description: flutter @@ -564,10 +564,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.19" flutter_riverpod: dependency: "direct main" description: @@ -628,10 +628,10 @@ packages: dependency: "direct main" description: name: flutter_slidable - sha256: "19ed4813003a6ff4e9c6bcce37e792a2a358919d7603b2b31ff200229191e44c" + sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -642,14 +642,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_zxing: - dependency: "direct main" - description: - name: flutter_zxing - sha256: ed8da1a0e4650c7645e94b5ab92f855a6b628385d815641761ccbdcaee131683 - url: "https://pub.dev" - source: hosted - version: "1.5.2" flutterlifecyclehooks: dependency: "direct main" description: @@ -699,18 +691,18 @@ packages: dependency: "direct main" description: name: home_widget - sha256: c58a9e6d3b94490f1a8d5ddcbeeeeebc79abd0befe5889c26b0713fb09be6857 + sha256: "29565bfee4b32eaf9e7e8b998d504618b779a74b2b1ac62dd4dac7468e66f1a3" url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.5.0" http: dependency: "direct main" description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -735,77 +727,13 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.7" - image_picker: - dependency: transitive - description: - name: image_picker - sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" - url: "https://pub.dev" - source: hosted - version: "1.0.7" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" - url: "https://pub.dev" - source: hosted - version: "0.8.9+3" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 - url: "https://pub.dev" - source: hosted - version: "3.0.2" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 - url: "https://pub.dev" - source: hosted - version: "0.8.9+1" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: "3d2c323daea9d60608f1caf30be32a938916f4975434b8352e6f73dae496da38" - url: "https://pub.dev" - source: hosted - version: "2.9.4" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" integration_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" @@ -832,18 +760,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "6.8.0" leak_tracker: dependency: transitive description: @@ -888,10 +816,10 @@ packages: dependency: "direct main" description: name: local_auth_android - sha256: "3bcd732dda7c75fcb7ddaef12e131230f53dcc8c00790d0d6efb3aa0fbbeda57" + sha256: e0e5b1ea247c5a0951c13a7ee13dc1beae69750e6a2e1910d1ed6a3cd4d56943 url: "https://pub.dev" source: hosted - version: "1.0.37" + version: "1.0.38" local_auth_darwin: dependency: "direct main" description: @@ -920,10 +848,10 @@ packages: dependency: "direct main" description: name: logger - sha256: b3ff55aeb08d9d8901b767650285872cb1bb8f508373b3e348d60268b0c7f770 + sha256: "8c94b8c219e7e50194efc8771cd0e9f10807d8d3e219af473d89b06cc2ee4e04" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" logging: dependency: transitive description: @@ -1032,18 +960,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + sha256: "2c582551839386fa7ddbc7770658be7c0f87f388a4bff72066478f597c34d17f" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "7.0.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" path: dependency: transitive description: @@ -1053,21 +981,21 @@ packages: source: hosted version: "1.9.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" path_provider_foundation: dependency: transitive description: @@ -1104,10 +1032,10 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44" + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "11.3.1" permission_handler_android: dependency: transitive description: @@ -1120,10 +1048,10 @@ packages: dependency: transitive description: name: permission_handler_apple - sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b + sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 url: "https://pub.dev" source: hosted - version: "9.4.0" + version: "9.4.4" permission_handler_html: dependency: transitive description: @@ -1136,10 +1064,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" + sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.1" permission_handler_windows: dependency: transitive description: @@ -1183,10 +1111,10 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "79fbafed02cfdbe85ef3fd06c7f4bc2cbcba0177e61b765264853d4253b21744" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "3.9.0" pool: dependency: transitive description: @@ -1204,7 +1132,7 @@ packages: source: hosted version: "5.0.2" protobuf: - dependency: "direct dev" + dependency: "direct main" description: name: protobuf sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" @@ -1227,6 +1155,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" riverpod: dependency: transitive description: @@ -1239,18 +1183,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_foundation: dependency: transitive description: @@ -1279,10 +1223,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: @@ -1323,6 +1267,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + simple_icons: + dependency: "direct main" + description: + name: simple_icons + sha256: "30067d70a9d72923fbc80e142e17fa46085dfa970e66bc4bede3be4819d05901" + url: "https://pub.dev" + source: hosted + version: "10.1.3" sky_engine: dependency: transitive description: flutter @@ -1460,10 +1412,10 @@ packages: dependency: transitive description: name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5 url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.3" timing: dependency: transitive description: @@ -1508,18 +1460,18 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.2.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_ios: dependency: transitive description: @@ -1556,10 +1508,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.1" url_launcher_windows: dependency: transitive description: @@ -1572,10 +1524,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vector_math: dependency: transitive description: @@ -1604,18 +1556,18 @@ packages: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.5" webdriver: dependency: transitive description: @@ -1636,18 +1588,18 @@ packages: dependency: transitive description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.5.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ca1571783..2fccb2787 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ publish_to: none # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 4.3.1+403107 # TODO Set the right version number +version: 4.4.0+404001 # TODO Set the right version number # version: major.minor.build + 2x major|2x minor|3x build # version: version number + build number (optional) # android: build-name + versionCode @@ -30,12 +30,12 @@ dependencies: sdk: flutter app_minimizer: ^1.0.0+2 flutter_local_notifications: ^17.0.0 - home_widget: ^0.4.1 + home_widget: ^0.5.0 image: ^4.1.6 json_annotation: ^4.8.1 local_auth: ^2.1.6 + local_auth_android: ^1.0.37 local_auth_darwin: ^1.2.2 - local_auth_android: ^1.0.32 logger: ^2.0.0 permission_handler: ^11.0.0 pi_authenticator_legacy: @@ -56,13 +56,13 @@ dependencies: shared_preferences: ^2.2.0 flutter_secure_storage: ^9.0.0 # Info - package_info_plus: ^5.0.1 - device_info_plus: ^9.0.3 + package_info_plus: ^7.0.0 + device_info_plus: ^10.0.1 # URI uni_links: ^0.5.1 url_launcher: ^6.0.12 http: ^1.2.0 - connectivity_plus: ^5.0.1 + connectivity_plus: ^6.0.1 flutter_mailer: ^2.1.1 # Riverpod flutter_riverpod: ^2.3.6 @@ -77,10 +77,17 @@ dependencies: flutter_slidable: ^3.0.0 expandable: ^5.0.1 camera: ^0.10.5+9 - flutter_zxing: ^1.5.2 firebase_messaging: ^14.7.19 - firebase_core: ^2.27.0 collection: ^1.18.0 + crypto: ^3.0.3 + intl: ^0.18.1 + protobuf: ^3.1.0 + firebase_core: ^2.27.0 + simple_icons: ^10.1.3 + path_provider: ^2.1.2 + qr_flutter: ^4.1.0 + # file_saver: ^0.2.12 + dev_dependencies: flutter_driver: @@ -92,7 +99,6 @@ dev_dependencies: flutter_lints: ^3.0.0 mockito: ^5.4.2 test: ^1.24.1 - protobuf: ^3.1.0 # dependencies to serialize objects to json diff --git a/test/tests_app_wrapper.mocks.dart b/test/tests_app_wrapper.mocks.dart index 2fc4ec168..3d940a926 100644 --- a/test/tests_app_wrapper.mocks.dart +++ b/test/tests_app_wrapper.mocks.dart @@ -11,12 +11,18 @@ import 'package:http/http.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i14; import 'package:pointycastle/export.dart' as _i4; -import 'package:privacyidea_authenticator/interfaces/repo/introduction_repository.dart' as _i19; -import 'package:privacyidea_authenticator/interfaces/repo/settings_repository.dart' as _i9; -import 'package:privacyidea_authenticator/interfaces/repo/token_folder_repository.dart' as _i10; -import 'package:privacyidea_authenticator/interfaces/repo/token_repository.dart' as _i6; -import 'package:privacyidea_authenticator/model/states/introduction_state.dart' as _i5; -import 'package:privacyidea_authenticator/model/states/settings_state.dart' as _i2; +import 'package:privacyidea_authenticator/interfaces/repo/introduction_repository.dart' + as _i19; +import 'package:privacyidea_authenticator/interfaces/repo/settings_repository.dart' + as _i9; +import 'package:privacyidea_authenticator/interfaces/repo/token_folder_repository.dart' + as _i10; +import 'package:privacyidea_authenticator/interfaces/repo/token_repository.dart' + as _i6; +import 'package:privacyidea_authenticator/model/states/introduction_state.dart' + as _i5; +import 'package:privacyidea_authenticator/model/states/settings_state.dart' + as _i2; import 'package:privacyidea_authenticator/model/token_folder.dart' as _i11; import 'package:privacyidea_authenticator/model/tokens/push_token.dart' as _i16; import 'package:privacyidea_authenticator/model/tokens/token.dart' as _i8; @@ -77,7 +83,9 @@ class _FakeRSAPrivateKey_3 extends _i1.SmartFake implements _i4.RSAPrivateKey { ); } -class _FakeAsymmetricKeyPair_4 extends _i1.SmartFake implements _i4.AsymmetricKeyPair { +class _FakeAsymmetricKeyPair_4 extends _i1.SmartFake + implements _i4.AsymmetricKeyPair { _FakeAsymmetricKeyPair_4( Object parent, Invocation parentInvocation, @@ -87,7 +95,8 @@ class _FakeAsymmetricKeyPair_4> saveOrReplaceTokens(List<_i8.Token>? tokens) => (super.noSuchMethod( + _i7.Future<_i8.Token?> loadToken(String? id) => (super.noSuchMethod( Invocation.method( - #saveNewState, - [tokens], + #loadToken, + [id], ), - returnValue: _i7.Future>.value(<_i8.Token>[]), - returnValueForMissingStub: _i7.Future>.value(<_i8.Token>[]), - ) as _i7.Future>); + returnValue: _i7.Future<_i8.Token?>.value(), + returnValueForMissingStub: _i7.Future<_i8.Token?>.value(), + ) as _i7.Future<_i8.Token?>); @override _i7.Future> loadTokens() => (super.noSuchMethod( @@ -118,26 +127,62 @@ class MockTokenRepository extends _i1.Mock implements _i6.TokenRepository { [], ), returnValue: _i7.Future>.value(<_i8.Token>[]), - returnValueForMissingStub: _i7.Future>.value(<_i8.Token>[]), + returnValueForMissingStub: + _i7.Future>.value(<_i8.Token>[]), ) as _i7.Future>); @override - _i7.Future> deleteTokens(List<_i8.Token>? tokens) => (super.noSuchMethod( + _i7.Future saveOrReplaceToken(_i8.Token? token) => (super.noSuchMethod( + Invocation.method( + #saveOrReplaceToken, + [token], + ), + returnValue: _i7.Future.value(false), + returnValueForMissingStub: _i7.Future.value(false), + ) as _i7.Future); + + @override + _i7.Future> saveOrReplaceTokens( + List? tokens) => + (super.noSuchMethod( + Invocation.method( + #saveOrReplaceTokens, + [tokens], + ), + returnValue: _i7.Future>.value([]), + returnValueForMissingStub: _i7.Future>.value([]), + ) as _i7.Future>); + + @override + _i7.Future deleteToken(_i8.Token? token) => (super.noSuchMethod( + Invocation.method( + #deleteToken, + [token], + ), + returnValue: _i7.Future.value(false), + returnValueForMissingStub: _i7.Future.value(false), + ) as _i7.Future); + + @override + _i7.Future> deleteTokens(List? tokens) => + (super.noSuchMethod( Invocation.method( #deleteTokens, [tokens], ), - returnValue: _i7.Future>.value(<_i8.Token>[]), - returnValueForMissingStub: _i7.Future>.value(<_i8.Token>[]), - ) as _i7.Future>); + returnValue: _i7.Future>.value([]), + returnValueForMissingStub: _i7.Future>.value([]), + ) as _i7.Future>); } /// A class which mocks [SettingsRepository]. /// /// See the documentation for Mockito's code generation for more information. -class MockSettingsRepository extends _i1.Mock implements _i9.SettingsRepository { +class MockSettingsRepository extends _i1.Mock + implements _i9.SettingsRepository { @override - _i7.Future saveSettings(_i2.SettingsState? settings) => (super.noSuchMethod( + _i7.Future saveSettings(_i2.SettingsState? settings) => + (super.noSuchMethod( Invocation.method( #saveSettings, [settings], @@ -159,7 +204,8 @@ class MockSettingsRepository extends _i1.Mock implements _i9.SettingsRepository [], ), )), - returnValueForMissingStub: _i7.Future<_i2.SettingsState>.value(_FakeSettingsState_0( + returnValueForMissingStub: + _i7.Future<_i2.SettingsState>.value(_FakeSettingsState_0( this, Invocation.method( #loadSettings, @@ -172,16 +218,18 @@ class MockSettingsRepository extends _i1.Mock implements _i9.SettingsRepository /// A class which mocks [TokenFolderRepository]. /// /// See the documentation for Mockito's code generation for more information. -class MockTokenFolderRepository extends _i1.Mock implements _i10.TokenFolderRepository { +class MockTokenFolderRepository extends _i1.Mock + implements _i10.TokenFolderRepository { @override - _i7.Future> saveOrReplaceFolders(List<_i11.TokenFolder>? folders) => (super.noSuchMethod( + _i7.Future saveReplaceList(List<_i11.TokenFolder>? folders) => + (super.noSuchMethod( Invocation.method( - #saveOrReplaceFolders, + #saveReplaceList, [folders], ), - returnValue: _i7.Future>.value(<_i11.TokenFolder>[]), - returnValueForMissingStub: _i7.Future>.value(<_i11.TokenFolder>[]), - ) as _i7.Future>); + returnValue: _i7.Future.value(false), + returnValueForMissingStub: _i7.Future.value(false), + ) as _i7.Future); @override _i7.Future> loadFolders() => (super.noSuchMethod( @@ -189,15 +237,18 @@ class MockTokenFolderRepository extends _i1.Mock implements _i10.TokenFolderRepo #loadFolders, [], ), - returnValue: _i7.Future>.value(<_i11.TokenFolder>[]), - returnValueForMissingStub: _i7.Future>.value(<_i11.TokenFolder>[]), + returnValue: + _i7.Future>.value(<_i11.TokenFolder>[]), + returnValueForMissingStub: + _i7.Future>.value(<_i11.TokenFolder>[]), ) as _i7.Future>); } /// A class which mocks [PrivacyIdeaIOClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockPrivacyIdeaIOClient extends _i1.Mock implements _i12.PrivacyIdeaIOClient { +class MockPrivacyIdeaIOClient extends _i1.Mock + implements _i12.PrivacyIdeaIOClient { @override _i7.Future triggerNetworkAccessPermission({ required Uri? url, @@ -246,7 +297,8 @@ class MockPrivacyIdeaIOClient extends _i1.Mock implements _i12.PrivacyIdeaIOClie }, ), )), - returnValueForMissingStub: _i7.Future<_i3.Response>.value(_FakeResponse_1( + returnValueForMissingStub: + _i7.Future<_i3.Response>.value(_FakeResponse_1( this, Invocation.method( #doPost, @@ -288,7 +340,8 @@ class MockPrivacyIdeaIOClient extends _i1.Mock implements _i12.PrivacyIdeaIOClie }, ), )), - returnValueForMissingStub: _i7.Future<_i3.Response>.value(_FakeResponse_1( + returnValueForMissingStub: + _i7.Future<_i3.Response>.value(_FakeResponse_1( this, Invocation.method( #doGet, @@ -308,7 +361,8 @@ class MockPrivacyIdeaIOClient extends _i1.Mock implements _i12.PrivacyIdeaIOClie /// See the documentation for Mockito's code generation for more information. class MockRsaUtils extends _i1.Mock implements _i13.RsaUtils { @override - _i4.RSAPublicKey deserializeRSAPublicKeyPKCS1(String? keyStr) => (super.noSuchMethod( + _i4.RSAPublicKey deserializeRSAPublicKeyPKCS1(String? keyStr) => + (super.noSuchMethod( Invocation.method( #deserializeRSAPublicKeyPKCS1, [keyStr], @@ -330,7 +384,8 @@ class MockRsaUtils extends _i1.Mock implements _i13.RsaUtils { ) as _i4.RSAPublicKey); @override - String serializeRSAPublicKeyPKCS1(_i4.RSAPublicKey? publicKey) => (super.noSuchMethod( + String serializeRSAPublicKeyPKCS1(_i4.RSAPublicKey? publicKey) => + (super.noSuchMethod( Invocation.method( #serializeRSAPublicKeyPKCS1, [publicKey], @@ -352,7 +407,8 @@ class MockRsaUtils extends _i1.Mock implements _i13.RsaUtils { ) as String); @override - _i4.RSAPublicKey deserializeRSAPublicKeyPKCS8(String? keyStr) => (super.noSuchMethod( + _i4.RSAPublicKey deserializeRSAPublicKeyPKCS8(String? keyStr) => + (super.noSuchMethod( Invocation.method( #deserializeRSAPublicKeyPKCS8, [keyStr], @@ -374,7 +430,8 @@ class MockRsaUtils extends _i1.Mock implements _i13.RsaUtils { ) as _i4.RSAPublicKey); @override - String serializeRSAPublicKeyPKCS8(_i4.RSAPublicKey? key) => (super.noSuchMethod( + String serializeRSAPublicKeyPKCS8(_i4.RSAPublicKey? key) => + (super.noSuchMethod( Invocation.method( #serializeRSAPublicKeyPKCS8, [key], @@ -396,7 +453,8 @@ class MockRsaUtils extends _i1.Mock implements _i13.RsaUtils { ) as String); @override - String serializeRSAPrivateKeyPKCS1(_i4.RSAPrivateKey? key) => (super.noSuchMethod( + String serializeRSAPrivateKeyPKCS1(_i4.RSAPrivateKey? key) => + (super.noSuchMethod( Invocation.method( #serializeRSAPrivateKeyPKCS1, [key], @@ -418,7 +476,8 @@ class MockRsaUtils extends _i1.Mock implements _i13.RsaUtils { ) as String); @override - _i4.RSAPrivateKey deserializeRSAPrivateKeyPKCS1(String? keyStr) => (super.noSuchMethod( + _i4.RSAPrivateKey deserializeRSAPrivateKeyPKCS1(String? keyStr) => + (super.noSuchMethod( Invocation.method( #deserializeRSAPrivateKeyPKCS1, [keyStr], @@ -476,27 +535,34 @@ class MockRsaUtils extends _i1.Mock implements _i13.RsaUtils { ) as _i7.Future); @override - _i7.Future<_i4.AsymmetricKeyPair<_i4.RSAPublicKey, _i4.RSAPrivateKey>> generateRSAKeyPair() => (super.noSuchMethod( - Invocation.method( - #generateRSAKeyPair, - [], - ), - returnValue: _i7.Future<_i4.AsymmetricKeyPair<_i4.RSAPublicKey, _i4.RSAPrivateKey>>.value(_FakeAsymmetricKeyPair_4<_i4.RSAPublicKey, _i4.RSAPrivateKey>( - this, - Invocation.method( - #generateRSAKeyPair, - [], - ), - )), - returnValueForMissingStub: - _i7.Future<_i4.AsymmetricKeyPair<_i4.RSAPublicKey, _i4.RSAPrivateKey>>.value(_FakeAsymmetricKeyPair_4<_i4.RSAPublicKey, _i4.RSAPrivateKey>( - this, - Invocation.method( - #generateRSAKeyPair, - [], - ), - )), - ) as _i7.Future<_i4.AsymmetricKeyPair<_i4.RSAPublicKey, _i4.RSAPrivateKey>>); + _i7.Future<_i4.AsymmetricKeyPair<_i4.RSAPublicKey, _i4.RSAPrivateKey>> + generateRSAKeyPair() => (super.noSuchMethod( + Invocation.method( + #generateRSAKeyPair, + [], + ), + returnValue: _i7.Future< + _i4.AsymmetricKeyPair<_i4.RSAPublicKey, + _i4.RSAPrivateKey>>.value( + _FakeAsymmetricKeyPair_4<_i4.RSAPublicKey, _i4.RSAPrivateKey>( + this, + Invocation.method( + #generateRSAKeyPair, + [], + ), + )), + returnValueForMissingStub: _i7.Future< + _i4.AsymmetricKeyPair<_i4.RSAPublicKey, + _i4.RSAPrivateKey>>.value( + _FakeAsymmetricKeyPair_4<_i4.RSAPublicKey, _i4.RSAPrivateKey>( + this, + Invocation.method( + #generateRSAKeyPair, + [], + ), + )), + ) as _i7.Future< + _i4.AsymmetricKeyPair<_i4.RSAPublicKey, _i4.RSAPrivateKey>>); @override String createBase32Signature( @@ -584,14 +650,67 @@ class MockFirebaseUtils extends _i1.Mock implements _i17.FirebaseUtils { returnValue: _i7.Future.value(), returnValueForMissingStub: _i7.Future.value(), ) as _i7.Future); + + @override + _i7.Future deleteFirebaseToken() => (super.noSuchMethod( + Invocation.method( + #deleteFirebaseToken, + [], + ), + returnValue: _i7.Future.value(false), + returnValueForMissingStub: _i7.Future.value(false), + ) as _i7.Future); + + @override + _i7.Future setCurrentFirebaseToken(String? str) => (super.noSuchMethod( + Invocation.method( + #setCurrentFirebaseToken, + [str], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future getCurrentFirebaseToken() => (super.noSuchMethod( + Invocation.method( + #getCurrentFirebaseToken, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future setNewFirebaseToken(String? str) => (super.noSuchMethod( + Invocation.method( + #setNewFirebaseToken, + [str], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future getNewFirebaseToken() => (super.noSuchMethod( + Invocation.method( + #getNewFirebaseToken, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); } /// A class which mocks [IntroductionRepository]. /// /// See the documentation for Mockito's code generation for more information. -class MockIntroductionRepository extends _i1.Mock implements _i19.IntroductionRepository { +class MockIntroductionRepository extends _i1.Mock + implements _i19.IntroductionRepository { @override - _i7.Future saveCompletedIntroductions(_i5.IntroductionState? introductions) => (super.noSuchMethod( + _i7.Future saveCompletedIntroductions( + _i5.IntroductionState? introductions) => + (super.noSuchMethod( Invocation.method( #saveCompletedIntroductions, [introductions], @@ -601,19 +720,22 @@ class MockIntroductionRepository extends _i1.Mock implements _i19.IntroductionRe ) as _i7.Future); @override - _i7.Future<_i5.IntroductionState> loadCompletedIntroductions() => (super.noSuchMethod( + _i7.Future<_i5.IntroductionState> loadCompletedIntroductions() => + (super.noSuchMethod( Invocation.method( #loadCompletedIntroductions, [], ), - returnValue: _i7.Future<_i5.IntroductionState>.value(_FakeIntroductionState_5( + returnValue: + _i7.Future<_i5.IntroductionState>.value(_FakeIntroductionState_5( this, Invocation.method( #loadCompletedIntroductions, [], ), )), - returnValueForMissingStub: _i7.Future<_i5.IntroductionState>.value(_FakeIntroductionState_5( + returnValueForMissingStub: + _i7.Future<_i5.IntroductionState>.value(_FakeIntroductionState_5( this, Invocation.method( #loadCompletedIntroductions, diff --git a/test/unit_test/model/encryption/aes_encrypted_test.dart b/test/unit_test/model/encryption/aes_encrypted_test.dart new file mode 100644 index 000000000..4db9484ff --- /dev/null +++ b/test/unit_test/model/encryption/aes_encrypted_test.dart @@ -0,0 +1,148 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:cryptography/cryptography.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/encryption/aes_encrypted.dart'; + +void main() { + _testAesEncrypted(); +} + +void _testAesEncrypted() { + group('Aes Encrypted', () { + test('constructor', () { + final AesEncrypted aesEncrypted = AesEncrypted( + data: Uint8List.fromList([41, 142, 95, 156]), + salt: Uint8List.fromList(List.generate(16, (index) => index)), + iv: Uint8List.fromList(List.generate(16, (index) => index)), + mac: const Mac([103, 169, 139, 92, 212, 40, 200, 3, 208, 110, 165, 128, 152, 185, 48, 3]), + kdf: Pbkdf2( + macAlgorithm: AesEncrypted.defaultMacAlgorithm, + iterations: AesEncrypted.defaultIterations, + bits: AesEncrypted.defaultBits, + ), + cypher: AesGcm.with256bits(), + ); + expect(aesEncrypted, isNotNull); + expect(aesEncrypted.data, Uint8List.fromList([41, 142, 95, 156])); + expect(aesEncrypted.salt, Uint8List.fromList(List.generate(16, (index) => index))); + expect(aesEncrypted.iv, Uint8List.fromList(List.generate(16, (index) => index))); + expect( + aesEncrypted.kdf, + Pbkdf2( + macAlgorithm: AesEncrypted.defaultMacAlgorithm, + iterations: AesEncrypted.defaultIterations, + bits: AesEncrypted.defaultBits, + ), + ); + expect(aesEncrypted.cypher, AesGcm.with256bits()); + expect(aesEncrypted.mac, const Mac([103, 169, 139, 92, 212, 40, 200, 3, 208, 110, 165, 128, 152, 185, 48, 3])); + }); + test('encrypt', () async { + final AesEncrypted aesEncrypted = await AesEncrypted.encrypt( + data: "test", + password: "password", + salt: Uint8List.fromList(List.generate(16, (index) => index)), + iv: Uint8List.fromList(List.generate(16, (index) => index)), + ); + expect(aesEncrypted, isNotNull); + expect(aesEncrypted.data, Uint8List.fromList([41, 142, 95, 156])); + final decrypted = await aesEncrypted.decrypt("password"); + expect(decrypted, Uint8List.fromList([116, 101, 115, 116])); + final decryptedString = await aesEncrypted.decryptToString("password"); + expect(decryptedString, "test"); + }); + test('decrypt', () async { + final AesEncrypted aesEncrypted = AesEncrypted( + data: Uint8List.fromList([41, 142, 95, 156]), + salt: Uint8List.fromList(List.generate(16, (index) => index)), + iv: Uint8List.fromList(List.generate(16, (index) => index)), + mac: const Mac([103, 169, 139, 92, 212, 40, 200, 3, 208, 110, 165, 128, 152, 185, 48, 3]), + kdf: Pbkdf2( + macAlgorithm: AesEncrypted.defaultMacAlgorithm, + iterations: AesEncrypted.defaultIterations, + bits: AesEncrypted.defaultBits, + ), + cypher: AesGcm.with256bits(), + ); + final decrypted = await aesEncrypted.decrypt("password"); + expect(decrypted, Uint8List.fromList([116, 101, 115, 116])); + }); + test('decryptToString', () async { + final AesEncrypted aesEncrypted = AesEncrypted( + data: Uint8List.fromList([41, 142, 95, 156]), + salt: Uint8List.fromList(List.generate(16, (index) => index)), + iv: Uint8List.fromList(List.generate(16, (index) => index)), + mac: const Mac([103, 169, 139, 92, 212, 40, 200, 3, 208, 110, 165, 128, 152, 185, 48, 3]), + kdf: Pbkdf2( + macAlgorithm: AesEncrypted.defaultMacAlgorithm, + iterations: AesEncrypted.defaultIterations, + bits: AesEncrypted.defaultBits, + ), + cypher: AesGcm.with256bits(), + ); + final decrypted = await aesEncrypted.decryptToString("password"); + expect(decrypted, "test"); + }); + test('toJson', () { + final AesEncrypted aesEncrypted = AesEncrypted( + data: Uint8List.fromList([41, 142, 95, 156]), + salt: Uint8List.fromList(List.generate(16, (index) => index)), + iv: Uint8List.fromList(List.generate(16, (index) => index)), + mac: Mac.empty, + kdf: Pbkdf2( + macAlgorithm: AesEncrypted.defaultMacAlgorithm, + iterations: AesEncrypted.defaultIterations, + bits: AesEncrypted.defaultBits, + ), + cypher: AesGcm.with256bits(), + ); + expect( + jsonEncode(aesEncrypted.toJson()), + '{"data":"KY5fnA==","salt":"AAECAwQFBgcICQoLDA0ODw==","iv":"AAECAwQFBgcICQoLDA0ODw==","mac":"","kdf":{"algorithm":"Pbkdf2","macAlgorithm":{"algorithm":"Hmac","hashAlgorithm":{"algorithm":"DartSha256"}},"iterations":100000,"bits":256},"cypher":{"algorithm":"AesGcm","secretKeyLength":32}}', + ); + }); + + test('toJson 2', () { +// TODO: implement test + }); + test('toJson 3', () { +// TODO: implement test + }); + test('toJson 4', () { +// TODO: implement test + }); + + test('fromJson', () async { + final json = { + "data": "KY5fnA==", + "salt": "AAECAwQFBgcICQoLDA0ODw==", + "iv": "AAECAwQFBgcICQoLDA0ODw==", + "mac": "Z6mLXNQoyAPQbqWAmLkwAw==", + "kdf": { + "algorithm": "Pbkdf2", + "macAlgorithm": { + "algorithm": "Hmac", + "hashAlgorithm": {"algorithm": "DartSha256"} + }, + "iterations": 100000, + "bits": 256 + }, + "cypher": {"algorithm": "AesGcm", "secretKeyLength": 32} + }; + final aesEncrypted = AesEncrypted.fromJson(json); + final decrypted = await aesEncrypted.decryptToString("password"); + expect(decrypted, "test"); + }); + }); + test('fromJson 2', () { + // TODO: implement test + }); + test('fromJson 3', () { + // TODO: implement test + }); + test('fromJson 4', () { + // TODO: implement test + }); +} diff --git a/test/unit_test/model/encryption/token_encryption_test.dart b/test/unit_test/model/encryption/token_encryption_test.dart new file mode 100644 index 000000000..774ebfa5f --- /dev/null +++ b/test/unit_test/model/encryption/token_encryption_test.dart @@ -0,0 +1,89 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/encryption/token_encryption.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/tokens/day_password_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/steam_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor.dart'; + +void main() { + _testTokenEncryption(); +} + +void _testTokenEncryption() { + group('Token Encryption', () { + test('encrypt', () async { + final tokensList = [ + HOTPToken(id: 'id1', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret1'), + TOTPToken(period: 30, id: 'id2', algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2'), + SteamToken(id: 'id3', secret: 'secret3'), + DayPasswordToken(period: const Duration(hours: 24), id: 'id4', algorithm: Algorithms.SHA512, digits: 10, secret: 'secret4'), + PushToken(serial: 'serial', id: 'id5'), + ]; + final encrypted = await TokenEncryption.encrypt(tokens: tokensList, password: 'password'); + expect(encrypted.isNotEmpty, true); + expect(encrypted.contains('"data":"'), true); + }); + test('decrypt', () async { + const encrypted = + '{"data":"jW5TJIY5dApfjZwYxJO7U5TYoV8JDbSHqlD2iPVDri8KrrisYRFy0ewg+YmU8XH9SS+TzEppAc4tbC69ZLXt5FLbQFprnJgP3eHEIw3ok1aHAaALtClyLnCNW265IjSrdqYdXm4DSHGG3Ol+9SyuCNjKwgdmkRO4Oqa2PimL0oOyjMLwVp908PY65lckBPAvX9CeAuLwglMCmg36tr2u0lKiPDqmYexPlpuriZOuzpBN4x+hWU75hBeo8hAJNIpnEBLCBufnOFCfFxgpr2mx4AsMh79AIeTENSTE2k327CKPpnJYXKfCdTVwVKtreeWyp4tN++9ACjmDx7QCRzAuDLHucyP4cE4gQ3uDkhhLtAOhaBlkTHWfQ0KP0dq3O5zQE6IwXRaMhN8kBiwqkQALjEtwhbWqtJPVK6fTYpGFb+gNg5dqwig4jx5h90drUUtlWWWvHCtAxFxNVgLtJIoAcHTrJy1rHU3gO85EaUClLYOQIx17gyA3FhO97VwRkk+8b8+kurjnEk+CVH3CTsBSEOKHMQDr2euoTlLukADm9qrJcXkprPfHLUnSCKAJ+9cDMvD13+Fa+xK7ybBnGnG13PkeNJplpwxNprITrvzq8QDpLBmAIaTeEbev9+qpuUOkS1UsDiXaYw/0tsRmsI0vc+864amfXMHiKl1VaAdL58/GjkveCu+nteers2Mubk48qWVyiw1MFR8c1gxDrL+V0WFD/YACNOjFUnUVP43XosbdM+7DRtW5m08uIcrap2SF7+Fzg9ye3WLSLCzAg5v6oNijHnaxNiWNaaX88vjLbCJAj00OX3xZGqefVMF4hV5l2SkTICEBh9Q21ZMJvA1WVs0LsYK2i9DHKVQohvhpjqCyn9xEGEvEOHOOYWNiBhLdEEQojfkdzmGOAw17Qi/7Ttd5bboMmUg6lIbkiDlfnkB6B2XtEmj/tASQJkWcWtamds+5VYu1j7L12Yk+133CeBXRYzHtUj4Ks7OCBilHS67kEJxJc2fcJvuQhJ7i1fZh4BB1/wCAjhRhoEmB8BXlD6xQeLcqSk/bvs4wbTf7AejfQpb4+yOW4sn6v00QrSDN52OFuTB0cDnFlNMQEAwaPgynkWafP5ibLerXd0EHzPpgioT70scgAV0WTVSItyAhuxixmp3Zr90g3hx0GfL3knCfHX3OwPOb7LGhqKQYcqG6MewDucHVftCAaUt6xg8tHTci9Zvv4d1mF/XZ8JLw/5IhRw4VxkqSsHWPQMGRNGFttHCCjwje4jEd9PZISK4dSA1TybTCvNek9dfrSLFDhpEXN9zrLHFYsYfHOhegFxdnFr9f8wZPeP1z1agoQXL9tKjrADPD0HmEBxBQtq/ihGRAggDK89BBufApj7IqSayBvS7JA/On22FGtIqKcnMeozNXGFGKeTRlQd7Rb+nBQuubNVx4qNjPrGRU5pZS1qAUNM4viK+8iZE1ZhObMf6hkFYOn8YcJx+PYsW83i6m9XqA/LbBUCKZOYhx101xLwsid1U2lftlwfVbmEyw095UnTLLSM5QDub0gZOpGWZ3YSPg6eteBBwlkiAnmmuT4li37BDxCDOGtCHY6c+LXOELZxTcTkwH7B7ODJxR5RS1+f+3AOekaNGaTBgN/7B6wKq6SG5y/BUrXebfAyyMofXFReLUHImJWxwKF1oVgf69ioN57xvbjbmLmeySlkZaIehrx5AEmMxW6PRzPbyEctOKesDBvlLT4LO7YBqYRLb9V0Ul0U1Gecbd4Uxi","salt":"68nMAFVeqzS5L9zaK3Rfrw==","iv":"z/3ZYNKTiwuDLzW9dfn9Kg==","mac":"Neo3ZresLNiEiM3Zs0F+tg==","kdf":{"algorithm":"Pbkdf2","macAlgorithm":{"algorithm":"Hmac","hashAlgorithm":{"algorithm":"DartSha256"}},"iterations":100000,"bits":256},"cypher":{"algorithm":"AesGcm","secretKeyLength":32}}'; + final decrypted = await TokenEncryption.decrypt(encryptedTokens: encrypted, password: 'password'); + + expect(decrypted.isNotEmpty, true); + expect(decrypted.length, 5); + expect(decrypted.whereType().length, 1); + expect(decrypted.whereType().length, 2); // TOTP and Steam + expect(decrypted.whereType().length, 1); + expect(decrypted.whereType().length, 1); + expect(decrypted.whereType().length, 1); + }); + test('generateQrCodeUri', () { + final tokensList = [ + HOTPToken(id: 'id1', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret1'), + TOTPToken(period: 30, id: 'id2', algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2'), + SteamToken(id: 'id3', secret: 'secret3'), + DayPasswordToken(period: const Duration(hours: 24), id: 'id4', algorithm: Algorithms.SHA512, digits: 10, secret: 'secret4'), + PushToken(serial: 'serial', id: 'id5'), + ]; + const compressedTokensBase64 = [ + 'eyJsYWJlbCI6IiIsImlzc3VlciI6IiIsImlkIjoiaWQxIiwicGluIjpmYWxzZSwiaXNMb2NrZWQiOmZhbHNlLCJpc0hpZGRlbiI6ZmFsc2UsInRva2VuSW1hZ2UiOm51bGwsImZvbGRlcklkIjpudWxsLCJzb3J0SW5kZXgiOm51bGwsIm9yaWdpbiI6bnVsbCwidHlwZSI6IkhPVFAiLCJhbGdvcml0aG0iOiJTSEExIiwiZGlnaXRzIjo2LCJzZWNyZXQiOiJzZWNyZXQxIiwiY291bnRlciI6MH0=', + 'eyJsYWJlbCI6IiIsImlzc3VlciI6IiIsImlkIjoiaWQyIiwicGluIjpmYWxzZSwiaXNMb2NrZWQiOmZhbHNlLCJpc0hpZGRlbiI6ZmFsc2UsInRva2VuSW1hZ2UiOm51bGwsImZvbGRlcklkIjpudWxsLCJzb3J0SW5kZXgiOm51bGwsIm9yaWdpbiI6bnVsbCwidHlwZSI6IlRPVFAiLCJhbGdvcml0aG0iOiJTSEEyNTYiLCJkaWdpdHMiOjgsInNlY3JldCI6InNlY3JldDIiLCJwZXJpb2QiOjMwfQ==', + 'eyJsYWJlbCI6IiIsImlzc3VlciI6IiIsImlkIjoiaWQzIiwicGluIjpmYWxzZSwiaXNMb2NrZWQiOmZhbHNlLCJpc0hpZGRlbiI6ZmFsc2UsInRva2VuSW1hZ2UiOm51bGwsImZvbGRlcklkIjpudWxsLCJzb3J0SW5kZXgiOm51bGwsIm9yaWdpbiI6bnVsbCwidHlwZSI6IlNURUFNIiwic2VjcmV0Ijoic2VjcmV0MyJ9', + 'eyJsYWJlbCI6IiIsImlzc3VlciI6IiIsImlkIjoiaWQ0IiwicGluIjpmYWxzZSwiaXNMb2NrZWQiOmZhbHNlLCJpc0hpZGRlbiI6ZmFsc2UsInRva2VuSW1hZ2UiOm51bGwsImZvbGRlcklkIjpudWxsLCJzb3J0SW5kZXgiOm51bGwsIm9yaWdpbiI6bnVsbCwidHlwZSI6IkRBWVBBU1NXT1JEIiwiYWxnb3JpdGhtIjoiU0hBNTEyIiwiZGlnaXRzIjoxMCwic2VjcmV0Ijoic2VjcmV0NCIsInZpZXdNb2RlIjoiVkFMSURGT1IiLCJwZXJpb2QiOjg2NDAwMDAwMDAwfQ==', + 'eyJsYWJlbCI6IiIsImlzc3VlciI6IiIsImlkIjoiaWQ1IiwicGluIjpmYWxzZSwiaXNMb2NrZWQiOmZhbHNlLCJpc0hpZGRlbiI6ZmFsc2UsInRva2VuSW1hZ2UiOm51bGwsImZvbGRlcklkIjpudWxsLCJzb3J0SW5kZXgiOm51bGwsIm9yaWdpbiI6bnVsbCwidHlwZSI6IlBJUFVTSCIsImV4cGlyYXRpb25EYXRlIjpudWxsLCJzZXJpYWwiOiJzZXJpYWwiLCJmYlRva2VuIjpudWxsLCJzc2xWZXJpZnkiOmZhbHNlLCJlbnJvbGxtZW50Q3JlZGVudGlhbHMiOm51bGwsInVybCI6bnVsbCwiaXNSb2xsZWRPdXQiOmZhbHNlLCJyb2xsb3V0U3RhdGUiOiJyb2xsb3V0Tm90U3RhcnRlZCIsInB1YmxpY1NlcnZlcktleSI6bnVsbCwicHJpdmF0ZVRva2VuS2V5IjpudWxsLCJwdWJsaWNUb2tlbktleSI6bnVsbH0=', + ]; + for (var i = 0; tokensList.length > i; i++) { + final token = tokensList[i]; + final compressed = compressedTokensBase64[i]; + final qrCodeUri = TokenEncryption.generateQrCodeUri(token: token); + final uriString = qrCodeUri.toString(); + expect(uriString.isNotEmpty, true); + expect(uriString, '${PrivacyIDEAAuthenticatorQrProcessor.scheme}://${PrivacyIDEAAuthenticatorQrProcessor.host}?data=$compressed'); + } + }); + test('fromQrCodeUri', () { + final tokensList = [ + HOTPToken(id: 'id1', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret1'), + TOTPToken(period: 30, id: 'id2', algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2'), + SteamToken(id: 'id3', secret: 'secret3'), + DayPasswordToken(period: const Duration(hours: 24), id: 'id4', algorithm: Algorithms.SHA512, digits: 10, secret: 'secret4'), + PushToken(serial: 'serial', id: 'id5'), + ]; + const uriStrings = [ + 'pia://qrbackup?data=eyJsYWJlbCI6IiIsImlzc3VlciI6IiIsImlkIjoiaWQxIiwicGluIjpmYWxzZSwiaXNMb2NrZWQiOmZhbHNlLCJpc0hpZGRlbiI6ZmFsc2UsInRva2VuSW1hZ2UiOm51bGwsImZvbGRlcklkIjpudWxsLCJzb3J0SW5kZXgiOm51bGwsIm9yaWdpbiI6bnVsbCwidHlwZSI6IkhPVFAiLCJhbGdvcml0aG0iOiJTSEExIiwiZGlnaXRzIjo2LCJzZWNyZXQiOiJzZWNyZXQxIiwiY291bnRlciI6MH0=', + 'pia://qrbackup?data=eyJsYWJlbCI6IiIsImlzc3VlciI6IiIsImlkIjoiaWQyIiwicGluIjpmYWxzZSwiaXNMb2NrZWQiOmZhbHNlLCJpc0hpZGRlbiI6ZmFsc2UsInRva2VuSW1hZ2UiOm51bGwsImZvbGRlcklkIjpudWxsLCJzb3J0SW5kZXgiOm51bGwsIm9yaWdpbiI6bnVsbCwidHlwZSI6IlRPVFAiLCJhbGdvcml0aG0iOiJTSEEyNTYiLCJkaWdpdHMiOjgsInNlY3JldCI6InNlY3JldDIiLCJwZXJpb2QiOjMwfQ==', + 'pia://qrbackup?data=eyJsYWJlbCI6IiIsImlzc3VlciI6IiIsImlkIjoiaWQzIiwicGluIjpmYWxzZSwiaXNMb2NrZWQiOmZhbHNlLCJpc0hpZGRlbiI6ZmFsc2UsInRva2VuSW1hZ2UiOm51bGwsImZvbGRlcklkIjpudWxsLCJzb3J0SW5kZXgiOm51bGwsIm9yaWdpbiI6bnVsbCwidHlwZSI6IlNURUFNIiwic2VjcmV0Ijoic2VjcmV0MyJ9', + 'pia://qrbackup?data=eyJsYWJlbCI6IiIsImlzc3VlciI6IiIsImlkIjoiaWQ0IiwicGluIjpmYWxzZSwiaXNMb2NrZWQiOmZhbHNlLCJpc0hpZGRlbiI6ZmFsc2UsInRva2VuSW1hZ2UiOm51bGwsImZvbGRlcklkIjpudWxsLCJzb3J0SW5kZXgiOm51bGwsIm9yaWdpbiI6bnVsbCwidHlwZSI6IkRBWVBBU1NXT1JEIiwiYWxnb3JpdGhtIjoiU0hBNTEyIiwiZGlnaXRzIjoxMCwic2VjcmV0Ijoic2VjcmV0NCIsInZpZXdNb2RlIjoiVkFMSURGT1IiLCJwZXJpb2QiOjg2NDAwMDAwMDAwfQ==', + 'pia://qrbackup?data=eyJsYWJlbCI6IiIsImlzc3VlciI6IiIsImlkIjoiaWQ1IiwicGluIjpmYWxzZSwiaXNMb2NrZWQiOmZhbHNlLCJpc0hpZGRlbiI6ZmFsc2UsInRva2VuSW1hZ2UiOm51bGwsImZvbGRlcklkIjpudWxsLCJzb3J0SW5kZXgiOm51bGwsIm9yaWdpbiI6bnVsbCwidHlwZSI6IlBJUFVTSCIsImV4cGlyYXRpb25EYXRlIjpudWxsLCJzZXJpYWwiOiJzZXJpYWwiLCJmYlRva2VuIjpudWxsLCJzc2xWZXJpZnkiOmZhbHNlLCJlbnJvbGxtZW50Q3JlZGVudGlhbHMiOm51bGwsInVybCI6bnVsbCwiaXNSb2xsZWRPdXQiOmZhbHNlLCJyb2xsb3V0U3RhdGUiOiJyb2xsb3V0Tm90U3RhcnRlZCIsInB1YmxpY1NlcnZlcktleSI6bnVsbCwicHJpdmF0ZVRva2VuS2V5IjpudWxsLCJwdWJsaWNUb2tlbktleSI6bnVsbH0=', + ]; + for (var i = 0; uriStrings.length > i; i++) { + final uri = Uri.parse(uriStrings[i]); + final token = tokensList[i]; + final decrypted = TokenEncryption.fromQrCodeUri(uri); + expect(decrypted, token); + } + }); + }); +} diff --git a/test/unit_test/model/encryption/uint_8_buffer_test.dart b/test/unit_test/model/encryption/uint_8_buffer_test.dart new file mode 100644 index 000000000..b6c321c8b --- /dev/null +++ b/test/unit_test/model/encryption/uint_8_buffer_test.dart @@ -0,0 +1,108 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/encryption/uint_8_buffer.dart'; + +void main() { + _testUint8Buffer(); +} + +void _testUint8Buffer() { + group('Uint 8 Buffer', () { + test('fromList', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + expect(buffer.data, equals(Uint8List.fromList(list))); + }); + + test('writeBytes', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + final list2 = [6, 7, 8, 9, 10]; + buffer.writeBytes(Uint8List.fromList(list2)); + expect(buffer.data, equals(Uint8List.fromList([...list, ...list2]))); + }); + + test('readBytes', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + final bytes = buffer.readBytes(3); + expect(bytes, equals(Uint8List.fromList([1, 2, 3]))); + expect(buffer.currentPos, equals(3)); + }); + + test('readBytes more than available', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + final bytes = buffer.readBytes(10); + expect(bytes, equals(Uint8List.fromList([1, 2, 3, 4, 5]))); + expect(buffer.currentPos, equals(5)); + }); + + test('readBytesToEnd with left', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + buffer.readBytes(1); + final bytes = buffer.readBytesToEnd(left: 2); + expect(bytes, equals(Uint8List.fromList([2, 3]))); + expect(buffer.currentPos, equals(3)); + }); + + test('readBytesToEnd without left', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + buffer.readBytes(1); + final bytes = buffer.readBytesToEnd(); + expect(bytes, equals(Uint8List.fromList([2, 3, 4, 5]))); + expect(buffer.currentPos, equals(5)); + }); + + test('moveCurrentPos', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + buffer.moveCurrentPos(3); + expect(buffer.currentPos, equals(3)); + final bytes = buffer.readBytesToEnd(); + expect(bytes, equals(Uint8List.fromList([4, 5]))); + }); + + test('moveCurrentPos to out of bounds', () { + final list = [1, 2, 3, 4, 5]; + final buffer = Uint8Buffer.fromList(list); + buffer.moveCurrentPos(10); + expect(buffer.currentPos, equals(5)); + final bytes = buffer.readBytesToEnd(); + expect(bytes, equals(Uint8List.fromList([]))); + }); + }); +} +/* + factory Uint8Buffer.fromList(List list) { + return Uint8Buffer(data: Uint8List.fromList(list)); + } + + /// Writes [bytes] to the buffer + void writeBytes(Uint8List bytes) { + data = Uint8List.fromList([...data, ...bytes]); + } + + /// Reads [length] bytes from the current position + /// and moves the position forward + Uint8List readBytes(int length) { + final bytes = data.sublist(currentPos, currentPos + length); + currentPos += length; + return bytes; + } + + /// Reads all bytes from the current position to the end of the buffer + /// If [left] is provided, it will leave [left] bytes at the end + /// and return the rest + Uint8List readBytesToEnd({int left = 0}) { + final bytes = data.sublist(currentPos, data.length - left); + currentPos = data.length - left; + return bytes; + } + + /// Moves the current position to [pos] + void moveCurrentPos(int pos) => currentPos = pos; + */ \ No newline at end of file diff --git a/test/unit_test/model/extensions/enum_extension_test.dart b/test/unit_test/model/extensions/enum_extension_test.dart new file mode 100644 index 000000000..322a496e9 --- /dev/null +++ b/test/unit_test/model/extensions/enum_extension_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/extensions/enum_extension.dart'; + +void main() { + _testEnumExtension(); +} + +enum _TestEnum { + entryOne, + entryTwo, + entryThree, +} + +void _testEnumExtension() { + group('Enum Extension', () { + group('isName', () { + test('caseSensitive', () { + expect(_TestEnum.entryOne.isName('entryOne'), true); + expect(_TestEnum.entryOne.isName('entryone'), false); + expect(_TestEnum.entryOne.isName('entryTwo'), false); + expect(_TestEnum.entryTwo.isName('entryTwo'), true); + expect(_TestEnum.entryTwo.isName('entrytwo'), false); + expect(_TestEnum.entryTwo.isName('entryThree'), false); + expect(_TestEnum.entryThree.isName('entryThree'), true); + expect(_TestEnum.entryThree.isName('entrythree'), false); + }); + test('caseInsensitive', () { + expect(_TestEnum.entryOne.isName('entryone', caseSensitive: false), true); + expect(_TestEnum.entryOne.isName('ENTRYONE', caseSensitive: false), true); + expect(_TestEnum.entryOne.isName('entryTwo', caseSensitive: false), false); + expect(_TestEnum.entryTwo.isName('entrytwo', caseSensitive: false), true); + expect(_TestEnum.entryTwo.isName('ENTRYTWO', caseSensitive: false), true); + expect(_TestEnum.entryTwo.isName('entryThree', caseSensitive: false), false); + expect(_TestEnum.entryThree.isName('entrythree', caseSensitive: false), true); + expect(_TestEnum.entryThree.isName('ENTRYTHREE', caseSensitive: false), true); + }); + }); + }); +} diff --git a/test/unit_test/model/extensions/enums/algorithms_extension_test.dart b/test/unit_test/model/extensions/enums/algorithms_extension_test.dart new file mode 100644 index 000000000..187987702 --- /dev/null +++ b/test/unit_test/model/extensions/enums/algorithms_extension_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/algorithms_extension.dart'; + +void main() { + _testAlgorithmsExtension(); +} + +void _testAlgorithmsExtension() { + group('Algorithms Extension', () { + group('generateTOTPCodeString', () {}); + group('generateHOTPCodeString', () { + group('different couters 6 digits', () { + test('OTP for counter == 0', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + expect(otpValue, equals('328482')); + }); + + test('OTP for counter == 1', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 1, length: 6); + expect(otpValue, equals('812658')); + }); + + test('OTP for counter == 2', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 2, length: 6); + expect(otpValue, equals('073348')); + }); + + test('OTP for counter == 8', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 8, length: 6); + expect(otpValue, equals('985814')); + }); + }); + group('different couters 8 digits', () { + test('OTP for counter == 0', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + expect(otpValue, equals('35328482')); + }); + + test('OTP for counter == 1', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 1, length: 8); + expect(otpValue, equals('30812658')); + }); + + test('OTP for counter == 2', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 2, length: 8); + expect(otpValue, equals('41073348')); + }); + + test('OTP for counter == 8', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 8, length: 8); + expect(otpValue, equals('12985814')); + }); + }); + group('different algorithms 6 digits', () { + test('OTP for sha1', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + expect(otpValue, equals('328482')); + }); + + test('OTP for sha256', () { + final otpValue = Algorithms.SHA256.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + expect(otpValue, equals('356306')); + }); + + test('OTP for sha512', () { + final otpValue = Algorithms.SHA512.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + expect(otpValue, equals('674061')); + }); + }); + group('different algorithms 8 digits', () { + test('OTP for sha1', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + expect(otpValue, equals('35328482')); + }); + + test('OTP for sha256', () { + final otpValue = Algorithms.SHA256.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + expect(otpValue, equals('03356306')); + }); + + test('OTP for sha512', () { + final otpValue = Algorithms.SHA512.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + expect(otpValue, equals('66674061')); + }); + }); + group('is not google', () { + test('OTP for sha1', () { + final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6, isGoogle: false); + expect(otpValue, equals('814628')); + }); + + test('OTP for sha256', () { + final otpValue = Algorithms.SHA256.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6, isGoogle: false); + expect(otpValue, equals('059019')); + }); + + test('OTP for sha512', () { + final otpValue = Algorithms.SHA512.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6, isGoogle: false); + expect(otpValue, equals('377469')); + }); + }); + }); + }); +} diff --git a/test/unit_test/model/extensions/enums/encodings_extension_test.dart b/test/unit_test/model/extensions/enums/encodings_extension_test.dart new file mode 100644 index 000000000..c5c53aa43 --- /dev/null +++ b/test/unit_test/model/extensions/enums/encodings_extension_test.dart @@ -0,0 +1,81 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; + +void main() { + _testEncodingsExtension(); +} + +void _testEncodingsExtension() { + group('Encodings Extension', () { + group('encode', () { + group('valid', () { + test('base32', () => expect(Encodings.base32.encode(Uint8List.fromList([153, 37, 57])), equals('TESTS==='))); + test('hex', () => expect(Encodings.hex.encode(Uint8List.fromList([153, 37, 57])), equals('992539'))); + test('none', () => expect(Encodings.none.encode(Uint8List.fromList([116, 101, 115, 116, 115])), equals('tests'))); + }); + + group('invalid', () { + test('none', () => expect(() => Encodings.none.encode(Uint8List.fromList([153, 37, 57])), throwsException)); + }); + }); + group('encodeStringTo', () { + test('base32 to hex', () => expect(Encodings.base32.encodeStringTo(Encodings.hex, 'TESTS==='), equals('992539'))); + test('hex to base32', () => expect(Encodings.hex.encodeStringTo(Encodings.base32, '992539'), equals('TESTS==='))); + }); + + group('decode', () { + group('valid', () { + test('base32', () => expect(Encodings.base32.decode('TESTS==='), equals(Uint8List.fromList([153, 37, 57])))); + test('hex', () => expect(Encodings.hex.decode('992539'), equals(Uint8List.fromList([153, 37, 57])))); + test('none', () => expect(Encodings.none.decode('tests'), equals(Uint8List.fromList([116, 101, 115, 116, 115])))); + }); + + group('invalid', () { + test('base32', () => expect(() => Encodings.base32.decode('TESTS+++'), throwsException)); + test('hex', () => expect(() => Encodings.hex.decode('abcdefg'), throwsException)); + // Every utf8 string has a valid binary representation + }); + }); + + group('isValidEncoding', () { + test('base32', () => expect(Encodings.base32.isValidEncoding('TESTS==='), isTrue)); + test('hex', () => expect(Encodings.hex.isValidEncoding('992539'), isTrue)); + test('none', () => expect(Encodings.none.isValidEncoding('tests'), isTrue)); + }); + + group('isInvalidEncoding', () { + test('base32', () => expect(Encodings.base32.isInvalidEncoding('TESTS==='), isFalse)); + test('hex', () => expect(Encodings.hex.isInvalidEncoding('992539'), isFalse)); + // Every utf8 string has a valid binary representation + }); + + group('tryDecode', () { + group('valid', () { + test('base32', () => expect(Encodings.base32.tryDecode('TESTS==='), equals(Uint8List.fromList([153, 37, 57])))); + test('hex', () => expect(Encodings.hex.tryDecode('992539'), equals(Uint8List.fromList([153, 37, 57])))); + test('none', () => expect(Encodings.none.tryDecode('tests'), equals(Uint8List.fromList([116, 101, 115, 116, 115])))); + }); + + group('invalid', () { + test('base32', () => expect(Encodings.base32.tryDecode('TESTS+++'), isNull)); + test('hex', () => expect(Encodings.hex.tryDecode('abcdefg'), isNull)); + // Every utf8 string has a valid binary representation + }); + }); + + group('tryEncode', () { + group('valid', () { + test('base32', () => expect(Encodings.base32.tryEncode(Uint8List.fromList([153, 37, 57])), equals('TESTS==='))); + test('hex', () => expect(Encodings.hex.tryEncode(Uint8List.fromList([153, 37, 57])), equals('992539'))); + test('none', () => expect(Encodings.none.tryEncode(Uint8List.fromList([116, 101, 115, 116, 115])), equals('tests'))); + }); + group('invalid', () { + // Every binary data can be encoded to base32 and hex + test('none', () => expect(Encodings.none.tryEncode(Uint8List.fromList([153, 37, 57])), isNull)); + }); + }); + }); +} diff --git a/test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart b/test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart new file mode 100644 index 000000000..dd2fa493f --- /dev/null +++ b/test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/push_token_rollout_state.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/push_token_rollout_state_extension.dart'; + +void main() { + _testPushTokenRolloutstateExtension(); +} + +void _testPushTokenRolloutstateExtension() { + group('Push-Token Rolloutstate Extension', () { + test('rollOutInProgress', () { + expect(PushTokenRollOutState.rolloutNotStarted.rollOutInProgress, false); + expect(PushTokenRollOutState.generatingRSAKeyPair.rollOutInProgress, true); + expect(PushTokenRollOutState.generatingRSAKeyPairFailed.rollOutInProgress, false); + expect(PushTokenRollOutState.sendRSAPublicKey.rollOutInProgress, true); + expect(PushTokenRollOutState.sendRSAPublicKeyFailed.rollOutInProgress, false); + expect(PushTokenRollOutState.parsingResponse.rollOutInProgress, true); + expect(PushTokenRollOutState.parsingResponseFailed.rollOutInProgress, false); + expect(PushTokenRollOutState.rolloutComplete.rollOutInProgress, false); + }); + test('getFailed', () { + expect(PushTokenRollOutState.rolloutNotStarted.getFailed(), PushTokenRollOutState.rolloutNotStarted); + expect(PushTokenRollOutState.generatingRSAKeyPair.getFailed(), PushTokenRollOutState.generatingRSAKeyPairFailed); + expect(PushTokenRollOutState.generatingRSAKeyPairFailed.getFailed(), PushTokenRollOutState.generatingRSAKeyPairFailed); + expect(PushTokenRollOutState.sendRSAPublicKey.getFailed(), PushTokenRollOutState.sendRSAPublicKeyFailed); + expect(PushTokenRollOutState.sendRSAPublicKeyFailed.getFailed(), PushTokenRollOutState.sendRSAPublicKeyFailed); + expect(PushTokenRollOutState.parsingResponse.getFailed(), PushTokenRollOutState.parsingResponseFailed); + expect(PushTokenRollOutState.parsingResponseFailed.getFailed(), PushTokenRollOutState.parsingResponseFailed); + expect(PushTokenRollOutState.rolloutComplete.getFailed(), PushTokenRollOutState.rolloutComplete); + }); + }); +} diff --git a/test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart b/test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart new file mode 100644 index 000000000..567c40e1d --- /dev/null +++ b/test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/token_import/token_origin_data.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; + +void main() { + _testTokenOriginSourceTypeExtension(); +} + +void _testTokenOriginSourceTypeExtension() { + group('Token Origin Source Type Extension', () { + test('toTokenOrigin', () { + final TokenOriginData tokenOriginDataMatch = TokenOriginData( + source: TokenOriginSourceType.qrScan, + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), + ); + final TokenOriginData tokenOriginData = TokenOriginSourceType.qrScan.toTokenOrigin( + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), + ); + expect(tokenOriginData.source, tokenOriginDataMatch.source); + expect(tokenOriginData.data, tokenOriginDataMatch.data); + expect(tokenOriginData.appName, tokenOriginDataMatch.appName); + expect(tokenOriginData.isPrivacyIdeaToken, tokenOriginDataMatch.isPrivacyIdeaToken); + expect(tokenOriginData.createdAt, tokenOriginDataMatch.createdAt); + expect(tokenOriginData, tokenOriginDataMatch); + }); + test('addOriginToToken', () { + final token = HOTPToken(id: 'id', algorithm: Algorithms.SHA512, digits: 6, secret: 'secret'); + final TokenOriginData tokenOriginDataMatch = TokenOriginData( + source: TokenOriginSourceType.qrScan, + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), + ); + final tokenMatch = token.copyWith(origin: tokenOriginDataMatch); + final tokenWithOrigin = TokenOriginSourceType.qrScan.addOriginToToken( + token: token, + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), + ); + expect(tokenWithOrigin.origin!.source, tokenOriginDataMatch.source); + expect(tokenWithOrigin.origin!.data, tokenOriginDataMatch.data); + expect(tokenWithOrigin.origin!.appName, tokenOriginDataMatch.appName); + expect(tokenWithOrigin.origin!.isPrivacyIdeaToken, tokenOriginDataMatch.isPrivacyIdeaToken); + expect(tokenWithOrigin.origin!.createdAt, tokenOriginDataMatch.createdAt); + expect(tokenWithOrigin, tokenMatch); + }); + }); +} diff --git a/test/unit_test/model/extensions/int_extension_test.dart b/test/unit_test/model/extensions/int_extension_test.dart new file mode 100644 index 000000000..1cea777c8 --- /dev/null +++ b/test/unit_test/model/extensions/int_extension_test.dart @@ -0,0 +1,97 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/extensions/int_extension.dart'; + +void main() { + _testIntExtension(); +} + +void _testIntExtension() { + group('int extension', () { + group('bytes', () { + test('min int value', () => expect((-0x8000000000000000).bytes, [128, 0, 0, 0, 0, 0, 0, 0])); + test('zero', () => expect(0.bytes, [0, 0, 0, 0, 0, 0, 0, 0])); + test('max int value', () => expect(0x7FFFFFFFFFFFFFFF.bytes, [127, 255, 255, 255, 255, 255, 255, 255])); + test('20 different int values', () { + expect(8254763140651989312.bytes, [114, 142, 207, 87, 63, 140, 185, 64]); + expect(6867929122700968103.bytes, [95, 79, 201, 38, 53, 38, 192, 167]); + expect(6658070822668124012.bytes, [92, 102, 56, 35, 34, 134, 191, 108]); + expect(6233195836444142436.bytes, [86, 128, 194, 150, 158, 178, 107, 100]); + expect(5665252064165200114.bytes, [78, 159, 4, 188, 143, 157, 128, 242]); + expect(3836812696023088046.bytes, [53, 63, 24, 209, 152, 42, 59, 174]); + expect(4217815205603023728.bytes, [58, 136, 176, 149, 34, 53, 155, 112]); + expect(1859730558376856620.bytes, [25, 207, 23, 22, 237, 253, 204, 44]); + expect((-1341891893474570224).bytes, [237, 96, 164, 110, 186, 120, 208, 16]); + expect((-1947790576164582988).bytes, [228, 248, 14, 202, 114, 219, 73, 180]); + expect((-5204853149876150168).bytes, [183, 196, 165, 162, 253, 143, 32, 104]); + expect((-5765512478999408848).bytes, [175, 252, 200, 242, 133, 40, 143, 48]); + expect((-6273311771369426144).bytes, [168, 240, 184, 46, 110, 63, 179, 32]); + expect((-6384342681465787276).bytes, [167, 102, 66, 40, 42, 221, 236, 116]); + expect((-6805707171905842232).bytes, [161, 141, 69, 98, 165, 57, 83, 200]); + expect((-7708924412950309696).bytes, [149, 4, 102, 23, 13, 194, 148, 192]); + expect((-7731444997339132318).bytes, [148, 180, 99, 188, 229, 33, 78, 98]); + expect((-7855692611255695686).bytes, [146, 250, 249, 48, 249, 113, 134, 186]); + expect((-8557951589827587072).bytes, [137, 60, 12, 94, 251, 86, 84, 0]); + expect((-9153687162235632943).bytes, [128, 247, 146, 6, 53, 228, 66, 209]); + }); + }); + test('digits', () { + expect(0.digits, [0]); + expect(1.digits, [1]); + expect(9.digits, [9]); + expect(10.digits, [0, 1]); + expect(11.digits, [1, 1]); + expect(99.digits, [9, 9]); + expect(100.digits, [0, 0, 1]); + expect(101.digits, [1, 0, 1]); + expect(999.digits, [9, 9, 9]); + expect(1000.digits, [0, 0, 0, 1]); + expect(1001.digits, [1, 0, 0, 1]); + expect(9999.digits, [9, 9, 9, 9]); + expect(10000.digits, [0, 0, 0, 0, 1]); + expect(10001.digits, [1, 0, 0, 0, 1]); + expect(99999.digits, [9, 9, 9, 9, 9]); + expect(100000.digits, [0, 0, 0, 0, 0, 1]); + expect(100001.digits, [1, 0, 0, 0, 0, 1]); + expect(999999.digits, [9, 9, 9, 9, 9, 9]); + expect(1000000.digits, [0, 0, 0, 0, 0, 0, 1]); + expect(1000001.digits, [1, 0, 0, 0, 0, 0, 1]); + expect(9999999.digits, [9, 9, 9, 9, 9, 9, 9]); + expect(10000000.digits, [0, 0, 0, 0, 0, 0, 0, 1]); + expect(10000001.digits, [1, 0, 0, 0, 0, 0, 0, 1]); + expect(99999999.digits, [9, 9, 9, 9, 9, 9, 9, 9]); + }); + test('pow', () { + expect(0.pow(0), 1); + expect(0.pow(1), 0); + expect(1.pow(0), 1); + expect(1.pow(1), 1); + expect(2.pow(0), 1); + expect(2.pow(1), 2); + expect(2.pow(2), 4); + expect(3.pow(1), 3); + expect(3.pow(2), 9); + expect(3.pow(3), 27); + expect(4.pow(2), 16); + expect(4.pow(3), 64); + expect(4.pow(4), 256); + expect(5.pow(3), 125); + expect(5.pow(4), 625); + expect(5.pow(5), 3125); + expect(6.pow(4), 1296); + expect(6.pow(5), 7776); + expect(6.pow(6), 46656); + expect(7.pow(5), 16807); + expect(7.pow(6), 117649); + expect(7.pow(7), 823543); + expect(8.pow(6), 262144); + expect(8.pow(7), 2097152); + expect(8.pow(8), 16777216); + expect(9.pow(7), 4782969); + expect(9.pow(8), 43046721); + expect(9.pow(9), 387420489); + expect(10.pow(8), 100000000); + expect(10.pow(9), 1000000000); + expect(10.pow(10), 10000000000); + }); + }); +} diff --git a/test/unit_test/model/extensions/sortable_list_test.dart b/test/unit_test/model/extensions/sortable_list_test.dart new file mode 100644 index 000000000..503e4e49f --- /dev/null +++ b/test/unit_test/model/extensions/sortable_list_test.dart @@ -0,0 +1,194 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/extensions/sortable_list.dart'; +import 'package:privacyidea_authenticator/model/mixins/sortable_mixin.dart'; + +void main() { + _testSortableList(); +} + +class _SortableTestClass with SortableMixin { + @override + int? sortIndex; + String name; + _SortableTestClass({this.sortIndex, required this.name}); + + @override + SortableMixin copyWith({int? sortIndex, String? name}) => _SortableTestClass( + sortIndex: sortIndex ?? this.sortIndex, + name: name ?? this.name, + ); + @override + operator ==(Object other) => other is _SortableTestClass && other.name == name; + @override + int get hashCode => name.hashCode; + + @override + String toString() => "_SortableTestClass(sortIndex: $sortIndex, name: '$name')"; +} + +void _testSortableList() { + group('Sortable List', () { + group('sorted', () { + // Arrange + test('1-5', () { + final list = [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + ]; + // Act + final result = list.sorted; + // Assert + expect(result, [ + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 5, name: '5'), + ]); + }); + test('with gaps', () { + final list = [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 12, name: '12'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 8, name: '8'), + ]; + // Act + final result = list.sorted; + // Assert + expect(result, [ + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: 8, name: '8'), + _SortableTestClass(sortIndex: 12, name: '12'), + _SortableTestClass(sortIndex: null, name: 'null'), + ]); + }); + test('1-5 and multible nulls', () { + final list = [ + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + ]; + // Act + final result = list.sorted; + // Assert + expect(result, [ + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: null, name: 'null'), + ]); + }); + }); + + group('fill Null Indices', () { + test('1-5', () { + final result = [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + ].fillNullIndices(); + expect(result, [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 5, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + ]); + }); + test('with gaps', () { + final result = [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 12, name: '12'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 8, name: '8'), + ].fillNullIndices(); + expect(result, [ + _SortableTestClass(sortIndex: 3, name: '3'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 12, name: '12'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: 13, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 8, name: '8'), + ]); + }); + }); + group('move between', () { + test('1-5', () { + final movedItem = _SortableTestClass(sortIndex: 2, name: '2'); + final moveBefore = _SortableTestClass(sortIndex: 3, name: '3'); + final list = [ + moveBefore, + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 5, name: '5'), + movedItem, + _SortableTestClass(sortIndex: 4, name: '4'), + ]; + // Act + final result = list.moveBetween(moveAfter: null, movedItem: movedItem, moveBefore: list[1]); + // Assert + expect(result, [ + _SortableTestClass(sortIndex: 0, name: '2'), + _SortableTestClass(sortIndex: 1, name: '1'), + _SortableTestClass(sortIndex: 2, name: '3'), + _SortableTestClass(sortIndex: 3, name: '4'), + _SortableTestClass(sortIndex: 4, name: '5'), + ]); + }); + test('with gaps', () { + final moveAfter = _SortableTestClass(sortIndex: 3, name: '3'); + final movedItem = _SortableTestClass(sortIndex: 12, name: '12'); + final moveBefore = _SortableTestClass(sortIndex: 5, name: '5'); + final list = [ + moveAfter, + _SortableTestClass(sortIndex: 1, name: '1'), + movedItem, + moveBefore, + _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(sortIndex: 2, name: '2'), + _SortableTestClass(sortIndex: 4, name: '4'), + _SortableTestClass(sortIndex: 8, name: '8'), + ]; + // Act + final result = list.moveBetween(moveAfter: moveAfter, movedItem: movedItem, moveBefore: moveBefore); + // Assert + expect(result, [ + _SortableTestClass(sortIndex: 0, name: '1'), + _SortableTestClass(sortIndex: 1, name: '2'), + _SortableTestClass(sortIndex: 2, name: '3'), + _SortableTestClass(sortIndex: 3, name: '4'), + _SortableTestClass(sortIndex: 4, name: '12'), + _SortableTestClass(sortIndex: 5, name: '5'), + _SortableTestClass(sortIndex: 6, name: '8'), + _SortableTestClass(sortIndex: 7, name: 'null'), + ]); + }); + }); + }); +} diff --git a/test/unit_test/model/mixins/sortable_mixin_test.dart b/test/unit_test/model/mixins/sortable_mixin_test.dart new file mode 100644 index 000000000..83861ab47 --- /dev/null +++ b/test/unit_test/model/mixins/sortable_mixin_test.dart @@ -0,0 +1,91 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/mixins/sortable_mixin.dart'; + +void main() { + _testSortableMixin(); +} + +class _SortableTestClass with SortableMixin { + @override + int? sortIndex; + String name; + _SortableTestClass({this.sortIndex, required this.name}); + + @override + SortableMixin copyWith({int? sortIndex, String? name}) => _SortableTestClass( + sortIndex: sortIndex ?? this.sortIndex, + name: name ?? this.name, + ); + @override + operator ==(Object other) => other is _SortableTestClass && other.name == name; + @override + int get hashCode => name.hashCode; + + @override + String toString() => "_SortableTestClass(sortIndex: $sortIndex, name: '$name')"; +} + +void _testSortableMixin() { + group('SortableMixin', () { + group('compareTo equal', () { + test('number', () { + // Arrange + final a = _SortableTestClass(sortIndex: 1, name: '1'); + final b = _SortableTestClass(sortIndex: 1, name: '1'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, 0); + }); + test('null', () { + // Arrange + final a = _SortableTestClass(sortIndex: null, name: 'null'); + final b = _SortableTestClass(sortIndex: null, name: 'null'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, 0); + }); + }); + group('compareTo a < b', () { + test('a < b', () { + // Arrange + final a = _SortableTestClass(sortIndex: 1, name: '1'); + final b = _SortableTestClass(sortIndex: 2, name: '2'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, -1); + }); + test('a = 1, b = null', () { + // Arrange + final a = _SortableTestClass(sortIndex: 1, name: '1'); + final b = _SortableTestClass(sortIndex: null, name: 'null'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, -1); + }); + }); + group('compareTo a > b', () { + test('a > b', () { + // Arrange + final a = _SortableTestClass(sortIndex: 2, name: '2'); + final b = _SortableTestClass(sortIndex: 1, name: '1'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, 1); + }); + test('a = null, b = 1', () { + // Arrange + final a = _SortableTestClass(sortIndex: null, name: 'null'); + final b = _SortableTestClass(sortIndex: 1, name: '1'); + // Act + final result = a.compareTo(b); + // Assert + expect(result, 1); + }); + }); + }); +} diff --git a/test/unit_test/model/processor_result_test.dart b/test/unit_test/model/processor_result_test.dart new file mode 100644 index 000000000..b510c8f99 --- /dev/null +++ b/test/unit_test/model/processor_result_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; + +void main() { + _testProcessorResult(); +} + +void _testProcessorResult() { + group('Processor Result', () { + group('constructors', () { + test('success', () { + const result = ProcessorResultSuccess('data'); + expect(result.resultData, 'data'); + }); + test('error', () { + const result = ProcessorResultFailed('error'); + expect(result, isA()); + expect(result.message, 'error'); + }); + }); + group('factories', () { + test('success', () { + final result = ProcessorResult.success('data'); + expect(result, isA()); + expect((result as ProcessorResultSuccess).resultData, 'data'); + }); + test('error', () { + final result = ProcessorResult.failed('error'); + expect(result, isA()); + expect((result as ProcessorResultFailed).message, 'error'); + }); + }); + }); +} diff --git a/test/unit_test/model/push_request_test.dart b/test/unit_test/model/push_request_test.dart new file mode 100644 index 000000000..91990c500 --- /dev/null +++ b/test/unit_test/model/push_request_test.dart @@ -0,0 +1,287 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/push_request.dart'; +import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; +import 'package:privacyidea_authenticator/utils/identifiers.dart'; + +void main() { + _testPushRequest(); +} + +void _testPushRequest() { + group('Push Request', () { + group('creation', () { + test('constructor', () { + // Arrange + final request = PushRequest( + title: 'title', + question: 'question', + uri: Uri.parse('https://example.com'), + nonce: 'nonce', + sslVerify: true, + id: 1, + expirationDate: DateTime.now(), + ); + // Assert + expect(request.title, 'title'); + expect(request.question, 'question'); + expect(request.uri, Uri.parse('https://example.com')); + expect(request.nonce, 'nonce'); + expect(request.sslVerify, true); + expect(request.id, 1); + expect(request.expirationDate, isA()); + expect(request.serial, ''); + expect(request.signature, ''); + expect(request.accepted, null); + }); + test('copyWith', () { + final dateTimeAfter = DateTime.now().add(const Duration(days: 1)); + // Arrange + final request = PushRequest( + title: 'title', + question: 'question', + uri: Uri.parse('https://example.com'), + nonce: 'nonce', + sslVerify: true, + id: 1, + expirationDate: DateTime.now(), + ); + // Act + final copy = request.copyWith( + title: 'new title', + question: 'new question', + uri: Uri.parse('https://new.example.com'), + nonce: 'new nonce', + sslVerify: false, + id: 2, + expirationDate: dateTimeAfter, + serial: 'serial', + signature: 'signature', + accepted: true, + ); + // Assert + expect(copy.title, 'new title'); + expect(copy.question, 'new question'); + expect(copy.uri, Uri.parse('https://new.example.com')); + expect(copy.nonce, 'new nonce'); + expect(copy.sslVerify, false); + expect(copy.id, 2); + expect(copy.expirationDate, equals(dateTimeAfter)); + expect(copy.serial, 'serial'); + expect(copy.signature, 'signature'); + expect(copy.accepted, true); + }); + test('fromMessageData', () { + // Arrange + final data = { + PUSH_REQUEST_TITLE: 'title', + PUSH_REQUEST_QUESTION: 'question', + PUSH_REQUEST_URL: 'https://example.com', + PUSH_REQUEST_NONCE: 'nonce', + PUSH_REQUEST_SSL_VERIFY: '1', + PUSH_REQUEST_SERIAL: 'serial', + PUSH_REQUEST_SIGNATURE: 'signature', + }; + // Act + final request = PushRequest.fromMessageData(data); + // Assert + expect(request.title, 'title'); + expect(request.question, 'question'); + expect(request.uri, Uri.parse('https://example.com')); + expect(request.nonce, 'nonce'); + expect(request.sslVerify, true); + expect(request.id, 'nonce'.hashCode); + expect(request.serial, 'serial'); + expect(request.signature, 'signature'); + }); + }); + group('serilization', () { + test('toJson', () { + // Arrange + final request = PushRequest( + title: 'title', + question: 'question', + uri: Uri.parse('https://example.com'), + nonce: 'nonce', + sslVerify: true, + id: 1, + expirationDate: DateTime.now(), + serial: 'serial', + signature: 'signature', + accepted: true, + ); + // Act + final json = request.toJson(); + // Assert + expect(json['title'], 'title'); + expect(json['question'], 'question'); + expect(json['uri'], 'https://example.com'); + expect(json['nonce'], 'nonce'); + expect(json['sslVerify'], true); + expect(json['id'], 1); + expect(json['expirationDate'], isA()); + expect(json['serial'], 'serial'); + expect(json['signature'], 'signature'); + expect(json['accepted'], true); + }); + + test('fromJson', () { + // Arrange + final json = { + 'title': 'title', + 'question': 'question', + 'uri': 'https://example.com', + 'nonce': 'nonce', + 'sslVerify': true, + 'id': 1, + 'expirationDate': DateTime.now().toIso8601String(), + 'serial': 'serial', + 'signature': 'signature', + 'accepted': true, + }; + // Act + final request = PushRequest.fromJson(json); + // Assert + expect(request.title, 'title'); + expect(request.question, 'question'); + expect(request.uri, Uri.parse('https://example.com')); + expect(request.nonce, 'nonce'); + expect(request.sslVerify, true); + expect(request.id, 1); + expect(request.expirationDate, isA()); + expect(request.serial, 'serial'); + expect(request.signature, 'signature'); + expect(request.accepted, true); + }); + }); + + test('verifyData', () { + // Arrange + final data = { + PUSH_REQUEST_TITLE: 'title', + PUSH_REQUEST_QUESTION: 'question', + PUSH_REQUEST_URL: 'https://example.com', + PUSH_REQUEST_NONCE: 'nonce', + PUSH_REQUEST_SSL_VERIFY: '1', + PUSH_REQUEST_SERIAL: 'serial', + PUSH_REQUEST_SIGNATURE: 'signature', + }; + // Assert + expect(() => PushRequest.verifyData(data), returnsNormally); + }); + + test('verifySignature true', () async { + // Arrange + final token = PushToken.fromJson(const { + "label": "PIPU00064CF0", + "issuer": "privacyIDEA", + "id": "94a40d5a-1dba-4985-95ce-5ce9bb36d32a", + "pin": false, + "isLocked": false, + "isHidden": false, + "tokenImage": "", + "folderId": null, + "sortIndex": 2, + "origin": { + "source": "qrScan", + "appName": "privacyIDEA Authenticator", + "data": + "otpauth://pipush/PIPU00064CF0?url=https%3A//192.168.56.103/ttype/push&ttl=10&issuer=privacyIDEA&enrollment_credential=9d3100908d3c76a948b6041c8338def8b15ec06a&v=1&serial=PIPU00064CF0&sslverify=0", + "isPrivacyIdeaToken": null, + "createdAt": "2024-04-11T15:19:53.567296", + "piServerVersion": null + }, + "type": "PIPUSH", + "expirationDate": "2024-04-11T15:29:53.562967", + "serial": "PIPU00064CF0", + "fbToken": + "ffhpC7m3R7GEsTimtuA7u1:APA91bF3KB7MD2HEuS3bEB2XrLApgD7XksB1tcaDvA8HReQyFLCF7rI2U8i57dVkfqIAEmBDPYqzszkKD3lGKk6ihEgyCJzk4NC2tCetN3li-p7sRRbnePm34xxK5Se72rl9CKtMZsKX", + "sslVerify": false, + "enrollmentCredentials": "9d3100908d3c76a948b6041c8338def8b15ec06a", + "url": "https://192.168.2.169/ttype/push", + "isRolledOut": true, + "rolloutState": "rolloutComplete", + "publicServerKey": + "MIICCgKCAgEA6u3K9x1poOy8upX7WjWldNdV883T+XQTxCIlqdFod3xA7uyA3tdnI+ahPB/ZSeTrCh4jzJjw9kSCN77I0c6TgyfeXHDQJu5nZ8eSbChnrLNGxaxf24LY7RukLEBdumeHbbuc3EozRCdTMDPEYnWH/ct3zMBuIBk22gIuxS979Htcc1SgmKBfiEOVG5D7/qTvg3/EttazoIfQUllY6vWLk3vdGurXj9CD9UVc5qhYI54dLSKnR0KXjXsQ1GwBivf1BFeR/NkaSsTGxDvucJdJDI9d7aWfNXQaYln6SyuSJN8FYEmfhldQyU2dgi7jfToLO5GZPdzJhWKaUG0HqCv0cYLC1fP72+KR8DVUU4yAU/npR9inwBPnqmCxE+pt6WZeLWIhk3B34WUkKsQ0MRxyds1aNP5AACQATDYmRRu05nYD/kLM9aw03dmxwOUGkIVlLkm5quucvWJ6GGfrkG0G+lq0RjG3Ra8tPda8P4wq7MZ6J1HCrtiXFeniShPZwH/jhiHzqjfHb6AEHVyv56Ycx941KXRv6LaH3LHHhiMapY0NbKdid0WFBqutFUvBVoaT0ma3gKMMbGw9zCi5PdVHk9WyhMF7IvmcTuAuFL1xc25dc7kYrp+jrKVlJdiTD9vEOODI3ZxVdbTlGI6HH3NejlQvVd0S30vNu0sMZ55k9TECAwEAAQ==", + "privateTokenKey": + "MIILKgIBAAKCAgEAitdUL+H0M+hIddD+2cXJ58sB77TkygmmiNft1o0gIx/1jlYk9tpy05TRuKzZT7A5qQLfwIb8qYoibvrn2qG3Vx1xM9X45NkyqnktReA8d/E7W9+WdN8x7hCLGROTI9y8cEUqJV/l4O4JcAM4CN9sn2KLBVxDkzfUyBTNNgU6v2rWonCJGSYuiWdVGwrcM6bxwHSMgIvZLdMSl7nPl0VHan+CtFarikuIN4J6YES00musVO4Ss6/nb4Z28umyGyGGXGv/nV5hABPipyDmLGxTChLn0lDYvTDhKgiQYwf+6bwnXmGUPPlUGHnlRQ0xnPt1BSHBtDnEVKkLXy8rJ1nMoBc7/tHZl7gMIPET4/WFi4/9unopLkObWZs6p+8mssC9HlbFNteMkkegc5uha5chxA/jxjRgbdEQNi9kr3Y46qJMZ+tSbioOXmCXYm9DdJGfjyAU78LJUGle1xEfiQDX6Egr6PvCyvhU2QW9aBJFKzDbEitp4cVLQ3Jz0X/yLrigLe+dVis3U3DcCe3l0dFcI6yAsVx1RG6DtgO9Wr2vzO/u8JUARYoVWroNGBg+Xr3Il0nloVsnJD7l+7vh/5JZmObWz0bTOHwZhqgagB+h/Y5ill9zqqMWIGSuuJx4tGN4iBITNM/wy8bzZc2LLzSNDsuWSBlcchF/2Ko5qz4uw3MCggIBAIeCxUN62Rt0ooyDcMpilr8qz/gC6a8cHjO/9P4MfgpqXgQPpkJdUeeaf9sssWlqycxg5ryFw8NB9Hu3XmZwon5fWSbLuAK8zwu9++cQhlRW2aXBI5tsYNJHyUuvZFSDVFSL3SffYoHYMtPmAGIAl4c06hVQSRA+VOr4CpaoIf/EKjsfdWBbeN5ZQaKERgoP7jL2TlAErvNf6Oy3yZqOXrbeP1b+pKz1Lb1oF0MwrAAYYXn/Z7eDf3LFngFXAKQkY3x/WFxjSm+F+RlALge7rozNiQAoMnLta0bYVvyhSDcfkpk2yjV88Ix3JMPxrbPXZg6dNO9P1oT+/P9pDUh6CBHU7CPBQIl+FkcEhVErqMXHkedfApjFNvejkEoyBdehojDTdP8t1Xk7T9IEoXvF1DB5H2TpiJlhe3IDSZ0k+S76OweJmxq3mjpfpo+W8ECML/Pl3eZPZ9qkwJlbEkC5EGfaJg00N6eqJQcBSkCwURhY3b2ynN0Jcs1ede/Xx+Jf6DTbop9S5bW9/E0epbNBWLFPIr3YAZ2t45sJ1tjj/FzdBYxDA4Dznu6k5fuxg/ps3PcoyJ7kmMDjc0WSDFrTVdY4DqJhy0fyJIlpaibXZU76z8H6rg0y7EPnOLw26aIPSw8qdKcUvIKVtls/pJXNNjsXmlZp5xVRDPSdxCLY80sRAoICAQCHgsVDetkbdKKMg3DKYpa/Ks/4AumvHB4zv/T+DH4Kal4ED6ZCXVHnmn/bLLFpasnMYOa8hcPDQfR7t15mcKJ+X1kmy7gCvM8LvfvnEIZUVtmlwSObbGDSR8lLr2RUg1RUi90n32KB2DLT5gBiAJeHNOoVUEkQPlTq+AqWqCH/xCo7H3VgW3jeWUGihEYKD+4y9k5QBK7zX+jst8majl623j9W/qSs9S29aBdDMKwAGGF5/2e3g39yxZ4BVwCkJGN8f1hcY0pvhfkZQC4Hu66MzYkAKDJy7WtG2Fb8oUg3H5KZNso1fPCMdyTD8a2z12YOnTTvT9aE/vz/aQ1IeggR1OwjwUCJfhZHBIVRK6jFx5HnXwKYxTb3o5BKMgXXoaIw03T/LdV5O0/SBKF7xdQweR9k6YiZYXtyA0mdJPku+jsHiZsat5o6X6aPlvBAjC/z5d3mT2fapMCZWxJAuRBn2iYNNDenqiUHAUpAsFEYWN29spzdCXLNXnXv18fiX+g026KfUuW1vfxNHqWzQVixTyK92AGdreObCdbY4/xc3QWMQwOA857upOX7sYP6bNz3KMie5JjA43NFkgxa01XWOA6iYctH8iSJaWom12VO+s/B+q4NMuxD5zi8NumiD0sPKnSnFLyClbZbP6SVzTY7F5pWaecVUQz0ncQi2PNLEQKCAQEA/G9PEH0qrMia7UvI4CRgW0KLMHFPKulzmD8DLJFuLbFDg/d+9v/jBVA13ZrFW1rAOEh8Oz7T2ujQwFw0RGkewYqaWsTI5jYB9ddEKfN3TPb2BHA1wxXSvlrR6T1Fp88aT9KFXDPlHur/q9qBCvkvP7n/1rd6Okx1wsy/zloJKrkyjTPNXjFC8zpGQurRo1t1tkcoOl7fLE2E8qC7I7XtNtZv3Wk0AISlWXDDw3UvRy+jV7ieclKKwaCxMHyAZJ0Op1xS1q/wG24ymafb8d0+h+4vd2iE7F0Kj7EQchwWBHhNnsnG1EtTVB8n0bP8U/T4Hc5D4q16jT71Zg23U5xEyQKCAQEAjM1Q8SBBCpu7N3f1jhoQav+Fty+tiLu1W+JGUbaUWBhDg5zsz47B6gJKCw/llDKqJ+pAgyUKJYU+3Z59UkaxkN+Se1fXxRJx4p3EOTfuI17RQFxADg9x6kkLovwXC9oG/r216kqoMT4j+YM56Vt4T2FgY1vRiifPhKHByol8bj8jnXRhZrg4Iz0ANIhMYZVEJABnjkjplLtPCpzayl6Y/J98y/herty0e0hhkouDDLO2IhlSZ3L56tFkSAQTdgNrquv7fmo8DJbe9emCYdIGIbHUEobSzO7rXu6F2yrGhn1xZfjnR58eJGwkL4aKdUFZEmHy1cvm3Ou+FkqlqkfQWwKCAQEA7clUKu4c0uGsvrbSpADgG1cVki5KKtv5nYJN1R+xL615MchjevwTt5+U/giau7FCvEHbdFt8aQtCCNFSEtcKt7l+KN6Rd/mL4y5B8Vp8GK3RlOC2Y+wctl8KuLCU+rvlxydBpFbmDzfCWvna8KFF1ru4uWPf6Sa5DySb0R+S3wHREp2naIDy1fcg1Ewp6b1vpqJkzIctpqfnAj5RyhPHPg7FFUXSTGKm9xd38JhkTqQbM7ie2IXUWwypnEjLEPu5IAGhrsXQYaZuV7t9Pdnw206Mu+hivdvu5Ogf272FJ/TC+T6M4tGJzwYCFlF68QMi7cCsxcwwUvjpZJarCEF9sQKCAQBex8b4ydF+pp48FJBDe+AZZrBIQ9v48wJ+O69CSjlJo+uuqO/wOBToxWm6UJUmUYShIdsTbNeLskpDPPD3dYcKErW0OcmRa30mIzV3nuK7BJSvUmn8DQGNyGYA7NlGrRmQWXwfnunhXAczataM83nlVZNgzuoaqfnTOmANSsdsHyyGTVVTpCaF8gY1Vpq0BZq88VjEOuihqgTnC/dryooJZALJ+wMhiogjhPHJiAhLgJ3WDl2eLZN2MkXjBHtlMaBEil3dFv4dK2Ii/3E5D/v4qpAreH5mXV4rpTyN8Bl7Zu3yyr5FRCMyOWmSZGrHy5l9+llQ+dUKWda3gsBKA9WJAoIBAQC+3ybL8bHxhM4FqXVxEBMxsavxsdEBHvUSqi6D8wSLpwnKhP+487ZaRw0Ez0hcbQjXzitbKGHpFKZccN+e1vVBsGra0uZHo8GV0AERTTOZs9pHNjYva8dJ4Kg+G1qT5xqM3c4kWa9w44ExgS8lVcKlXrestO30HLXRZOgf8wmaCy5ybvVqRlARgUcUPMltPdevOMfJnFXO6YmcqRV1ugCauCqPydp6TC1cy1uanVs3K6H/D2jB2JLTdLQUtrwugHVR1GMhyeiPpstdxmrWVfZNPfUF7WRq5KXloAiC7XHIl0frO+KM7D6lXxQYxSwS1lTnXLwuvQ9yQLZOeCcwkjD0", + "publicTokenKey": + "MIICCgKCAgEAitdUL+H0M+hIddD+2cXJ58sB77TkygmmiNft1o0gIx/1jlYk9tpy05TRuKzZT7A5qQLfwIb8qYoibvrn2qG3Vx1xM9X45NkyqnktReA8d/E7W9+WdN8x7hCLGROTI9y8cEUqJV/l4O4JcAM4CN9sn2KLBVxDkzfUyBTNNgU6v2rWonCJGSYuiWdVGwrcM6bxwHSMgIvZLdMSl7nPl0VHan+CtFarikuIN4J6YES00musVO4Ss6/nb4Z28umyGyGGXGv/nV5hABPipyDmLGxTChLn0lDYvTDhKgiQYwf+6bwnXmGUPPlUGHnlRQ0xnPt1BSHBtDnEVKkLXy8rJ1nMoBc7/tHZl7gMIPET4/WFi4/9unopLkObWZs6p+8mssC9HlbFNteMkkegc5uha5chxA/jxjRgbdEQNi9kr3Y46qJMZ+tSbioOXmCXYm9DdJGfjyAU78LJUGle1xEfiQDX6Egr6PvCyvhU2QW9aBJFKzDbEitp4cVLQ3Jz0X/yLrigLe+dVis3U3DcCe3l0dFcI6yAsVx1RG6DtgO9Wr2vzO/u8JUARYoVWroNGBg+Xr3Il0nloVsnJD7l+7vh/5JZmObWz0bTOHwZhqgagB+h/Y5ill9zqqMWIGSuuJx4tGN4iBITNM/wy8bzZc2LLzSNDsuWSBlcchF/2Ko5qz4uw3MCAwEAAQ==" + }); + + final request = PushRequest.fromJson({ + "title": "privacyIDEA", + "question": "Do you want to confirm the login?", + "id": 134382661, + "uri": "https://192.168.56.103/ttype/push", + "nonce": "DIHEUYEDNJ6AC5FSGM7T3OTHTD6T5NK4", + "sslVerify": false, + "expirationDate": "2024-04-11T15:52:00.136352", + "serial": "PIPU00064CF0", + "signature": + "LNIZZSTEFVECXOFBHT4ANPLJXXUJA2S7CQ6S52KMWGE22LUBMWMZSF6BSQHV3NAI2RTHVUVFAPYALQ3A4W3Z4H7S26QZPRSEVHT4EMX2JPJOWNH5A6SSXLLPQJAZ3MDMKKXYSJOU27KHHHL56YQNKHOJQZPH5TPLFC6NPMQ4IGZB6TX4MLA2PIHZGGHIJM2TOXE4NFWNLDR5YKQ6JH7WO4G24VCACK7KKQRTZYXZFMSYAMO4ERBAYYQDS7SL6Y7CDKA4MBKSR2BKGYUVTR5AZUNVNHFWO7KPF3Y2THIXOSSMQ7VHHDCUQN6NGV63A27V7IX4EP6JRIDMHNOVEAVEIPFHKK55QCBFX2Y6HO4EZBP2X3ZXI5NEI7FO3CJ2VIC4ZFXOT4HKYTZRGTENAMLTAP56XCTDSKPNEUSZZMO6UQCCWGTQ5QTST47OIML4BLZJQOESXJ3OVUUWCZHCS6V46OMIRAQDIGRHGS7KQY7ZY4MKRRW4RDW7J4IYQ3EZWP777IKZCFNMQ6WV2KDA6W7T6O7VJB5NQ7VFQ3JGYPR6STX52H2RIEFKMNLMNW7UFDPZWDVCRLHI7FHOROHSKOECEC3T3LP7GLBZHHTGX46DGCOETLLEF67HU62DZHAUCOUPWHF6TY7KKKTQ3XNMDF5H4TWO7C5JTL46QC4PYFFOUDEUULTY2DVJBCMIXXF63PZ4YGAYFU4BPW3LTMBTM3PT6YBNJ6EQFUBR6N3KYAAUENZQBK3J5VZS6UWEBFL33AW3AFVV6TMVZQU4UJOJMSPL7T46F2VRRS27TA4FFE4JA5O6AVNETRYA====", + "accepted": null + }); + + final success = await request.verifySignature(token); + // Assert + expect(success, true); + }); + test('verifySignature false', () async { + // Arrange + final token = PushToken.fromJson(const { + "label": "PIPU00064CF0", + "issuer": "privacyIDEA", + "id": "94a40d5a-1dba-4985-95ce-5ce9bb36d32a", + "pin": false, + "isLocked": false, + "isHidden": false, + "tokenImage": "", + "folderId": null, + "sortIndex": 2, + "origin": { + "source": "qrScan", + "appName": "privacyIDEA Authenticator", + "data": + "otpauth://pipush/PIPU00064CF0?url=https%3A//192.168.56.103/ttype/push&ttl=10&issuer=privacyIDEA&enrollment_credential=9d3100908d3c76a948b6041c8338def8b15ec06a&v=1&serial=PIPU00064CF0&sslverify=0", + "isPrivacyIdeaToken": null, + "createdAt": "2024-04-11T15:19:53.567296", + "piServerVersion": null + }, + "type": "PIPUSH", + "expirationDate": "2024-04-11T15:29:53.562967", + "serial": "FALSCHER SERIAL", + "fbToken": + "ffhpC7m3R7GEsTimtuA7u1:APA91bF3KB7MD2HEuS3bEB2XrLApgD7XksB1tcaDvA8HReQyFLCF7rI2U8i57dVkfqIAEmBDPYqzszkKD3lGKk6ihEgyCJzk4NC2tCetN3li-p7sRRbnePm34xxK5Se72rl9CKtMZsKX", + "sslVerify": false, + "enrollmentCredentials": "9d3100908d3c76a948b6041c8338def8b15ec06a", + "url": "https://192.168.2.169/ttype/push", + "isRolledOut": true, + "rolloutState": "rolloutComplete", + "publicServerKey": + "MIICCgKCAgEA6u3K9x1poOy8upX7WjWldNdV883T+XQTxCIlqdFod3xA7uyA3tdnI+ahPB/ZSeTrCh4jzJjw9kSCN77I0c6TgyfeXHDQJu5nZ8eSbChnrLNGxaxf24LY7RukLEBdumeHbbuc3EozRCdTMDPEYnWH/ct3zMBuIBk22gIuxS979Htcc1SgmKBfiEOVG5D7/qTvg3/EttazoIfQUllY6vWLk3vdGurXj9CD9UVc5qhYI54dLSKnR0KXjXsQ1GwBivf1BFeR/NkaSsTGxDvucJdJDI9d7aWfNXQaYln6SyuSJN8FYEmfhldQyU2dgi7jfToLO5GZPdzJhWKaUG0HqCv0cYLC1fP72+KR8DVUU4yAU/npR9inwBPnqmCxE+pt6WZeLWIhk3B34WUkKsQ0MRxyds1aNP5AACQATDYmRRu05nYD/kLM9aw03dmxwOUGkIVlLkm5quucvWJ6GGfrkG0G+lq0RjG3Ra8tPda8P4wq7MZ6J1HCrtiXFeniShPZwH/jhiHzqjfHb6AEHVyv56Ycx941KXRv6LaH3LHHhiMapY0NbKdid0WFBqutFUvBVoaT0ma3gKMMbGw9zCi5PdVHk9WyhMF7IvmcTuAuFL1xc25dc7kYrp+jrKVlJdiTD9vEOODI3ZxVdbTlGI6HH3NejlQvVd0S30vNu0sMZ55k9TECAwEAAQ==", + "privateTokenKey": + "MIILKgIBAAKCAgEAitdUL+H0M+hIddD+2cXJ58sB77TkygmmiNft1o0gIx/1jlYk9tpy05TRuKzZT7A5qQLfwIb8qYoibvrn2qG3Vx1xM9X45NkyqnktReA8d/E7W9+WdN8x7hCLGROTI9y8cEUqJV/l4O4JcAM4CN9sn2KLBVxDkzfUyBTNNgU6v2rWonCJGSYuiWdVGwrcM6bxwHSMgIvZLdMSl7nPl0VHan+CtFarikuIN4J6YES00musVO4Ss6/nb4Z28umyGyGGXGv/nV5hABPipyDmLGxTChLn0lDYvTDhKgiQYwf+6bwnXmGUPPlUGHnlRQ0xnPt1BSHBtDnEVKkLXy8rJ1nMoBc7/tHZl7gMIPET4/WFi4/9unopLkObWZs6p+8mssC9HlbFNteMkkegc5uha5chxA/jxjRgbdEQNi9kr3Y46qJMZ+tSbioOXmCXYm9DdJGfjyAU78LJUGle1xEfiQDX6Egr6PvCyvhU2QW9aBJFKzDbEitp4cVLQ3Jz0X/yLrigLe+dVis3U3DcCe3l0dFcI6yAsVx1RG6DtgO9Wr2vzO/u8JUARYoVWroNGBg+Xr3Il0nloVsnJD7l+7vh/5JZmObWz0bTOHwZhqgagB+h/Y5ill9zqqMWIGSuuJx4tGN4iBITNM/wy8bzZc2LLzSNDsuWSBlcchF/2Ko5qz4uw3MCggIBAIeCxUN62Rt0ooyDcMpilr8qz/gC6a8cHjO/9P4MfgpqXgQPpkJdUeeaf9sssWlqycxg5ryFw8NB9Hu3XmZwon5fWSbLuAK8zwu9++cQhlRW2aXBI5tsYNJHyUuvZFSDVFSL3SffYoHYMtPmAGIAl4c06hVQSRA+VOr4CpaoIf/EKjsfdWBbeN5ZQaKERgoP7jL2TlAErvNf6Oy3yZqOXrbeP1b+pKz1Lb1oF0MwrAAYYXn/Z7eDf3LFngFXAKQkY3x/WFxjSm+F+RlALge7rozNiQAoMnLta0bYVvyhSDcfkpk2yjV88Ix3JMPxrbPXZg6dNO9P1oT+/P9pDUh6CBHU7CPBQIl+FkcEhVErqMXHkedfApjFNvejkEoyBdehojDTdP8t1Xk7T9IEoXvF1DB5H2TpiJlhe3IDSZ0k+S76OweJmxq3mjpfpo+W8ECML/Pl3eZPZ9qkwJlbEkC5EGfaJg00N6eqJQcBSkCwURhY3b2ynN0Jcs1ede/Xx+Jf6DTbop9S5bW9/E0epbNBWLFPIr3YAZ2t45sJ1tjj/FzdBYxDA4Dznu6k5fuxg/ps3PcoyJ7kmMDjc0WSDFrTVdY4DqJhy0fyJIlpaibXZU76z8H6rg0y7EPnOLw26aIPSw8qdKcUvIKVtls/pJXNNjsXmlZp5xVRDPSdxCLY80sRAoICAQCHgsVDetkbdKKMg3DKYpa/Ks/4AumvHB4zv/T+DH4Kal4ED6ZCXVHnmn/bLLFpasnMYOa8hcPDQfR7t15mcKJ+X1kmy7gCvM8LvfvnEIZUVtmlwSObbGDSR8lLr2RUg1RUi90n32KB2DLT5gBiAJeHNOoVUEkQPlTq+AqWqCH/xCo7H3VgW3jeWUGihEYKD+4y9k5QBK7zX+jst8majl623j9W/qSs9S29aBdDMKwAGGF5/2e3g39yxZ4BVwCkJGN8f1hcY0pvhfkZQC4Hu66MzYkAKDJy7WtG2Fb8oUg3H5KZNso1fPCMdyTD8a2z12YOnTTvT9aE/vz/aQ1IeggR1OwjwUCJfhZHBIVRK6jFx5HnXwKYxTb3o5BKMgXXoaIw03T/LdV5O0/SBKF7xdQweR9k6YiZYXtyA0mdJPku+jsHiZsat5o6X6aPlvBAjC/z5d3mT2fapMCZWxJAuRBn2iYNNDenqiUHAUpAsFEYWN29spzdCXLNXnXv18fiX+g026KfUuW1vfxNHqWzQVixTyK92AGdreObCdbY4/xc3QWMQwOA857upOX7sYP6bNz3KMie5JjA43NFkgxa01XWOA6iYctH8iSJaWom12VO+s/B+q4NMuxD5zi8NumiD0sPKnSnFLyClbZbP6SVzTY7F5pWaecVUQz0ncQi2PNLEQKCAQEA/G9PEH0qrMia7UvI4CRgW0KLMHFPKulzmD8DLJFuLbFDg/d+9v/jBVA13ZrFW1rAOEh8Oz7T2ujQwFw0RGkewYqaWsTI5jYB9ddEKfN3TPb2BHA1wxXSvlrR6T1Fp88aT9KFXDPlHur/q9qBCvkvP7n/1rd6Okx1wsy/zloJKrkyjTPNXjFC8zpGQurRo1t1tkcoOl7fLE2E8qC7I7XtNtZv3Wk0AISlWXDDw3UvRy+jV7ieclKKwaCxMHyAZJ0Op1xS1q/wG24ymafb8d0+h+4vd2iE7F0Kj7EQchwWBHhNnsnG1EtTVB8n0bP8U/T4Hc5D4q16jT71Zg23U5xEyQKCAQEAjM1Q8SBBCpu7N3f1jhoQav+Fty+tiLu1W+JGUbaUWBhDg5zsz47B6gJKCw/llDKqJ+pAgyUKJYU+3Z59UkaxkN+Se1fXxRJx4p3EOTfuI17RQFxADg9x6kkLovwXC9oG/r216kqoMT4j+YM56Vt4T2FgY1vRiifPhKHByol8bj8jnXRhZrg4Iz0ANIhMYZVEJABnjkjplLtPCpzayl6Y/J98y/herty0e0hhkouDDLO2IhlSZ3L56tFkSAQTdgNrquv7fmo8DJbe9emCYdIGIbHUEobSzO7rXu6F2yrGhn1xZfjnR58eJGwkL4aKdUFZEmHy1cvm3Ou+FkqlqkfQWwKCAQEA7clUKu4c0uGsvrbSpADgG1cVki5KKtv5nYJN1R+xL615MchjevwTt5+U/giau7FCvEHbdFt8aQtCCNFSEtcKt7l+KN6Rd/mL4y5B8Vp8GK3RlOC2Y+wctl8KuLCU+rvlxydBpFbmDzfCWvna8KFF1ru4uWPf6Sa5DySb0R+S3wHREp2naIDy1fcg1Ewp6b1vpqJkzIctpqfnAj5RyhPHPg7FFUXSTGKm9xd38JhkTqQbM7ie2IXUWwypnEjLEPu5IAGhrsXQYaZuV7t9Pdnw206Mu+hivdvu5Ogf272FJ/TC+T6M4tGJzwYCFlF68QMi7cCsxcwwUvjpZJarCEF9sQKCAQBex8b4ydF+pp48FJBDe+AZZrBIQ9v48wJ+O69CSjlJo+uuqO/wOBToxWm6UJUmUYShIdsTbNeLskpDPPD3dYcKErW0OcmRa30mIzV3nuK7BJSvUmn8DQGNyGYA7NlGrRmQWXwfnunhXAczataM83nlVZNgzuoaqfnTOmANSsdsHyyGTVVTpCaF8gY1Vpq0BZq88VjEOuihqgTnC/dryooJZALJ+wMhiogjhPHJiAhLgJ3WDl2eLZN2MkXjBHtlMaBEil3dFv4dK2Ii/3E5D/v4qpAreH5mXV4rpTyN8Bl7Zu3yyr5FRCMyOWmSZGrHy5l9+llQ+dUKWda3gsBKA9WJAoIBAQC+3ybL8bHxhM4FqXVxEBMxsavxsdEBHvUSqi6D8wSLpwnKhP+487ZaRw0Ez0hcbQjXzitbKGHpFKZccN+e1vVBsGra0uZHo8GV0AERTTOZs9pHNjYva8dJ4Kg+G1qT5xqM3c4kWa9w44ExgS8lVcKlXrestO30HLXRZOgf8wmaCy5ybvVqRlARgUcUPMltPdevOMfJnFXO6YmcqRV1ugCauCqPydp6TC1cy1uanVs3K6H/D2jB2JLTdLQUtrwugHVR1GMhyeiPpstdxmrWVfZNPfUF7WRq5KXloAiC7XHIl0frO+KM7D6lXxQYxSwS1lTnXLwuvQ9yQLZOeCcwkjD0", + "publicTokenKey": + "MIICCgKCAgEAitdUL+H0M+hIddD+2cXJ58sB77TkygmmiNft1o0gIx/1jlYk9tpy05TRuKzZT7A5qQLfwIb8qYoibvrn2qG3Vx1xM9X45NkyqnktReA8d/E7W9+WdN8x7hCLGROTI9y8cEUqJV/l4O4JcAM4CN9sn2KLBVxDkzfUyBTNNgU6v2rWonCJGSYuiWdVGwrcM6bxwHSMgIvZLdMSl7nPl0VHan+CtFarikuIN4J6YES00musVO4Ss6/nb4Z28umyGyGGXGv/nV5hABPipyDmLGxTChLn0lDYvTDhKgiQYwf+6bwnXmGUPPlUGHnlRQ0xnPt1BSHBtDnEVKkLXy8rJ1nMoBc7/tHZl7gMIPET4/WFi4/9unopLkObWZs6p+8mssC9HlbFNteMkkegc5uha5chxA/jxjRgbdEQNi9kr3Y46qJMZ+tSbioOXmCXYm9DdJGfjyAU78LJUGle1xEfiQDX6Egr6PvCyvhU2QW9aBJFKzDbEitp4cVLQ3Jz0X/yLrigLe+dVis3U3DcCe3l0dFcI6yAsVx1RG6DtgO9Wr2vzO/u8JUARYoVWroNGBg+Xr3Il0nloVsnJD7l+7vh/5JZmObWz0bTOHwZhqgagB+h/Y5ill9zqqMWIGSuuJx4tGN4iBITNM/wy8bzZc2LLzSNDsuWSBlcchF/2Ko5qz4uw3MCAwEAAQ==" + }); + + final request = PushRequest.fromJson({ + "title": "privacyIDEA", + "question": "Do you want to confirm the login?", + "id": 134382661, + "uri": "https://192.168.56.103/ttype/push", + "nonce": "DIHEUYEDNJ6AC5FSGM7T3OTHTD6T5NK4", + "sslVerify": false, + "expirationDate": "2024-04-11T15:52:00.136352", + "serial": "FALSCHER SERIAL", + "signature": + "LNIZZSTEFVECXOFBHT4ANPLJXXUJA2S7CQ6S52KMWGE22LUBMWMZSF6BSQHV3NAI2RTHVUVFAPYALQ3A4W3Z4H7S26QZPRSEVHT4EMX2JPJOWNH5A6SSXLLPQJAZ3MDMKKXYSJOU27KHHHL56YQNKHOJQZPH5TPLFC6NPMQ4IGZB6TX4MLA2PIHZGGHIJM2TOXE4NFWNLDR5YKQ6JH7WO4G24VCACK7KKQRTZYXZFMSYAMO4ERBAYYQDS7SL6Y7CDKA4MBKSR2BKGYUVTR5AZUNVNHFWO7KPF3Y2THIXOSSMQ7VHHDCUQN6NGV63A27V7IX4EP6JRIDMHNOVEAVEIPFHKK55QCBFX2Y6HO4EZBP2X3ZXI5NEI7FO3CJ2VIC4ZFXOT4HKYTZRGTENAMLTAP56XCTDSKPNEUSZZMO6UQCCWGTQ5QTST47OIML4BLZJQOESXJ3OVUUWCZHCS6V46OMIRAQDIGRHGS7KQY7ZY4MKRRW4RDW7J4IYQ3EZWP777IKZCFNMQ6WV2KDA6W7T6O7VJB5NQ7VFQ3JGYPR6STX52H2RIEFKMNLMNW7UFDPZWDVCRLHI7FHOROHSKOECEC3T3LP7GLBZHHTGX46DGCOETLLEF67HU62DZHAUCOUPWHF6TY7KKKTQ3XNMDF5H4TWO7C5JTL46QC4PYFFOUDEUULTY2DVJBCMIXXF63PZ4YGAYFU4BPW3LTMBTM3PT6YBNJ6EQFUBR6N3KYAAUENZQBK3J5VZS6UWEBFL33AW3AFVV6TMVZQU4UJOJMSPL7T46F2VRRS27TA4FFE4JA5O6AVNETRYA====", + "accepted": null + }); + + final success = await request.verifySignature(token); + // Assert + expect(success, false); + }); + }); +} diff --git a/test/unit_test/model/states_test/introduction_state_test.dart b/test/unit_test/model/states/introduction_state_test.dart similarity index 76% rename from test/unit_test/model/states_test/introduction_state_test.dart rename to test/unit_test/model/states/introduction_state_test.dart index c78aaeda4..d18bcbf60 100644 --- a/test/unit_test/model/states_test/introduction_state_test.dart +++ b/test/unit_test/model/states/introduction_state_test.dart @@ -6,20 +6,20 @@ void main() { group('IntroductionState', () { test('withCompletedIntroduction add introduction', () { const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder}); - final updatedState = introductionState.withCompletedIntroduction(Introduction.addTokenManually); - expect(updatedState.completedIntroductions, {Introduction.addFolder, Introduction.addTokenManually}); + final updatedState = introductionState.withCompletedIntroduction(Introduction.addManually); + expect(updatedState.completedIntroductions, {Introduction.addFolder, Introduction.addManually}); }); test('withoutCompletedIntroduction remove introduction', () { - const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder, Introduction.addTokenManually}); - final updatedState = introductionState.withoutCompletedIntroduction(Introduction.addTokenManually); + const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder, Introduction.addManually}); + final updatedState = introductionState.withoutCompletedIntroduction(Introduction.addManually); expect(updatedState.completedIntroductions, {Introduction.addFolder}); }); test('withoutCompletedIntroduction add duplicate introduction', () { - const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder, Introduction.addTokenManually}); - final updatedState = introductionState.withCompletedIntroduction(Introduction.addTokenManually); - expect(updatedState.completedIntroductions, {Introduction.addFolder, Introduction.addTokenManually}); + const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder, Introduction.addManually}); + final updatedState = introductionState.withCompletedIntroduction(Introduction.addManually); + expect(updatedState.completedIntroductions, {Introduction.addFolder, Introduction.addManually}); }); }); } diff --git a/test/unit_test/model/states_test/settings_state_test.dart b/test/unit_test/model/states/settings_state_test.dart similarity index 100% rename from test/unit_test/model/states_test/settings_state_test.dart rename to test/unit_test/model/states/settings_state_test.dart diff --git a/test/unit_test/model/states_test/token_folder_state_test.dart b/test/unit_test/model/states/token_folder_state_test.dart similarity index 84% rename from test/unit_test/model/states_test/token_folder_state_test.dart rename to test/unit_test/model/states/token_folder_state_test.dart index 2c08bad92..c137bb027 100644 --- a/test/unit_test/model/states_test/token_folder_state_test.dart +++ b/test/unit_test/model/states/token_folder_state_test.dart @@ -14,7 +14,7 @@ void _testTokenFolderState() { expect(state.folders.first.folderId, 1); }); test('withFolder', () { - final newState = state.withFolder('newFolder'); + final newState = state.addNewFolder('newFolder'); expect(state.folders.first.label, 'label'); expect(state.folders.first.folderId, 1); expect(newState.folders.length, 2); @@ -24,7 +24,7 @@ void _testTokenFolderState() { expect(newState.folders.last.folderId, 2); }); test('withUpdated', () { - final newState = state.withUpdated([const TokenFolder(label: 'labelUpdated', folderId: 1)]); + final newState = state.addOrReplaceFolders([const TokenFolder(label: 'labelUpdated', folderId: 1)]); expect(state.folders.first.label, 'label'); expect(state.folders.first.folderId, 1); expect(newState.folders.length, 1); @@ -32,7 +32,7 @@ void _testTokenFolderState() { expect(newState.folders.first.folderId, 1); }); test('withoutFolder', () { - final newState = state.withoutFolder(const TokenFolder(label: 'label', folderId: 1)); + final newState = state.removeFolder(const TokenFolder(label: 'label', folderId: 1)); expect(state.folders.first.label, 'label'); expect(state.folders.first.folderId, 1); expect(newState.folders.length, 0); diff --git a/test/unit_test/model/states_test/token_state_test.dart b/test/unit_test/model/states/token_state_test.dart similarity index 93% rename from test/unit_test/model/states_test/token_state_test.dart rename to test/unit_test/model/states/token_state_test.dart index d7439a6a1..53b121b4b 100644 --- a/test/unit_test/model/states_test/token_state_test.dart +++ b/test/unit_test/model/states/token_state_test.dart @@ -22,12 +22,6 @@ void _testTokenState() { final state = TokenState(tokens: [_TokenMock(id: 'id')]); expect(state.tokens.first, isA<_TokenMock>()); }); - test('repaceList', () { - final state = TokenState(tokens: [_TokenMock(id: 'id')]); - final newState = state.repaceList(tokens: [_TokenMock(id: 'idCopy')]); - expect((state.tokens.first as _TokenMock).id, 'id'); - expect((newState.tokens.first as _TokenMock).id, 'idCopy'); - }); test('withToken', () { final state = TokenState(tokens: [_TokenMock(id: 'id')]); final newState = state.withToken(_TokenMock(id: 'newid')); diff --git a/test/unit_test/model/token_test/day_password_test.dart b/test/unit_test/model/token/day_password_test.dart similarity index 82% rename from test/unit_test/model/token_test/day_password_test.dart rename to test/unit_test/model/token/day_password_test.dart index 96b93e836..9c78ec64f 100644 --- a/test/unit_test/model/token_test/day_password_test.dart +++ b/test/unit_test/model/token/day_password_test.dart @@ -2,11 +2,11 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; -import 'package:privacyidea_authenticator/model/enums/day_passoword_token_view_mode.dart'; +import 'package:privacyidea_authenticator/model/enums/day_password_token_view_mode.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/model/tokens/day_password_token.dart'; import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; -import 'package:privacyidea_authenticator/utils/crypto_utils.dart'; void main() { _testDayPasswordToken(); @@ -60,7 +60,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -70,7 +70,7 @@ void _testDayPasswordToken() { period: period, algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -82,7 +82,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -92,7 +92,7 @@ void _testDayPasswordToken() { period: period, algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -104,7 +104,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -114,7 +114,7 @@ void _testDayPasswordToken() { period: period, algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -126,7 +126,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -136,7 +136,7 @@ void _testDayPasswordToken() { period: period, algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -148,7 +148,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -158,7 +158,7 @@ void _testDayPasswordToken() { period: period, algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -173,7 +173,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -183,7 +183,7 @@ void _testDayPasswordToken() { period: period, algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -195,7 +195,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -205,7 +205,7 @@ void _testDayPasswordToken() { period: period, algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -217,7 +217,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -227,7 +227,7 @@ void _testDayPasswordToken() { period: period, algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -239,7 +239,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -249,7 +249,7 @@ void _testDayPasswordToken() { period: period, algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -261,7 +261,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -271,7 +271,7 @@ void _testDayPasswordToken() { period: period, algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -287,7 +287,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -297,7 +297,7 @@ void _testDayPasswordToken() { period: period, algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -309,7 +309,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -319,7 +319,7 @@ void _testDayPasswordToken() { period: period, algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -331,7 +331,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -341,7 +341,7 @@ void _testDayPasswordToken() { period: period, algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -357,7 +357,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -367,7 +367,7 @@ void _testDayPasswordToken() { period: period, algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -379,7 +379,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -389,7 +389,7 @@ void _testDayPasswordToken() { period: period, algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); @@ -401,7 +401,7 @@ void _testDayPasswordToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final dayPassword1h = DayPasswordToken( @@ -411,7 +411,7 @@ void _testDayPasswordToken() { period: period, algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(hotpToken.otpValue, dayPassword1h.otpValue); }); diff --git a/test/unit_test/model/token_test/hotp_token_test.dart b/test/unit_test/model/token/hotp_token_test.dart similarity index 89% rename from test/unit_test/model/token_test/hotp_token_test.dart rename to test/unit_test/model/token/hotp_token_test.dart index ab9ec6ad6..c064675fc 100644 --- a/test/unit_test/model/token_test/hotp_token_test.dart +++ b/test/unit_test/model/token/hotp_token_test.dart @@ -4,8 +4,8 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; -import 'package:privacyidea_authenticator/utils/crypto_utils.dart'; void main() { _testHotpToken(); @@ -144,7 +144,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: 6, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: 0, ); expect(token0.otpValue, '814628'); @@ -157,7 +157,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: 6, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: 1, ); expect(token1.otpValue, '533881'); @@ -170,7 +170,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: 6, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: 2, ); expect(token2.otpValue, '720111'); @@ -183,7 +183,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: 6, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: 8, ); expect(token8.otpValue, '963685'); @@ -200,7 +200,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: 8, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: 0, ); expect(token0.otpValue, '31814628'); @@ -213,7 +213,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: 8, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: 1, ); expect(token1.otpValue, '28533881'); @@ -226,7 +226,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: 8, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: 2, ); expect(token2.otpValue, '31720111'); @@ -239,7 +239,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: 8, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: 8, ); expect(token8.otpValue, '15963685'); @@ -256,7 +256,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: 6, - secret: encodeSecretAs(utf8.encode('Secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('Secret')), counter: 0, ); expect(token0.otpValue, '292574'); @@ -269,7 +269,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA256, digits: 6, - secret: encodeSecretAs(utf8.encode('Secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('Secret')), counter: 0, ); expect(token1.otpValue, '203782'); @@ -282,7 +282,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA512, digits: 6, - secret: encodeSecretAs(utf8.encode('Secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('Secret')), counter: 0, ); expect(token2.otpValue, '636350'); @@ -298,7 +298,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: 8, - secret: encodeSecretAs(utf8.encode('Secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('Secret')), counter: 0, ); expect(token0.otpValue, '25292574'); @@ -311,7 +311,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA256, digits: 8, - secret: encodeSecretAs(utf8.encode('Secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('Secret')), counter: 0, ); expect(token1.otpValue, '25203782'); @@ -324,7 +324,7 @@ void _testHotpToken() { issuer: '', algorithm: Algorithms.SHA512, digits: 8, - secret: encodeSecretAs(utf8.encode('Secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('Secret')), counter: 0, ); expect(token2.otpValue, '99636350'); diff --git a/test/unit_test/model/token_test/push_token_test.dart b/test/unit_test/model/token/push_token_test.dart similarity index 64% rename from test/unit_test/model/token_test/push_token_test.dart rename to test/unit_test/model/token/push_token_test.dart index f5e778ddf..31a935157 100644 --- a/test/unit_test/model/token_test/push_token_test.dart +++ b/test/unit_test/model/token/push_token_test.dart @@ -2,10 +2,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/enums/push_token_rollout_state.dart'; -import 'package:privacyidea_authenticator/model/push_request.dart'; -import 'package:privacyidea_authenticator/model/push_request_queue.dart'; import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; -import 'package:privacyidea_authenticator/utils/custom_int_buffer.dart'; void main() { _testPushToken(); @@ -13,15 +10,6 @@ void main() { void _testPushToken() { group('Push Token creation/method', () { - final pr = PushRequest( - title: 'title', - question: 'question', - uri: Uri.parse('http://www.example.com'), - nonce: 'nonce', - sslVerify: false, - id: 0, - expirationDate: DateTime(2017, 9, 7, 17, 30), - ); final pushToken = PushToken( serial: 'serial', expirationDate: DateTime(2017, 9, 7, 17, 30), @@ -36,8 +24,6 @@ void _testPushToken() { privateTokenKey: 'privateTokenKey', isRolledOut: true, rolloutState: PushTokenRollOutState.rolloutNotStarted, - pushRequests: PushRequestQueue(), - knownPushRequests: CustomIntBuffer(), type: 'type', sortIndex: 0, tokenImage: 'example.png', @@ -59,8 +45,6 @@ void _testPushToken() { expect(pushToken.privateTokenKey, 'privateTokenKey'); expect(pushToken.isRolledOut, true); expect(pushToken.rolloutState, PushTokenRollOutState.rolloutNotStarted); - expect(pushToken.pushRequests, PushRequestQueue()); - expect(pushToken.knownPushRequests.list, CustomIntBuffer().list); expect(pushToken.type, 'PIPUSH'); expect(pushToken.sortIndex, 0); expect(pushToken.tokenImage, 'example.png'); @@ -83,8 +67,6 @@ void _testPushToken() { privateTokenKey: 'privateTokenKeyCopy', isRolledOut: false, rolloutState: PushTokenRollOutState.rolloutComplete, - pushRequests: PushRequestQueue()..add(pr), - knownPushRequests: CustomIntBuffer()..put(0), sortIndex: 1, tokenImage: 'exampleCopy.png', folderId: () => 1, @@ -104,95 +86,12 @@ void _testPushToken() { expect(copy.privateTokenKey, 'privateTokenKeyCopy'); expect(copy.isRolledOut, false); expect(copy.rolloutState, PushTokenRollOutState.rolloutComplete); - expect(copy.pushRequests.list, [pr]); - expect(copy.knownPushRequests.list, [0]); expect(copy.sortIndex, 1); expect(copy.tokenImage, 'exampleCopy.png'); expect(copy.folderId, 1); expect(copy.isLocked, false); expect(copy.pin, false); }); - test('withPushRequest', () { - final tokenWithPr = PushToken( - serial: 'serial', - expirationDate: DateTime(2017, 9, 7, 17, 30), - label: 'label', - issuer: 'issuer', - id: 'id', - sslVerify: true, - enrollmentCredentials: 'enrollmentCredentials', - url: Uri.parse('http://www.example.com'), - publicServerKey: 'publicServerKey', - publicTokenKey: 'publicTokenKey', - privateTokenKey: 'privateTokenKey', - isRolledOut: true, - rolloutState: PushTokenRollOutState.rolloutNotStarted, - pushRequests: PushRequestQueue(), - knownPushRequests: CustomIntBuffer(), - type: 'type', - sortIndex: 0, - tokenImage: 'example.png', - folderId: 0, - isLocked: true, - pin: true, - ).withPushRequest(pr); - expect(tokenWithPr.pushRequests.list, [pr]); - expect(tokenWithPr.knownPushRequests.list, [0]); - }); - test('withoutPushrequest', () { - final tokenWithPr = PushToken( - serial: 'serial', - expirationDate: DateTime(2017, 9, 7, 17, 30), - label: 'label', - issuer: 'issuer', - id: 'id', - sslVerify: true, - enrollmentCredentials: 'enrollmentCredentials', - url: Uri.parse('http://www.example.com'), - publicServerKey: 'publicServerKey', - publicTokenKey: 'publicTokenKey', - privateTokenKey: 'privateTokenKey', - isRolledOut: true, - rolloutState: PushTokenRollOutState.rolloutNotStarted, - pushRequests: PushRequestQueue()..add(pr), - knownPushRequests: CustomIntBuffer()..put(0), - type: 'type', - sortIndex: 0, - tokenImage: 'example.png', - folderId: 0, - isLocked: true, - pin: true, - ); - final tokenWithoutPr = tokenWithPr.withoutPushRequest(pr); - expect(tokenWithoutPr.pushRequests.list, []); - expect(tokenWithoutPr.knownPushRequests.list, [0]); - }); - test('knowsRequestWithId', () { - final tokenWithPr = PushToken( - serial: 'serial', - expirationDate: DateTime(2017, 9, 7, 17, 30), - label: 'label', - issuer: 'issuer', - id: 'id', - sslVerify: true, - enrollmentCredentials: 'enrollmentCredentials', - url: Uri.parse('http://www.example.com'), - publicServerKey: 'publicServerKey', - publicTokenKey: 'publicTokenKey', - privateTokenKey: 'privateTokenKey', - isRolledOut: true, - rolloutState: PushTokenRollOutState.rolloutNotStarted, - pushRequests: PushRequestQueue()..add(pr), - knownPushRequests: CustomIntBuffer()..put(0), - type: 'type', - sortIndex: 0, - tokenImage: 'example.png', - folderId: 0, - isLocked: true, - pin: true, - ); - expect(tokenWithPr.knowsRequestWithId(0), true); - }); test('fromJson', () { final json = { "label": "label", @@ -214,8 +113,6 @@ void _testPushToken() { "publicServerKey": "publicServerKey", "privateTokenKey": "privateTokenKey", "publicTokenKey": "publicTokenKey", - "pushRequests": {"list": []}, - "knownPushRequests": {"list": []} }; final token = PushToken.fromJson(json); expect(token.label, 'label'); @@ -238,8 +135,6 @@ void _testPushToken() { expect(token.publicServerKey, 'publicServerKey'); expect(token.privateTokenKey, 'privateTokenKey'); expect(token.publicTokenKey, 'publicTokenKey'); - expect(token.pushRequests.list, []); - expect(token.knownPushRequests.list, []); }); test('toJson', () { final tokenJson = pushToken.toJson(); @@ -257,6 +152,7 @@ void _testPushToken() { "type": "PIPUSH", "expirationDate": "2017-09-07T17:30:00.000", "serial": "serial", + "fbToken": null, "sslVerify": true, "enrollmentCredentials": "enrollmentCredentials", "url": "http://www.example.com", @@ -264,9 +160,7 @@ void _testPushToken() { "rolloutState": "rolloutNotStarted", "publicServerKey": "publicServerKey", "privateTokenKey": "privateTokenKey", - "publicTokenKey": "publicTokenKey", - "pushRequests": {"list": []}, - "knownPushRequests": {"list": []} + "publicTokenKey": "publicTokenKey" }; expect(jsonEncode(tokenJson), jsonEncode(json)); }); diff --git a/test/unit_test/model/token/steam_token_test.dart b/test/unit_test/model/token/steam_token_test.dart new file mode 100644 index 000000000..6f96dc2fe --- /dev/null +++ b/test/unit_test/model/token/steam_token_test.dart @@ -0,0 +1,164 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/tokens/steam_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; + +void main() { + _testSteamToken(); +} + +void _testSteamToken() { + group('Steam Token', () { + group('TOTP Token creation/method', () { + final steamToken = SteamToken( + label: 'label', + issuer: 'issuer', + id: 'id', + secret: 'secret', + pin: false, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: false, + folderId: 0, + ); + test('constructor', () { + expect(steamToken.period, 30); // default period + expect(steamToken.label, 'label'); + expect(steamToken.issuer, 'issuer'); + expect(steamToken.id, 'id'); + expect(steamToken.algorithm, Algorithms.SHA1); // default algorithm + expect(steamToken.digits, 5); // default digits + expect(steamToken.secret, 'secret'); + expect(steamToken.type, 'STEAM'); + expect(steamToken.pin, false); + expect(steamToken.tokenImage, 'example.png'); + expect(steamToken.sortIndex, 0); + expect(steamToken.isLocked, false); + expect(steamToken.folderId, 0); + }); + test('copyWith', () { + final totpCopy = steamToken.copyWith( + period: 60, // Should not affect the period because steam tokens always have 30 seconds period + label: 'labelCopy', + issuer: 'issuerCopy', + id: 'idCopy', + algorithm: Algorithms.SHA256, // Should not affect the algorithm because steam tokens always have SHA1 algorithm + digits: 8, // Should not affect the digits because steam tokens always have 5 digits + secret: 'secretCopy', + pin: true, + tokenImage: 'exampleCopy.png', + sortIndex: 1, + isLocked: true, + folderId: () => 1, + ); + expect(totpCopy.period, 30); + expect(totpCopy.label, 'labelCopy'); + expect(totpCopy.issuer, 'issuerCopy'); + expect(totpCopy.id, 'idCopy'); + expect(totpCopy.algorithm, Algorithms.SHA1); + expect(totpCopy.digits, 5); + expect(totpCopy.secret, 'secretCopy'); + expect(totpCopy.type, 'STEAM'); + expect(totpCopy.pin, true); + expect(totpCopy.tokenImage, 'exampleCopy.png'); + expect(totpCopy.sortIndex, 1); + expect(totpCopy.isLocked, true); + expect(totpCopy.folderId, 1); + }); + group('fromUriMap', () { + test('with full map', () { + final uriMap = { + 'URI_LABEL': 'label', + 'URI_ISSUER': 'issuer', + 'URI_SECRET': Uint8List.fromList(utf8.encode('secret')), + 'URI_TYPE': 'totp', + 'URI_PIN': false, + 'URI_IMAGE': 'example.png', + }; + final totpFromUriMap = SteamToken.fromUriMap(uriMap); + expect(totpFromUriMap.period, 30); + expect(totpFromUriMap.label, 'label'); + expect(totpFromUriMap.issuer, 'issuer'); + expect(totpFromUriMap.algorithm, Algorithms.SHA1); + expect(totpFromUriMap.digits, 5); + expect(totpFromUriMap.secret, 'ONSWG4TFOQ======'); + expect(totpFromUriMap.type, 'STEAM'); + expect(totpFromUriMap.pin, false); + expect(totpFromUriMap.tokenImage, 'example.png'); + }); + test('with missing secret', () { + final uriMap = { + 'URI_LABEL': 'label', + 'URI_ISSUER': 'issuer', + 'URI_TYPE': 'totp', + 'URI_PIN': false, + 'URI_IMAGE': 'example.png', + }; + expect(() => SteamToken.fromUriMap(uriMap), throwsA(isA())); + }); + test('with empty map', () { + final uriMap = {}; + expect(() => TOTPToken.fromUriMap(uriMap), throwsA(isA())); + }); + }); + test('fromJson', () { + final steamJson = { + 'label': 'label', + 'issuer': 'issuer', + 'id': 'id', + 'secret': 'secret', + 'type': 'STEAM', + 'pin': true, + 'tokenImage': 'example.png', + 'sortIndex': 33, + 'isLocked': true, + 'folderId': 44, + }; + final steamFromJson = SteamToken.fromJson(steamJson); + expect(steamFromJson.period, 30); + expect(steamFromJson.label, 'label'); + expect(steamFromJson.issuer, 'issuer'); + expect(steamFromJson.id, 'id'); + expect(steamFromJson.algorithm, Algorithms.SHA1); + expect(steamFromJson.digits, 5); + expect(steamFromJson.secret, 'secret'); + expect(steamFromJson.type, 'STEAM'); + expect(steamFromJson.pin, true); + expect(steamFromJson.tokenImage, 'example.png'); + expect(steamFromJson.sortIndex, 33); + expect(steamFromJson.isLocked, true); + expect(steamFromJson.folderId, 44); + }); + test('toJson', () { + final totpJson = steamToken.toJson(); + expect(totpJson['label'], 'label'); + expect(totpJson['issuer'], 'issuer'); + expect(totpJson['id'], 'id'); + expect(totpJson['secret'], 'secret'); + expect(totpJson['type'], 'STEAM'); + expect(totpJson['pin'], false); + expect(totpJson['tokenImage'], 'example.png'); + expect(totpJson['sortIndex'], 0); + expect(totpJson['isLocked'], false); + expect(totpJson['folderId'], 0); + }); + }); + test('otpValue', () { + final time = DateTime.fromMillisecondsSinceEpoch(1712666212056); + + final steamToken = SteamToken( + label: '', + issuer: '', + id: '', + secret: 'SECRETA=', + ); + final otp = steamToken.otpOfTime(time); + final otpNow = steamToken.otpOfTime(DateTime.now()); + expect(otp, equals('JGPCJ')); // Checks if the otpOfTime works correctly + expect(steamToken.otpValue, equals(otpNow)); // Checks if the otpValue delivers the same value as the otpOfTime method + }); + }); +} diff --git a/test/unit_test/model/token_test/totp_token_test.dart b/test/unit_test/model/token/totp_token_test.dart similarity index 89% rename from test/unit_test/model/token_test/totp_token_test.dart rename to test/unit_test/model/token/totp_token_test.dart index 97390d2bf..2a0b01932 100644 --- a/test/unit_test/model/token_test/totp_token_test.dart +++ b/test/unit_test/model/token/totp_token_test.dart @@ -4,9 +4,9 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; -import 'package:privacyidea_authenticator/utils/crypto_utils.dart'; void main() { _testTotpToken(); @@ -170,7 +170,7 @@ void _testTotpToken() { expect(totpFromJson.algorithm, Algorithms.SHA1); expect(totpFromJson.digits, 22); expect(totpFromJson.secret, 'secret'); - expect(totpFromJson.type, 'TOTP'); + expect(totpFromJson.type, 'totp'); expect(totpFromJson.pin, true); expect(totpFromJson.tokenImage, 'example.png'); expect(totpFromJson.sortIndex, 33); @@ -204,7 +204,7 @@ void _testTotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final totpToken = TOTPToken( @@ -214,7 +214,7 @@ void _testTotpToken() { id: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(totpToken.otpValue, hotpToken.otpValue); }); @@ -226,7 +226,7 @@ void _testTotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final totpToken = TOTPToken( @@ -236,7 +236,7 @@ void _testTotpToken() { id: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(totpToken.otpValue, hotpToken.otpValue); }); @@ -251,7 +251,7 @@ void _testTotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final totpToken = TOTPToken( @@ -261,7 +261,7 @@ void _testTotpToken() { id: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(totpToken.otpValue, hotpToken.otpValue); }); @@ -273,7 +273,7 @@ void _testTotpToken() { issuer: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final totpToken = TOTPToken( @@ -283,7 +283,7 @@ void _testTotpToken() { id: '', algorithm: Algorithms.SHA1, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(totpToken.otpValue, hotpToken.otpValue); }); @@ -299,7 +299,7 @@ void _testTotpToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final totpToken = TOTPToken( @@ -309,7 +309,7 @@ void _testTotpToken() { id: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(totpToken.otpValue, hotpToken.otpValue); }); @@ -321,7 +321,7 @@ void _testTotpToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final totpToken = TOTPToken( @@ -331,7 +331,7 @@ void _testTotpToken() { id: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(totpToken.otpValue, hotpToken.otpValue); }); @@ -343,7 +343,7 @@ void _testTotpToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final totpToken = TOTPToken( @@ -353,7 +353,7 @@ void _testTotpToken() { id: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(totpToken.otpValue, hotpToken.otpValue); }); @@ -369,7 +369,7 @@ void _testTotpToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final totpToken = TOTPToken( @@ -379,7 +379,7 @@ void _testTotpToken() { id: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(totpToken.otpValue, hotpToken.otpValue); }); @@ -391,7 +391,7 @@ void _testTotpToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final totpToken = TOTPToken( @@ -401,7 +401,7 @@ void _testTotpToken() { id: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(totpToken.otpValue, hotpToken.otpValue); }); @@ -413,7 +413,7 @@ void _testTotpToken() { issuer: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, ); final totpToken = TOTPToken( @@ -423,7 +423,7 @@ void _testTotpToken() { id: '', algorithm: algorithm, digits: digits, - secret: encodeSecretAs(utf8.encode('secret'), Encodings.base32), + secret: Encodings.base32.encode(utf8.encode('secret')), ); expect(totpToken.otpValue, hotpToken.otpValue); }); diff --git a/test/unit_test/model/token_import/token_origin_data_test.dart b/test/unit_test/model/token_import/token_origin_data_test.dart new file mode 100644 index 000000000..643d311a8 --- /dev/null +++ b/test/unit_test/model/token_import/token_origin_data_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/token_import/token_origin_data.dart'; +import 'package:privacyidea_authenticator/model/version.dart'; + +void main() { + _testTokenOriginData(); +} + +void _testTokenOriginData() { + group('Token Origin Data', () { + TokenOriginData; + group('create', () { + test('constructor', () { + final tokenOriginData = TokenOriginData( + source: TokenOriginSourceType.manually, + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.now(), + piServerVersion: const Version(1, 0, 0), + ); + expect(tokenOriginData.source, TokenOriginSourceType.manually); + expect(tokenOriginData.data, 'data'); + expect(tokenOriginData.appName, 'appName'); + expect(tokenOriginData.isPrivacyIdeaToken, true); + expect(tokenOriginData.createdAt, isA()); + expect(tokenOriginData.piServerVersion, isA()); + }); + test('copyWith', () { + final tokenOriginData = TokenOriginData( + source: TokenOriginSourceType.manually, + data: 'data', + appName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.now(), + piServerVersion: const Version(1, 0, 0), + ); + final copy = tokenOriginData.copyWith( + source: TokenOriginSourceType.qrScan, + data: 'data2', + appName: 'appName2', + isPrivacyIdeaToken: false, + createdAt: DateTime.now().add(const Duration(days: 1)), + piServerVersion: const Version(1, 0, 1), + ); + expect(copy.source, TokenOriginSourceType.qrScan); + expect(copy.data, 'data2'); + expect(copy.appName, 'appName2'); + expect(copy.isPrivacyIdeaToken, false); + expect(copy.createdAt, isA()); + expect(copy.piServerVersion, isA()); + }); + }); + }); +} diff --git a/test/unit_test/model/version_test.dart b/test/unit_test/model/version_test.dart new file mode 100644 index 000000000..4d699f853 --- /dev/null +++ b/test/unit_test/model/version_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/version.dart'; + +void main() { + _testTokenVersion(); +} + +void _testTokenVersion() { + group('Token Version Test', () { + group('serialzation', () { + test('toJson', () { + // Arrange + const version = Version(1, 2, 3); + // Act + final result = version.toJson(); + // Assert + expect(result, {'major': 1, 'minor': 2, 'patch': 3}); + }); + test('fromJson', () { + // Arrange + const json = {'major': 1, 'minor': 2, 'patch': 3}; + // Act + final result = Version.fromJson(json); + // Assert + expect(result, const Version(1, 2, 3)); + }); + }); + }); +} diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart new file mode 100644 index 000000000..8ea234d60 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/free_otp_plus_qr_processor.dart'; + +void main() { + _testFreeOtpPlusQrProcessor(); +} + +void _testFreeOtpPlusQrProcessor() { + group('Free Otp Plus Qr Processor', () { + FreeOtpPlusQrProcessor; + test('processUri', () async { + // Arrange + final normalOtpAuthUri = Uri.parse('otpauth://totp/FreeOTP+:alice?secret=secret&issuer=FreeOTP&algorithm=SHA1&digits=6&period=30'); + // Act + final results = await const FreeOtpPlusQrProcessor().processUri(normalOtpAuthUri); + // Assert + expect(results.length, equals(1)); + expect(results.first, isA>()); + final firstResult = results.first as ProcessorResultSuccess; + expect(firstResult.resultData, isA()); + expect(firstResult.resultData.issuer, equals('FreeOTP+')); + expect(firstResult.resultData.label, equals('alice')); + expect(firstResult.resultData, isA()); + expect(firstResult.resultData.origin!.appName, equals('FreeOTP+')); + }); + test('processUri without secret', () async { + // Arrange + final normalOtpAuthUri = Uri.parse('otpauth://totp/FreeOTP+:alice?issuer=FreeOTP&algorithm=SHA1&digits=6&period=30'); + // Act + final results = await const FreeOtpPlusQrProcessor().processUri(normalOtpAuthUri); + // Assert + expect(results.length, equals(1)); + expect(results.first, isA>()); + final firstResult = results.first as ProcessorResultFailed; + expect(firstResult.message.isNotEmpty, equals(true)); + }); + test('processUri without counter', () async { + // Arrange + final normalOtpAuthUri = Uri.parse('otpauth://hotp/FreeOTP+:alice?secret=secret&issuer=FreeOTP&algorithm=SHA1&digits=6'); + // Act + final results = await const FreeOtpPlusQrProcessor().processUri(normalOtpAuthUri); + // Assert + expect(results.length, equals(1)); + expect(results.first, isA>()); + final firstResult = results.first.asSuccess!; + expect(firstResult.resultData, isA()); + expect(firstResult.resultData.issuer, equals('FreeOTP+')); + expect(firstResult.resultData.label, equals('alice')); + expect(firstResult.resultData, isA()); + expect(firstResult.resultData.origin!.appName, equals('FreeOTP+')); + final hotpToken = firstResult.resultData as HOTPToken; + expect(hotpToken.counter, equals(0)); + }); + }); +} diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart new file mode 100644 index 000000000..29eb52fdb --- /dev/null +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/token_import/token_origin_data.dart'; +import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor.dart'; +import 'package:privacyidea_authenticator/utils/token_import_origins.dart'; + +void main() { + _testGooleAuthenticatorQrProcessor(); +} + +void _testGooleAuthenticatorQrProcessor() { + group('Google Authenticator Qr Processor', () { + test('processUri', () async { + // Arrange + const processor = GoogleAuthenticatorQrProcessor(); + const uriString = + 'otpauth-migration://offline?data=ChkKCpklNznImSU3OcgSBVRlc3QxIAEoATACChsKCpklNznamSU3OdoSBVRlc3QyIAEoATABOAAQARgBIAAo8enF1vr%2F%2F%2F%2F%2FAQ%3D%3D'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(2)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.label, equals('Test1')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNotNull); + final tokenOriginData0Matcher = TokenOriginData( + source: TokenOriginSourceType.qrScanImport, + data: 'ChkKCpklNznImSU3OcgSBVRlc3QxIAEoATACChsKCpklNznamSU3OdoSBVRlc3QyIAEoATABOAAQARgBIAAo8enF1vr/////AQ==', + appName: TokenImportOrigins.googleAuthenticator.appName, + isPrivacyIdeaToken: false, + createdAt: token0.origin!.createdAt, + ); + expect(token0.origin, tokenOriginData0Matcher); + final result1 = results[1]; + expect(result1, isA()); + final token1 = result1.asSuccess!.resultData; + expect(token1.label, equals('Test2')); + expect(token1.type, equals('HOTP')); + expect(token1.origin, isNotNull); + final tokenOriginData1Matcher = TokenOriginData( + source: TokenOriginSourceType.qrScanImport, + data: 'ChkKCpklNznImSU3OcgSBVRlc3QxIAEoATACChsKCpklNznamSU3OdoSBVRlc3QyIAEoATABOAAQARgBIAAo8enF1vr/////AQ==', + appName: TokenImportOrigins.googleAuthenticator.appName, + isPrivacyIdeaToken: false, + createdAt: token1.origin!.createdAt, + ); + expect(token1.origin, tokenOriginData1Matcher); + }); + }); +} diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart new file mode 100644 index 000000000..3e111d643 --- /dev/null +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor_test.dart @@ -0,0 +1,450 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/tokens/day_password_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +import 'package:privacyidea_authenticator/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart'; + +void main() { + _testOtpAuthProcessor(); +} + +void _testOtpAuthProcessor() { + group('Otp Auth Processor Test', () { + group('TOTP', () { + test('processUri', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/account?secret=secret&issuer=issuer&algorithm=SHA256&digits=8&period=45'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final totpToken = token0 as TOTPToken; + expect(totpToken.period, equals(45)); + expect(totpToken.digits, equals(8)); + expect(totpToken.algorithm.name, equals('SHA256')); + }); + test('processUri missing algorithm', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/account?secret=secret&issuer=issuer&digits=6&period=30'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final totpToken = token0 as TOTPToken; + expect(totpToken.period, equals(30)); + expect(totpToken.digits, equals(6)); + expect(totpToken.algorithm.name, equals('SHA1')); + }); + test('processUri missing digits', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/account?secret=secret&issuer=issuer&algorithm=SHA1&period=30'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final totpToken = token0 as TOTPToken; + expect(totpToken.period, equals(30)); + expect(totpToken.digits, equals(6)); + expect(totpToken.algorithm.name, equals('SHA1')); + }); + test('processUri missing period', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/account?secret=secret&issuer=issuer&algorithm=SHA1&digits=6'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final totpToken = token0 as TOTPToken; + expect(totpToken.period, equals(30)); + expect(totpToken.digits, equals(6)); + expect(totpToken.algorithm.name, equals('SHA1')); + }); + test('processUri missing secret', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/account?issuer=issuer&algorithm=SHA1&digits=6&period=30'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final message = result0.asFailed!.message; + expect(message.toLowerCase().contains('secret'), isTrue); + }); + test('processUri issuer from path', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://totp/issuer:account?secret=secret&issuer=issuer2&algorithm=SHA1&digits=6&period=30'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final totpToken = token0 as TOTPToken; + expect(totpToken.period, equals(30)); + expect(totpToken.digits, equals(6)); + expect(totpToken.algorithm.name, equals('SHA1')); + }); + group('2step', () {}); + }); + group('HOTP', () { + test('processUri', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/account?secret=secret&issuer=issuer&algorithm=SHA256&digits=8&counter=5'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('HOTP')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.counter, equals(5)); + expect(hotpToken.digits, equals(8)); + expect(hotpToken.algorithm.name, equals('SHA256')); + }); + test('processUri missing algorithm', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/account?secret=secret&issuer=issuer&digits=8&counter=5'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('HOTP')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.counter, equals(5)); + expect(hotpToken.digits, equals(8)); + expect(hotpToken.algorithm.name, equals('SHA1')); + }); + test('processUri missing digits', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/account?secret=secret&issuer=issuer&algorithm=SHA256&counter=5'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('HOTP')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.counter, equals(5)); + expect(hotpToken.digits, equals(6)); + expect(hotpToken.algorithm.name, equals('SHA256')); + }); + test('processUri missing counter', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/account?secret=secret&issuer=issuer&algorithm=SHA256&digits=8'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('HOTP')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.counter, equals(0)); + expect(hotpToken.digits, equals(8)); + expect(hotpToken.algorithm.name, equals('SHA256')); + }); + test('processUri missing secret', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/account?issuer=issuer&algorithm=SHA256&digits=8&counter=5'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final message = result0.asFailed!.message; + expect(message.toLowerCase().contains('secret'), isTrue); + }); + test('processUri issuer from path', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://hotp/issuer:account?secret=secret&algorithm=SHA256&digits=8&counter=5'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type, equals('HOTP')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.counter, equals(5)); + expect(hotpToken.digits, equals(8)); + expect(hotpToken.algorithm.name, equals('SHA256')); + }); + + test('2step', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = + 'otpauth://hotp/issuer:account?secret=secret&algorithm=SHA256&digits=8&counter=5&2step_salt=10&2step_output=20&2step_difficulty=10000'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); // FIXME: 2step secret is currently generated by the ui so it will fail in tests + }); + }); + group('DayPassword', () { + test('processUri', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://daypassword/account?secret=secret&issuer=issuer&algorithm=SHA256&period=86400&digits=8'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type.toLowerCase(), equals('daypassword')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final dayPasswordToken = token0 as DayPasswordToken; + expect(dayPasswordToken.period, equals(const Duration(days: 1))); + expect(dayPasswordToken.digits, equals(8)); + expect(dayPasswordToken.algorithm.name, equals('SHA256')); + }); + + test('processUri missing algorithm', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://daypassword/account?secret=secret&issuer=issuer&period=86400&digits=8'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type.toLowerCase(), equals('daypassword')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final dayPasswordToken = token0 as DayPasswordToken; + expect(dayPasswordToken.period, equals(const Duration(days: 1))); + expect(dayPasswordToken.digits, equals(8)); + expect(dayPasswordToken.algorithm.name, equals('SHA1')); + }); + + test('processUri missing digits', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://daypassword/account?secret=secret&issuer=issuer&algorithm=SHA256&period=172800'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type.toLowerCase(), equals('daypassword')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final dayPasswordToken = token0 as DayPasswordToken; + expect(dayPasswordToken.period, equals(const Duration(days: 2))); + expect(dayPasswordToken.digits, equals(6)); + expect(dayPasswordToken.algorithm.name, equals('SHA256')); + }); + + test('processUri missing period', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://daypassword/account?secret=secret&issuer=issuer&algorithm=SHA256&digits=8'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('issuer')); + expect(token0.label, equals('account')); + expect(token0.type.toLowerCase(), equals('daypassword')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final dayPasswordToken = token0 as DayPasswordToken; + expect(dayPasswordToken.period, equals(const Duration(days: 1))); + expect(dayPasswordToken.digits, equals(8)); + expect(dayPasswordToken.algorithm.name, equals('SHA256')); + }); + + test('processUri missing secret', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = 'otpauth://daypassword/account?issuer=issuer&algorithm=SHA256&period=86400&digits=8'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final message = result0.asFailed!.message; + expect(message.toLowerCase().contains('secret'), isTrue); + }); + }); + group('Push Token', () { + test('processUri', () async { + // Arrange + const processor = OtpAuthProcessor(); + const uriString = + 'otpauth://pipush/PIPU0000D79E?url=https%3A//123.456.78.9/ttype/push&ttl=10&issuer=privacyIDEA&enrollment_credential=3342826741eb64e8f94e01920a88745bccdecd9e&v=1&serial=PIPU0000D79E&sslverify=0'; + final uri = Uri.parse(uriString); + // Act + final results = await processor.processUri(uri); + // Assert + expect(results.length, equals(1)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.issuer, equals('privacyIDEA')); + expect(token0.label, equals('PIPU0000D79E')); + expect(token0.type.toLowerCase(), equals('pipush')); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, isNull); + expect(token0.origin!.isPrivacyIdeaToken, isNull); + expect(token0.origin!.data, equals(uriString)); + final pushToken = token0 as PushToken; + expect(pushToken.url, equals(Uri.parse('https://123.456.78.9/ttype/push'))); + expect(pushToken.expirationDate, isNotNull); + // DateTimes.now() are never the same + // So we check the difference in minutes and allow a 5 second difference (9:59 => 9 minutes, 10:04 => 10 minutes) + expect(pushToken.expirationDate!.difference(DateTime.now().subtract(const Duration(seconds: 5))).inMinutes, equals(10)); + expect(pushToken.serial, equals('PIPU0000D79E')); + expect(pushToken.sslVerify, isFalse); + }); + }); + }); +} diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart new file mode 100644 index 000000000..2ed307def --- /dev/null +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/privacyidea_authenticator_qr_processor_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + _testPrivacyideaAuthenticatorQrProcessor(); +} + +void _testPrivacyideaAuthenticatorQrProcessor() { + group('Privacyidea Authenticator Qr Processor test', () { + test('', () { + // TODO: implement test when its sure that the functionallity will not change anymore + }); + }); +} diff --git a/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart new file mode 100644 index 000000000..987b4ceaa --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart @@ -0,0 +1,182 @@ +import 'dart:convert'; + +import 'package:camera/camera.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +import 'package:privacyidea_authenticator/processors/token_import_file_processor/aegis_import_file_processor.dart'; +import 'package:privacyidea_authenticator/processors/token_import_file_processor/two_fas_import_file_processor.dart'; + +void main() { + _testAegisImportFileProcessor(); +} + +void _testAegisImportFileProcessor() { + group('Aegis Import File Processor test', () { + group('version 3', () { + group('import json', () { + test('plain', () async { + // Arrange + const byteDataString = + '[123, 10, 32, 32, 32, 32, 34, 118, 101, 114, 115, 105, 111, 110, 34, 58, 32, 49, 44, 10, 32, 32, 32, 32, 34, 104, 101, 97, 100, 101, 114, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 108, 111, 116, 115, 34, 58, 32, 110, 117, 108, 108, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 112, 97, 114, 97, 109, 115, 34, 58, 32, 110, 117, 108, 108, 10, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 34, 100, 98, 34, 58, 32, 123,' + '10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 118, 101, 114, 115, 105, 111, 110, 34, 58, 32, 51, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 101, 110, 116, 114, 105, 101, 115, 34, 58, 32, 91, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 121, 112, 101, 34, 58, 32, 34, 116, 111, 116, 112, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 34, 117, 117, 105, 100, 34, 58, 32, 34, 99, 52, 57, 51, 102, 50, 52, 97, 45, 48, 54, 102, 55, 45, 52, 54, 57, 51, 45, 57, 100, 98, 102, 45, 53, 50, 53, 102, 56, 49, 54, 54, 102, 57, 100, 97, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 97, 109, 101, 34, 58, 32, 34, 84, 101, 115, 116, 49, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 34, 105, 115, 115, 117, 101, 114, 34, 58, 32, 34, 84, 101, 115, 116, 105, 110, 103, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 111, 116, 101, 34, 58, 32, 34, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 102, 97, 118, 111, 114, 105, 116, 101, 34, 58, 32, 102, 97, 108, 115, 101, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 34, 105, 99, 111, 110, 34, 58, 32, 110, 117, 108, 108, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 110, 102, 111, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 101, 99, 114, 101, 116, 34, 58, 32, 34, 65, 65, 65, 65, 65, 65, 65, 65, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 34, 97, 108, 103, 111, 34, 58, 32, 34, 83, 72, 65, 49, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 100, 105, 103, 105, 116, 115, 34, 58, 32, 54, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 112, 101, 114, 105, 111, 100, 34, 58, 32, 51, 48, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 103, 114, 111, 117, 112, 115, 34, 58, 32, 91, 93, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 121, 112, 101, 34, 58, 32, 34, 104, 111, 116, 112, 34, 44, 10, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 117, 117, 105, 100, 34, 58, 32, 34, 50, 48, 55, 102, 49, 49, 102, 48, 45, 54, 100, 101, 52, 45, 52, 97, 52, 99, 45, 57, 97, 98, 97, 45, 57, 55, 98, 50, 55, 55, 102, 101, 54, 48, 56, 97, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 97, 109, 101, 34, 58, 32, 34, 84, 101, 115, 116, 50, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 115, 115, 117, 101, 114, 34, 58, 32, 34, 84, 101, 115, 116, 105, 110, 103, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 111, 116, 101, 34, 58, 32, 34, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 102, 97, 118, 111, 114, 105, 116, 101, 34, 58, 32, 102, 97, 108, 115, 101, 44, 10, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 99, 111, 110, 34, 58, 32, 110, 117, 108, 108, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 110, 102, 111, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 101, 99, 114, 101, 116, 34, 58, 32, 34, 65, 65, 65, 65, 65, 65, 65, 65, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 97, 108, 103, 111, 34, 58, 32, 34, 83, 72, 65, 49, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 100, 105, 103, 105, 116, 115, 34, 58, 32, 54, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 99, 111, 117, 110, 116, 101, 114, 34, 58, 32, 48, 10, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 103, 114, 111, 117, 112, 115, 34, 58, 32, 91, 93, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 93, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 103, 114, 111, 117, 112, 115, 34, 58, 32, 91, 93, 10, 32, 32, 32, 32, 125, 10, 125]'; + final byteData = ByteData.view(Uint8List.fromList((jsonDecode(byteDataString) as List).cast()).buffer); + + const aegisImportFileProcessor = AegisImportFileProcessor(); + final XFile file = XFile.fromData(byteData.buffer.asUint8List(), name: 'aegis_plain.json'); + // Act + final isValid = await aegisImportFileProcessor.fileIsValid(file); + final results = await aegisImportFileProcessor.processFile(file); + // Assert + expect(isValid, isTrue); + expect(results.length, equals(2)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.label, equals('Test1')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNotNull); + final totpToken = token0 as TOTPToken; + expect(totpToken.digits, equals(6)); + expect(totpToken.algorithm.name, equals('SHA1')); + expect(totpToken.period, equals(30)); + expect(totpToken.secret, equals('AAAAAAAA')); + expect(totpToken.issuer, equals('Testing')); + expect(totpToken.otpFromTime(DateTime.fromMillisecondsSinceEpoch(1713352639317)), equals('220975')); + final result1 = results[1]; + expect(result1, isA()); + final token1 = result1.asSuccess!.resultData; + expect(token1.label, equals('Test2')); + expect(token1.type, equals('HOTP')); + expect(token1.origin, isNotNull); + final hotpToken = token1 as HOTPToken; + expect(hotpToken.digits, equals(6)); + expect(hotpToken.algorithm.name, equals('SHA1')); + expect(hotpToken.counter, equals(0)); + expect(hotpToken.secret, equals('AAAAAAAA')); + expect(hotpToken.issuer, equals('Testing')); + expect(hotpToken.otpValue, equals('328482')); + }); + + test('encrypted', () async { + // Arrange + const encryptedBytesString = + '[123, 10, 32, 32, 32, 32, 34, 118, 101, 114, 115, 105, 111, 110, 34, 58, 32, 49, 44, 10, 32, 32, 32, 32, 34, 104, 101, 97, 100, 101, 114, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 108, 111, 116, 115, 34, 58, 32, 91, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 121, 112, 101, 34, 58, 32, 49, 44, 10, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 117, 117, 105, 100, 34, 58, 32, 34, 99, 101, 53, 50, 101, 98, 99, 56, 45, 53, 56, 53, 54, 45, 52, 52, 100, 52, 45, 97, 99, 49, 49, 45, 102, 102, 54, 48, 49, 57, 57, 53, 48, 101, 98, 102, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 107, 101, 121, 34, 58, 32, 34, 54, 48, 102, 51, 101, 99, 102, 101, 57, 57, 54, 53, 55, 54, 55, 98,' + '97, 49, 53, 51, 53, 50, 49, 49, 48, 99, 50, 54, 56, 101, 53, 56, 48, 50, 53, 53, 56, 53, 101, 54, 52, 98, 57, 99, 54, 99, 53, 98, 53, 99, 97, 97, 54, 98, 101, 52, 56, 98, 53, 98, 101, 98, 57, 50, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 107, 101, 121, 95, 112, 97, 114, 97, 109, 115, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 34, 110, 111, 110, 99, 101, 34, 58, 32, 34, 98, 56, 100, 50, 54, 97, 48, 53, 99, 57, 51, 49, 56, 54, 99, 57, 55, 52, 98, 55, 49, 54, 99, 51, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 97, 103, 34, 58, 32, 34, 51, 56, 99, 49, 102, 48, 50, 100, 57, 48, 56, 51, 49, 50, 55, 56, 99, 53, 55, 56, 48, 100, 54, 53, 54, 97, 56, 99, 53, 50, 48, 97,' + '34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 34, 58, 32, 51, 50, 55, 54, 56, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 114, 34, 58, 32, 56, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 112, 34, 58, 32, 49, 44, 10, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 97, 108, 116, 34, 58, 32, 34, 99, 51, 49, 57, 49, 57, 100, 53, 97, 50, 57, 48, 54, 102, 50, 54, 51, 57, 97, 51, 52, 50, 50, 99, 49, 53, 102, 102, 52, 52, 100, 51, 101, 55, 50, 50, 56, 53, 101, 97, 52, 54, 98, 48, 48, 97, 51, 48, 48, 57, 55, 52, 100, 57, 102, 98, 54, 99, 100, 97, 97, 48, 101, 100, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 34, 114, 101, 112, 97, 105, 114, 101, 100, 34, 58, 32, 116, 114, 117, 101, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 115, 95, 98, 97, 99, 107, 117, 112, 34, 58, 32, 102, 97, 108, 115, 101, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 93, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 112, 97, 114, 97, 109, 115, 34, 58,' + '32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 111, 110, 99, 101, 34, 58, 32, 34, 48, 57, 102, 48, 53, 54, 52, 49, 48, 50, 55, 49, 102, 50, 99, 50, 52, 97, 51, 51, 100, 52, 99, 54, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 97, 103, 34, 58, 32, 34, 51, 48, 54, 53, 54, 100, 102, 50, 102, 54, 97, 49, 97, 100, 99, 48, 99, 56, 51, 98, 99, 97, 55, 57, 99, 101, 101, 97,' + '57, 99, 100, 54, 34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 34, 100, 98, 34, 58, 32, 34, 82, 89, 75, 89, 119, 107, 119, 100, 71, 121, 54, 76, 49, 54, 85, 87, 119, 116, 68, 84, 57, 48, 67, 102, 100, 81, 77, 119, 97, 105, 115, 72, 78, 70, 99, 51, 116, 103, 103, 87, 87, 56, 65, 79, 84, 113, 118, 51, 118, 106, 121, 52, 74, 56, 50, 52, 107, 87, 66, 108, 108, 105, 77, 50, 65, 85,' + '48, 92, 47, 53, 98, 116, 108, 108, 86, 106, 78, 77, 121, 74, 100, 97, 79, 86, 79, 98, 98, 121, 85, 90, 52, 106, 78, 117, 118, 72, 106, 114, 79, 97, 99, 73, 92, 47, 121, 97, 51, 88, 70, 54, 85, 48, 113, 43, 87, 89, 68, 111, 70, 97, 110, 57, 116, 110, 78, 56, 83, 48, 78, 78, 97, 66, 67, 102, 82, 87, 72, 66, 66, 118, 119, 83, 118, 77, 118, 112, 92, 47, 98, 109, 43, 66, 76, 113, 69, 56, 92, 47, 108, 82, 72, 74, 81, 119, 102, 54,' + '71, 49, 83, 49, 106, 103, 56, 110, 84, 122, 108, 120, 113, 54, 53, 52, 109, 121, 103, 115, 122, 118, 111, 72, 80, 65, 97, 52, 76, 99, 83, 99, 48, 77, 103, 50, 87, 48, 109, 53, 99, 103, 81, 112, 69, 73, 111, 51, 73, 111, 88, 113, 104, 66, 87, 101, 67, 85, 55, 70, 70, 68, 87, 102, 88, 120, 86, 104, 117, 56, 99, 72, 69, 105, 112, 114, 86, 73, 103, 43, 73, 48, 83, 74, 82, 119, 98, 117, 78, 102, 88, 107, 106, 105, 75, 122, 67, 116, 52, 111,' + '74, 119, 67, 85, 113, 111, 48, 88, 84, 57, 84, 121, 88, 87, 110, 75, 89, 51, 108, 70, 73, 107, 101, 77, 111, 101, 90, 114, 84, 100, 103, 122, 105, 118, 79, 111, 52, 54, 87, 80, 52, 76, 52, 67, 99, 74, 92, 47, 69, 110, 101, 73, 85, 83, 107, 100, 100, 73, 90, 109, 55, 106, 106, 88, 112, 70, 92, 47, 82, 112, 120, 106, 53, 107, 112, 67, 89, 75, 74, 54, 50, 69, 89, 54, 89, 108, 79, 88, 99, 110, 74, 78, 88, 81, 92, 47, 100, 86, 51, 75,' + '66, 101, 112, 119, 51, 104, 69, 97, 113, 109, 116, 81, 43, 82, 75, 98, 102, 108, 111, 108, 69, 77, 106, 50, 80, 65, 120, 112, 114, 43, 104, 118, 79, 107, 112, 105, 69, 90, 86, 48, 113, 120, 79, 48, 43, 98, 70, 67, 66, 108, 120, 107, 78, 74, 50, 110, 99, 49, 110, 117, 82, 99, 88, 102, 73, 49, 56, 121, 75, 112, 56, 82, 51, 99, 69, 75, 43, 105, 99, 90, 119, 67, 54, 65, 114, 100, 120, 107, 101, 116, 76, 49, 122, 114, 80, 103, 68, 117, 67, 100,' + '122, 54, 53, 108, 111, 43, 90, 69, 112, 118, 81, 116, 73, 78, 92, 47, 67, 115, 80, 69, 113, 118, 73, 89, 72, 55, 81, 55, 114, 111, 112, 112, 87, 101, 79, 105, 122, 80, 80, 120, 84, 118, 99, 98, 72, 57, 99, 80, 122, 50, 105, 76, 53, 104, 107, 105, 55, 48, 116, 97, 106, 85, 120, 113, 88, 103, 80, 120, 68, 116, 68, 113, 43, 78, 50, 120, 48, 92, 47, 103, 71, 100, 99, 105, 65, 118, 99, 98, 116, 97, 52, 55, 99, 74, 119, 88, 108, 76, 50, 67,' + '97, 67, 69, 87, 116, 97, 89, 98, 54, 87, 54, 108, 112, 120, 67, 103, 57, 66, 54, 82, 56, 77, 43, 90, 73, 79, 115, 89, 83, 81, 114, 77, 117, 97, 99, 80, 51, 51, 90, 101, 76, 118, 97, 113, 99, 83, 80, 69, 115, 67, 78, 78, 107, 107, 122, 108, 71, 75, 90, 75, 100, 49, 54, 115, 90, 49, 43, 55, 112, 99, 69, 121, 76, 119, 68, 57, 74, 55, 107, 85, 115, 85, 80, 72, 88, 69, 53, 56, 101, 92, 47, 111, 97, 77, 68, 53, 109, 104, 85, 100,' + '81, 66, 99, 66, 78, 112, 107, 101, 49, 50, 56, 73, 89, 109, 49, 51, 67, 85, 83, 119, 82, 120, 116, 85, 66, 48, 111, 98, 87, 87, 106, 120, 111, 102, 80, 115, 55, 54, 114, 89, 112, 84, 112, 75, 87, 103, 98, 99, 48, 118, 118, 104, 57, 66, 99, 43, 84, 100, 82, 101, 117, 51, 79, 88, 105, 80, 121, 92, 47, 111, 73, 73, 79, 117, 122, 53, 115, 92, 47, 54, 109, 56, 89, 106, 48, 48, 70, 74, 79, 67, 113, 110, 89, 108, 78, 100, 69, 99, 88, 107,' + '90, 113, 89, 74, 97, 92, 47, 50, 54, 76, 85, 121, 54, 120, 69, 89, 97, 84, 78, 111, 109, 99, 89, 67, 73, 89, 56, 84, 92, 47, 112, 87, 116, 54, 97, 92, 47, 109, 111, 108, 89, 69, 52, 122, 82, 108, 80, 55, 71, 68, 88, 48, 104, 76, 51, 114, 51, 98, 84, 100, 97, 52, 120, 106, 57, 81, 112, 109, 110, 49, 100, 110, 116, 92, 47, 118, 101, 119, 116, 84, 103, 72, 100, 118, 100, 86, 97, 108, 120, 52, 75, 116, 105, 97, 86, 54, 107, 120, 80, 98,' + '109, 110, 81, 121, 76, 79, 111, 86, 73, 121, 106, 70, 57, 92, 47, 56, 67, 48, 67, 106, 75, 109, 111, 85, 70, 100, 78, 89, 102, 68, 76, 66, 114, 122, 74, 56, 43, 80, 67, 103, 84, 71, 51, 88, 100, 75, 101, 51, 43, 110, 100, 51, 119, 50, 100, 121, 77, 121, 78, 69, 70, 104, 114, 50, 120, 99, 112, 74, 121, 116, 120, 69, 92, 47, 65, 57, 101, 108, 77, 77, 121, 74, 82, 103, 77, 51, 82, 120, 83, 98, 82, 84, 98, 89, 112, 109, 99, 102, 101, 54,' + '72, 50, 100, 48, 110, 113, 118, 83, 97, 82, 110, 51, 98, 49, 57, 84, 48, 121, 49, 43, 57, 68, 67, 49, 113, 86, 78, 79, 102, 109, 109, 67, 66, 90, 43, 101, 87, 86, 68, 50, 78, 110, 107, 88, 81, 68, 110, 115, 67, 78, 71, 50, 104, 76, 52, 101, 73, 75, 117, 81, 97, 122, 68, 81, 112, 53, 86, 74, 104, 114, 100, 122, 105, 121, 106, 79, 68, 105, 85, 57, 78, 86, 77, 67, 70, 65, 68, 52, 76, 106, 102, 74, 79, 100, 116, 98, 122, 87, 78, 112,' + '67, 100, 79, 109, 56, 66, 108, 86, 74, 121, 97, 56, 86, 48, 84, 81, 65, 100, 118, 54, 104, 70, 50, 119, 112, 65, 83, 54, 86, 92, 47, 104, 107, 109, 71, 101, 106, 103, 65, 57, 101, 108, 118, 118, 50, 82, 113, 97, 78, 90, 84, 79, 102, 103, 102, 80, 108, 110, 113, 74, 113, 81, 51, 102, 98, 81, 99, 116, 122, 72, 66, 118, 98, 114, 107, 100, 77, 87, 86, 76, 102, 108, 102, 49, 103, 92, 47, 90, 121, 115, 69, 97, 106, 66, 75, 121, 108, 43, 107, 57,' + '115, 82, 102, 85, 76, 120, 112, 72, 116, 77, 70, 54, 86, 109, 117, 120, 52, 87, 51, 115, 43, 77, 88, 102, 67, 72, 66, 66, 71, 81, 112, 90, 105, 117, 48, 51, 98, 121, 86, 116, 101, 86, 111, 104, 68, 78, 97, 108, 52, 77, 70, 81, 117, 72, 52, 72, 66, 52, 106, 55, 71, 118, 99, 98, 113, 120, 56, 122, 56, 120, 73, 90, 113, 90, 104, 75, 73, 50, 111, 82, 97, 86, 81, 106, 53, 69, 48, 68, 56, 71, 85, 110, 74, 84, 49, 81, 100, 86, 65, 69,' + '43, 77, 86, 75, 65, 78, 74, 68, 109, 104, 84, 56, 86, 111, 34, 10, 125]'; + + final byteData = ByteData.view(Uint8List.fromList((jsonDecode(encryptedBytesString) as List).cast()).buffer); + + const aegisImportFileProcessor = AegisImportFileProcessor(); + final XFile file = XFile.fromData(byteData.buffer.asUint8List(), name: 'aegis_encrypted.json'); + // Act + final isValid = await aegisImportFileProcessor.fileIsValid(file); + final fileNeedsPassword = await aegisImportFileProcessor.fileNeedsPassword(file); + final results = await aegisImportFileProcessor.processFile(file, password: 'test123'); + // Assert + expect(isValid, isTrue); + expect(fileNeedsPassword, isTrue); + expect(results.length, equals(2)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.label, equals('Test1')); + expect(token0.type, equals('TOTP')); + expect(token0.origin, isNotNull); + final totpToken = token0 as TOTPToken; + expect(totpToken.digits, equals(6)); + expect(totpToken.algorithm.name, equals('SHA1')); + expect(totpToken.period, equals(30)); + expect(totpToken.secret, equals('AAAAAAAA')); + expect(totpToken.issuer, equals('Testing')); + expect(totpToken.otpFromTime(DateTime.fromMillisecondsSinceEpoch(1713352639317)), equals('220975')); + final result1 = results[1]; + expect(result1, isA()); + final token1 = result1.asSuccess!.resultData; + expect(token1.label, equals('Test2')); + expect(token1.type, equals('HOTP')); + expect(token1.origin, isNotNull); + final hotpToken = token1 as HOTPToken; + expect(hotpToken.digits, equals(6)); + expect(hotpToken.algorithm.name, equals('SHA1')); + expect(hotpToken.counter, equals(0)); + expect(hotpToken.secret, equals('AAAAAAAA')); + expect(hotpToken.issuer, equals('Testing')); + expect(hotpToken.otpValue, equals('328482')); + }); + + test('exncrypted bad password', () async { + // Arrange + const encryptedBytesString = + '[123, 10, 32, 32, 32, 32, 34, 118, 101, 114, 115, 105, 111, 110, 34, 58, 32, 49, 44, 10, 32, 32, 32, 32, 34, 104, 101, 97, 100, 101, 114, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 108, 111, 116, 115, 34, 58, 32, 91, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 121, 112, 101, 34, 58, 32, 49, 44, 10, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 117, 117, 105, 100, 34, 58, 32, 34, 99, 101, 53, 50, 101, 98, 99, 56, 45, 53, 56, 53, 54, 45, 52, 52, 100, 52, 45, 97, 99, 49, 49, 45, 102, 102, 54, 48, 49, 57, 57, 53, 48, 101, 98, 102, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 107, 101, 121, 34, 58, 32, 34, 54, 48, 102, 51, 101, 99, 102, 101, 57, 57, 54, 53, 55, 54, 55, 98,' + '97, 49, 53, 51, 53, 50, 49, 49, 48, 99, 50, 54, 56, 101, 53, 56, 48, 50, 53, 53, 56, 53, 101, 54, 52, 98, 57, 99, 54, 99, 53, 98, 53, 99, 97, 97, 54, 98, 101, 52, 56, 98, 53, 98, 101, 98, 57, 50, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 107, 101, 121, 95, 112, 97, 114, 97, 109, 115, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 34, 110, 111, 110, 99, 101, 34, 58, 32, 34, 98, 56, 100, 50, 54, 97, 48, 53, 99, 57, 51, 49, 56, 54, 99, 57, 55, 52, 98, 55, 49, 54, 99, 51, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 97, 103, 34, 58, 32, 34, 51, 56, 99, 49, 102, 48, 50, 100, 57, 48, 56, 51, 49, 50, 55, 56, 99, 53, 55, 56, 48, 100, 54, 53, 54, 97, 56, 99, 53, 50, 48, 97,' + '34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 34, 58, 32, 51, 50, 55, 54, 56, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 114, 34, 58, 32, 56, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 112, 34, 58, 32, 49, 44, 10, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 97, 108, 116, 34, 58, 32, 34, 99, 51, 49, 57, 49, 57, 100, 53, 97, 50, 57, 48, 54, 102, 50, 54, 51, 57, 97, 51, 52, 50, 50, 99, 49, 53, 102, 102, 52, 52, 100, 51, 101, 55, 50, 50, 56, 53, 101, 97, 52, 54, 98, 48, 48, 97, 51, 48, 48, 57, 55, 52, 100, 57, 102, 98, 54, 99, 100, 97, 97, 48, 101, 100, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 34, 114, 101, 112, 97, 105, 114, 101, 100, 34, 58, 32, 116, 114, 117, 101, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 115, 95, 98, 97, 99, 107, 117, 112, 34, 58, 32, 102, 97, 108, 115, 101, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 93, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 112, 97, 114, 97, 109, 115, 34, 58,' + '32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 110, 111, 110, 99, 101, 34, 58, 32, 34, 48, 57, 102, 48, 53, 54, 52, 49, 48, 50, 55, 49, 102, 50, 99, 50, 52, 97, 51, 51, 100, 52, 99, 54, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 97, 103, 34, 58, 32, 34, 51, 48, 54, 53, 54, 100, 102, 50, 102, 54, 97, 49, 97, 100, 99, 48, 99, 56, 51, 98, 99, 97, 55, 57, 99, 101, 101, 97,' + '57, 99, 100, 54, 34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 34, 100, 98, 34, 58, 32, 34, 82, 89, 75, 89, 119, 107, 119, 100, 71, 121, 54, 76, 49, 54, 85, 87, 119, 116, 68, 84, 57, 48, 67, 102, 100, 81, 77, 119, 97, 105, 115, 72, 78, 70, 99, 51, 116, 103, 103, 87, 87, 56, 65, 79, 84, 113, 118, 51, 118, 106, 121, 52, 74, 56, 50, 52, 107, 87, 66, 108, 108, 105, 77, 50, 65, 85,' + '48, 92, 47, 53, 98, 116, 108, 108, 86, 106, 78, 77, 121, 74, 100, 97, 79, 86, 79, 98, 98, 121, 85, 90, 52, 106, 78, 117, 118, 72, 106, 114, 79, 97, 99, 73, 92, 47, 121, 97, 51, 88, 70, 54, 85, 48, 113, 43, 87, 89, 68, 111, 70, 97, 110, 57, 116, 110, 78, 56, 83, 48, 78, 78, 97, 66, 67, 102, 82, 87, 72, 66, 66, 118, 119, 83, 118, 77, 118, 112, 92, 47, 98, 109, 43, 66, 76, 113, 69, 56, 92, 47, 108, 82, 72, 74, 81, 119, 102, 54,' + '71, 49, 83, 49, 106, 103, 56, 110, 84, 122, 108, 120, 113, 54, 53, 52, 109, 121, 103, 115, 122, 118, 111, 72, 80, 65, 97, 52, 76, 99, 83, 99, 48, 77, 103, 50, 87, 48, 109, 53, 99, 103, 81, 112, 69, 73, 111, 51, 73, 111, 88, 113, 104, 66, 87, 101, 67, 85, 55, 70, 70, 68, 87, 102, 88, 120, 86, 104, 117, 56, 99, 72, 69, 105, 112, 114, 86, 73, 103, 43, 73, 48, 83, 74, 82, 119, 98, 117, 78, 102, 88, 107, 106, 105, 75, 122, 67, 116, 52, 111,' + '74, 119, 67, 85, 113, 111, 48, 88, 84, 57, 84, 121, 88, 87, 110, 75, 89, 51, 108, 70, 73, 107, 101, 77, 111, 101, 90, 114, 84, 100, 103, 122, 105, 118, 79, 111, 52, 54, 87, 80, 52, 76, 52, 67, 99, 74, 92, 47, 69, 110, 101, 73, 85, 83, 107, 100, 100, 73, 90, 109, 55, 106, 106, 88, 112, 70, 92, 47, 82, 112, 120, 106, 53, 107, 112, 67, 89, 75, 74, 54, 50, 69, 89, 54, 89, 108, 79, 88, 99, 110, 74, 78, 88, 81, 92, 47, 100, 86, 51, 75,' + '66, 101, 112, 119, 51, 104, 69, 97, 113, 109, 116, 81, 43, 82, 75, 98, 102, 108, 111, 108, 69, 77, 106, 50, 80, 65, 120, 112, 114, 43, 104, 118, 79, 107, 112, 105, 69, 90, 86, 48, 113, 120, 79, 48, 43, 98, 70, 67, 66, 108, 120, 107, 78, 74, 50, 110, 99, 49, 110, 117, 82, 99, 88, 102, 73, 49, 56, 121, 75, 112, 56, 82, 51, 99, 69, 75, 43, 105, 99, 90, 119, 67, 54, 65, 114, 100, 120, 107, 101, 116, 76, 49, 122, 114, 80, 103, 68, 117, 67, 100,' + '122, 54, 53, 108, 111, 43, 90, 69, 112, 118, 81, 116, 73, 78, 92, 47, 67, 115, 80, 69, 113, 118, 73, 89, 72, 55, 81, 55, 114, 111, 112, 112, 87, 101, 79, 105, 122, 80, 80, 120, 84, 118, 99, 98, 72, 57, 99, 80, 122, 50, 105, 76, 53, 104, 107, 105, 55, 48, 116, 97, 106, 85, 120, 113, 88, 103, 80, 120, 68, 116, 68, 113, 43, 78, 50, 120, 48, 92, 47, 103, 71, 100, 99, 105, 65, 118, 99, 98, 116, 97, 52, 55, 99, 74, 119, 88, 108, 76, 50, 67,' + '97, 67, 69, 87, 116, 97, 89, 98, 54, 87, 54, 108, 112, 120, 67, 103, 57, 66, 54, 82, 56, 77, 43, 90, 73, 79, 115, 89, 83, 81, 114, 77, 117, 97, 99, 80, 51, 51, 90, 101, 76, 118, 97, 113, 99, 83, 80, 69, 115, 67, 78, 78, 107, 107, 122, 108, 71, 75, 90, 75, 100, 49, 54, 115, 90, 49, 43, 55, 112, 99, 69, 121, 76, 119, 68, 57, 74, 55, 107, 85, 115, 85, 80, 72, 88, 69, 53, 56, 101, 92, 47, 111, 97, 77, 68, 53, 109, 104, 85, 100,' + '81, 66, 99, 66, 78, 112, 107, 101, 49, 50, 56, 73, 89, 109, 49, 51, 67, 85, 83, 119, 82, 120, 116, 85, 66, 48, 111, 98, 87, 87, 106, 120, 111, 102, 80, 115, 55, 54, 114, 89, 112, 84, 112, 75, 87, 103, 98, 99, 48, 118, 118, 104, 57, 66, 99, 43, 84, 100, 82, 101, 117, 51, 79, 88, 105, 80, 121, 92, 47, 111, 73, 73, 79, 117, 122, 53, 115, 92, 47, 54, 109, 56, 89, 106, 48, 48, 70, 74, 79, 67, 113, 110, 89, 108, 78, 100, 69, 99, 88, 107,' + '90, 113, 89, 74, 97, 92, 47, 50, 54, 76, 85, 121, 54, 120, 69, 89, 97, 84, 78, 111, 109, 99, 89, 67, 73, 89, 56, 84, 92, 47, 112, 87, 116, 54, 97, 92, 47, 109, 111, 108, 89, 69, 52, 122, 82, 108, 80, 55, 71, 68, 88, 48, 104, 76, 51, 114, 51, 98, 84, 100, 97, 52, 120, 106, 57, 81, 112, 109, 110, 49, 100, 110, 116, 92, 47, 118, 101, 119, 116, 84, 103, 72, 100, 118, 100, 86, 97, 108, 120, 52, 75, 116, 105, 97, 86, 54, 107, 120, 80, 98,' + '109, 110, 81, 121, 76, 79, 111, 86, 73, 121, 106, 70, 57, 92, 47, 56, 67, 48, 67, 106, 75, 109, 111, 85, 70, 100, 78, 89, 102, 68, 76, 66, 114, 122, 74, 56, 43, 80, 67, 103, 84, 71, 51, 88, 100, 75, 101, 51, 43, 110, 100, 51, 119, 50, 100, 121, 77, 121, 78, 69, 70, 104, 114, 50, 120, 99, 112, 74, 121, 116, 120, 69, 92, 47, 65, 57, 101, 108, 77, 77, 121, 74, 82, 103, 77, 51, 82, 120, 83, 98, 82, 84, 98, 89, 112, 109, 99, 102, 101, 54,' + '72, 50, 100, 48, 110, 113, 118, 83, 97, 82, 110, 51, 98, 49, 57, 84, 48, 121, 49, 43, 57, 68, 67, 49, 113, 86, 78, 79, 102, 109, 109, 67, 66, 90, 43, 101, 87, 86, 68, 50, 78, 110, 107, 88, 81, 68, 110, 115, 67, 78, 71, 50, 104, 76, 52, 101, 73, 75, 117, 81, 97, 122, 68, 81, 112, 53, 86, 74, 104, 114, 100, 122, 105, 121, 106, 79, 68, 105, 85, 57, 78, 86, 77, 67, 70, 65, 68, 52, 76, 106, 102, 74, 79, 100, 116, 98, 122, 87, 78, 112,' + '67, 100, 79, 109, 56, 66, 108, 86, 74, 121, 97, 56, 86, 48, 84, 81, 65, 100, 118, 54, 104, 70, 50, 119, 112, 65, 83, 54, 86, 92, 47, 104, 107, 109, 71, 101, 106, 103, 65, 57, 101, 108, 118, 118, 50, 82, 113, 97, 78, 90, 84, 79, 102, 103, 102, 80, 108, 110, 113, 74, 113, 81, 51, 102, 98, 81, 99, 116, 122, 72, 66, 118, 98, 114, 107, 100, 77, 87, 86, 76, 102, 108, 102, 49, 103, 92, 47, 90, 121, 115, 69, 97, 106, 66, 75, 121, 108, 43, 107, 57,' + '115, 82, 102, 85, 76, 120, 112, 72, 116, 77, 70, 54, 86, 109, 117, 120, 52, 87, 51, 115, 43, 77, 88, 102, 67, 72, 66, 66, 71, 81, 112, 90, 105, 117, 48, 51, 98, 121, 86, 116, 101, 86, 111, 104, 68, 78, 97, 108, 52, 77, 70, 81, 117, 72, 52, 72, 66, 52, 106, 55, 71, 118, 99, 98, 113, 120, 56, 122, 56, 120, 73, 90, 113, 90, 104, 75, 73, 50, 111, 82, 97, 86, 81, 106, 53, 69, 48, 68, 56, 71, 85, 110, 74, 84, 49, 81, 100, 86, 65, 69,' + '43, 77, 86, 75, 65, 78, 74, 68, 109, 104, 84, 56, 86, 111, 34, 10, 125]'; + + final byteData = ByteData.view(Uint8List.fromList((jsonDecode(encryptedBytesString) as List).cast()).buffer); + + const aegisImportFileProcessor = AegisImportFileProcessor(); + final XFile file = XFile.fromData(byteData.buffer.asUint8List(), name: 'aegis_encrypted.json'); + // Act/Assert + expect(() async => await aegisImportFileProcessor.processFile(file, password: 'wrongPassword'), throwsA(isA())); + }); + }); + group('import HTML', () { + // Unimplemented + }); + + group('import TXT', () { + // Unimplemented + }); + }); + }); +} diff --git a/test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart new file mode 100644 index 000000000..bdedc5ef2 --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart @@ -0,0 +1,209 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/enums/token_types.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +import 'package:privacyidea_authenticator/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart'; +import 'package:privacyidea_authenticator/utils/token_import_origins.dart'; + +void main() { + _testAuthenticatorProImportFileProcessor(); +} + +void _assertSuccessResults(List> results) { + expect(results.length, equals(2)); + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.label, 'Test1'); + expect(token0.issuer, 'Test1'); + expect(token0.type, TokenTypes.TOTP.name); + expect(token0, isA()); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, TokenImportOrigins.authenticatorPro.appName); + expect(token0.origin!.source, TokenOriginSourceType.backupFile); + final totpToken = token0 as TOTPToken; + expect(totpToken.secret, 'AAAAAAAA'); + expect(totpToken.algorithm, Algorithms.SHA1); + expect(totpToken.digits, 6); + expect(totpToken.period, 30); + final result1 = results[1]; + expect(result1, isA()); + final token1 = result1.asSuccess!.resultData; + expect(token1.label, 'Test2'); + expect(token1.issuer, 'Test2'); + expect(token1.type, TokenTypes.HOTP.name); + expect(token1, isA()); + expect(token1.origin, isNotNull); + expect(token1.origin!.appName, TokenImportOrigins.authenticatorPro.appName); + expect(token1.origin!.source, TokenOriginSourceType.backupFile); + final hotpToken = token1 as HOTPToken; + expect(hotpToken.secret, 'BBBBBBBB'); + expect(hotpToken.algorithm, Algorithms.SHA1); + expect(hotpToken.digits, 6); + expect(hotpToken.counter, 0); +} + +void _testAuthenticatorProImportFileProcessor() { + group('Authenticator ProImport File Processor Test', () { + const processor = AuthenticatorProImportFileProcessor(); + group('json', () { + group('encryped', () {}); + group('plain', () { + // Arrange + const byteDataString = + '[123, 34, 65, 117, 116, 104, 101, 110, 116, 105, 99, 97, 116, 111, 114, 115, 34, 58, 91, 123, 34, 84, 121, 112, 101, 34, 58, 50, 44, 34, 73, 99, 111, 110, 34, 58, 110, 117, 108, 108, 44, 34, 73, 115, 115, 117, 101, 114, 34, 58, 34, 84, 101, 115, 116, 49, 34, 44, 34, 85, 115, 101, 114, 110, 97, 109, 101, 34, 58, 34, 84, 101, 115, 116, 49, 34, 44, 34, 83, 101, 99, 114, 101, 116, 34, 58, 34, 65, 65, 65, 65, 65, 65, 65, 65, 34, 44, 34, 80, 105,' + '110, 34, 58, 110, 117, 108, 108, 44, 34, 65, 108, 103, 111, 114, 105, 116, 104, 109, 34, 58, 48, 44, 34, 68, 105, 103, 105, 116, 115, 34, 58, 54, 44, 34, 80, 101, 114, 105, 111, 100, 34, 58, 51, 48, 44, 34, 67, 111, 117, 110, 116, 101, 114, 34, 58, 48, 44, 34, 67, 111, 112, 121, 67, 111, 117, 110, 116, 34, 58, 48, 44, 34, 82, 97, 110, 107, 105, 110, 103, 34, 58, 48, 125, 44, 123, 34, 84, 121, 112, 101, 34, 58, 49, 44, 34, 73, 99, 111, 110, 34,' + '58, 110, 117, 108, 108, 44, 34, 73, 115, 115, 117, 101, 114, 34, 58, 34, 84, 101, 115, 116, 50, 34, 44, 34, 85, 115, 101, 114, 110, 97, 109, 101, 34, 58, 34, 84, 101, 115, 116, 50, 34, 44, 34, 83, 101, 99, 114, 101, 116, 34, 58, 34, 66, 66, 66, 66, 66, 66, 66, 66, 34, 44, 34, 80, 105, 110, 34, 58, 110, 117, 108, 108, 44, 34, 65, 108, 103, 111, 114, 105, 116, 104, 109, 34, 58, 48, 44, 34, 68, 105, 103, 105, 116, 115, 34, 58, 54, 44, 34, 80,' + '101, 114, 105, 111, 100, 34, 58, 51, 48, 44, 34, 67, 111, 117, 110, 116, 101, 114, 34, 58, 48, 44, 34, 67, 111, 112, 121, 67, 111, 117, 110, 116, 34, 58, 48, 44, 34, 82, 97, 110, 107, 105, 110, 103, 34, 58, 48, 125, 93, 44, 34, 67, 97, 116, 101, 103, 111, 114, 105, 101, 115, 34, 58, 91, 93, 44, 34, 65, 117, 116, 104, 101, 110, 116, 105, 99, 97, 116, 111, 114, 67, 97, 116, 101, 103, 111, 114, 105, 101, 115, 34, 58, 91, 93, 44, 34, 67, 117, 115, 116,' + '111, 109, 73, 99, 111, 110, 115, 34, 58, 91, 93, 125]'; + final byteData = Uint8List.fromList((jsonDecode(byteDataString) as List).cast()); + final XFile file = XFile.fromData(byteData, name: 'auth_pro_plain.json'); + group('fileIsValid', () { + test('isTrue', () async { + // Act + final result = await processor.fileIsValid(file); + // Assert + expect(result, isTrue); + }); + test('isFalse', () async { + // Arrange + final byteData = Uint8List.fromList((jsonDecode(byteDataString) as List).cast()..removeLast()); + final XFile file = XFile.fromData(byteData, name: 'auth_pro_plain_invalid.json'); + + // Act + final result = await processor.fileIsValid(file); + // Assert + expect(result, isFalse); + }); + }); + + test('fileNeedsPassword', () async { + // Act + final result = await processor.fileNeedsPassword(file); + // Assert + expect(result, isFalse); + }); + test('processFile', () async { + // Act + final results = await processor.processFile(file); + // Assert + _assertSuccessResults(results); + }); + }); + }); + group('html plain', () { + // Arrange + const htmlFileByteString = + '[60, 33, 100, 111, 99, 116, 121, 112, 101, 32, 104, 116, 109, 108, 62, 10, 60, 104, 116, 109, 108, 62, 10, 32, 32, 32, 32, 60, 104, 101, 97, 100, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 109, 101, 116, 97, 32, 99, 104, 97, 114, 115, 101, 116, 61, 34, 117, 116, 102, 45, 56, 34, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 105, 116, 108, 101, 62, 65, 117, 116, 104, 101, 110, 116, 105, 99, 97, 116, 111, 114, 32, 80, 114, 111, 32, 66, 97,' + '99, 107, 117, 112, 60, 47, 116, 105, 116, 108, 101, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 115, 116, 121, 108, 101, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 98, 111, 100, 121, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 102, 111, 110, 116, 45, 102, 97, 109, 105, 108, 121, 58, 32, 115, 97, 110, 115, 45, 115, 101, 114, 105, 102, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 116, 97, 98, 108, 101, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 119, 105, 100, 116, 104, 58, 32, 49, 48, 48, 37, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 98, 111, 114, 100, 101, 114, 45, 99, 111, 108, 108, 97, 112, 115, 101, 58, 32, 99, 111, 108, 108, 97, 112,' + '115, 101, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 116, 97, 98, 108, 101, 32, 116, 104, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 116, 101, 120, 116, 45, 97, 108, 105, 103, 110, 58, 32, 108, 101, 102, 116, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 116, 97, 98, 108, 101, 32, 116, 114, 58, 110, 111, 116, 40, 58, 108, 97, 115, 116, 45, 111, 102, 45, 116, 121, 112, 101, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 98, 111, 114, 100, 101, 114, 45, 98, 111, 116, 116, 111, 109, 58, 32, 115, 111, 108, 105, 100, 32, 49, 112, 120, 32, 35, 101,' + '101, 101, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 116, 97, 98, 108, 101, 32, 116, 114, 58, 102, 105, 114, 115, 116, 45, 111, 102, 45, 116, 121, 112, 101, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 98, 111, 114, 100, 101, 114, 45, 99, 111, 108, 111, 114, 58, 32, 35, 48,' + '48, 48, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 116, 97, 98, 108, 101, 32, 116, 104, 44, 32, 116, 97, 98, 108, 101, 32, 116, 100, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 112, 97, 100, 100, 105, 110, 103, 58, 32, 56, 112, 120, 59, 10, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 47, 115, 116, 121, 108, 101, 62, 10, 32, 32, 32, 32, 60, 47, 104, 101, 97, 100, 62, 10, 32, 32, 32, 32, 60, 98, 111, 100, 121, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 97, 98, 108, 101, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 114, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 60, 116, 104, 62, 73, 115, 115, 117, 101, 114, 60, 47, 116, 104, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 104, 62, 85, 115, 101, 114, 110, 97, 109, 101, 60, 47, 116, 104, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 104, 62, 79, 84, 80, 32, 65, 117, 116, 104, 32, 85, 82, 73, 60, 47, 116, 104, 62, 10, 32, 32, 32, 32, 32, 32,' + '32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 104, 62, 81, 82, 32, 67, 111, 100, 101, 60, 47, 116, 104, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 47, 116, 114, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 114, 62, 10, 32, 32, 32, 32, 60, 116, 100, 62, 84, 101, 115, 116, 49, 60, 47, 116, 100, 62, 10, 32, 32, 32, 32, 60, 116,' + '100, 62, 84, 101, 115, 116, 49, 60, 47, 116, 100, 62, 10, 32, 32, 32, 32, 60, 116, 100, 62, 60, 99, 111, 100, 101, 62, 111, 116, 112, 97, 117, 116, 104, 58, 47, 47, 116, 111, 116, 112, 47, 84, 101, 115, 116, 49, 37, 51, 65, 84, 101, 115, 116, 49, 63, 115, 101, 99, 114, 101, 116, 61, 65, 65, 65, 65, 65, 65, 65, 65, 38, 105, 115, 115, 117, 101, 114, 61, 84, 101, 115, 116, 49, 60, 47, 99, 111, 100, 101, 62, 60, 47, 116, 100, 62, 10, 32, 32, 32,' + '32, 60, 116, 100, 62, 60, 105, 109, 103, 32, 115, 114, 99, 61, 34, 100, 97, 116, 97, 58, 105, 109, 97, 103, 101, 47, 112, 110, 103, 59, 98, 97, 115, 101, 54, 52, 44, 105, 86, 66, 79, 82, 119, 48, 75, 71, 103, 111, 65, 65, 65, 65, 78, 83, 85, 104, 69, 85, 103, 65, 65, 65, 76, 81, 65, 65, 65, 67, 48, 65, 81, 65, 65, 65, 65, 65, 86, 116, 106, 117, 102, 65, 65, 65, 66, 99, 107, 108, 69, 81, 86, 82, 52, 110, 79, 50, 88, 85, 90, 76,' + '67, 77, 65, 120, 68, 100, 81, 80, 102, 47, 53, 97, 54, 103, 98, 72, 107, 108, 116, 73, 100, 47, 108, 98, 119, 82, 101, 105, 85, 53, 115, 70, 77, 107, 79, 48, 111, 66, 118, 49, 50, 69, 68, 47, 43, 76, 81, 54, 103, 109, 113, 88, 82, 120, 100, 112, 112, 107, 78, 100, 99, 71, 68, 65, 118, 43, 77, 77, 79, 56, 53, 107, 78, 66, 54, 105, 53, 110, 118, 78, 99, 54, 111, 112, 100, 57, 83, 70, 79, 72, 69, 47, 111, 80, 74, 43, 76, 67, 116, 114,' + '107, 71, 51, 47, 105, 109, 101, 66, 75, 57, 77, 117, 52, 49, 85, 79, 67, 98, 54, 70, 121, 118, 116, 68, 85, 106, 55, 106, 86, 99, 52, 68, 84, 67, 87, 100, 118, 48, 105, 85, 117, 121, 52, 87, 116, 82, 113, 86, 70, 104, 84, 68, 76, 116, 101, 88, 109, 112, 113, 113, 97, 108, 86, 48, 65, 85, 81, 55, 72, 114, 67, 89, 47, 85, 107, 102, 74, 105, 47, 74, 90, 97, 108, 89, 99, 114, 70, 88, 88, 81, 75, 74, 99, 43, 90, 89, 100, 114, 98, 82,' + '83, 87, 85, 85, 53, 54, 88, 116, 98, 50, 77, 122, 65, 77, 66, 56, 49, 74, 114, 85, 98, 114, 56, 79, 56, 108, 87, 52, 118, 51, 54, 113, 116, 48, 86, 104, 104, 118, 115, 98, 82, 76, 105, 111, 53, 101, 74, 104, 106, 99, 52, 69, 49, 119, 77, 79, 47, 99, 49, 121, 110, 50, 99, 122, 109, 114, 116, 66, 86, 110, 47, 115, 105, 120, 88, 87, 86, 107, 54, 79, 113, 57, 83, 54, 80, 99, 114, 49, 82, 53, 48, 70, 98, 72, 90, 57, 43, 109, 79, 74,' + '110, 118, 118, 115, 111, 114, 84, 66, 110, 55, 82, 80, 87, 110, 112, 53, 49, 108, 101, 76, 108, 90, 111, 87, 55, 74, 89, 67, 114, 98, 119, 108, 120, 90, 49, 55, 74, 116, 48, 85, 112, 101, 70, 108, 117, 73, 87, 52, 115, 102, 71, 122, 50, 53, 83, 99, 90, 98, 104, 43, 49, 110, 56, 75, 78, 88, 89, 102, 53, 72, 118, 120, 87, 66, 80, 105, 65, 113, 121, 120, 88, 53, 71, 66, 84, 57, 90, 53, 79, 99, 120, 47, 74, 48, 113, 79, 65, 79, 88, 74,' + '90, 55, 114, 53, 88, 75, 53, 118, 87, 108, 90, 99, 85, 120, 49, 98, 86, 108, 113, 52, 69, 53, 114, 107, 83, 119, 106, 51, 53, 49, 102, 57, 43, 103, 72, 80, 47, 72, 77, 106, 57, 48, 116, 120, 120, 107, 54, 122, 116, 117, 99, 53, 122, 80, 56, 88, 88, 75, 75, 122, 75, 80, 85, 122, 102, 47, 107, 47, 57, 110, 55, 56, 98, 80, 47, 52, 108, 47, 103, 65, 80, 48, 119, 89, 80, 99, 50, 108, 73, 67, 103, 65, 65, 65, 65, 66, 74, 82, 85, 53,' + '69, 114, 107, 74, 103, 103, 103, 61, 61, 34, 62, 60, 47, 116, 100, 62, 10, 60, 47, 116, 114, 62, 60, 116, 114, 62, 10, 32, 32, 32, 32, 60, 116, 100, 62, 84, 101, 115, 116, 50, 60, 47, 116, 100, 62, 10, 32, 32, 32, 32, 60, 116, 100, 62, 84, 101, 115, 116, 50, 60, 47, 116, 100, 62, 10, 32, 32, 32, 32, 60, 116, 100, 62, 60, 99, 111, 100, 101, 62, 111, 116, 112, 97, 117, 116, 104, 58, 47, 47, 104, 111, 116, 112, 47, 84, 101, 115, 116, 50, 37,' + '51, 65, 84, 101, 115, 116, 50, 63, 115, 101, 99, 114, 101, 116, 61, 66, 66, 66, 66, 66, 66, 66, 66, 38, 105, 115, 115, 117, 101, 114, 61, 84, 101, 115, 116, 50, 38, 99, 111, 117, 110, 116, 101, 114, 61, 48, 60, 47, 99, 111, 100, 101, 62, 60, 47, 116, 100, 62, 10, 32, 32, 32, 32, 60, 116, 100, 62, 60, 105, 109, 103, 32, 115, 114, 99, 61, 34, 100, 97, 116, 97, 58, 105, 109, 97, 103, 101, 47, 112, 110, 103, 59, 98, 97, 115, 101, 54, 52, 44, 105,' + '86, 66, 79, 82, 119, 48, 75, 71, 103, 111, 65, 65, 65, 65, 78, 83, 85, 104, 69, 85, 103, 65, 65, 65, 77, 81, 65, 65, 65, 68, 69, 65, 81, 65, 65, 65, 65, 68, 111, 51, 98, 80, 71, 65, 65, 65, 66, 118, 85, 108, 69, 81, 86, 82, 52, 110, 79, 50, 88, 87, 50, 52, 68, 81, 81, 103, 69, 117, 81, 72, 51, 118, 50, 88, 102, 103, 70, 68, 78, 98, 71, 120, 76, 121, 100, 56, 103, 53, 83, 78, 106, 120, 52, 56, 112, 83, 55, 115, 119, 84,' + '85, 79, 105, 102, 108, 109, 75, 102, 47, 73, 51, 83, 69, 83, 107, 112, 72, 52, 116, 47, 112, 84, 104, 116, 43, 115, 107, 43, 53, 107, 90, 121, 115, 113, 53, 73, 84, 89, 50, 83, 80, 82, 43, 104, 102, 113, 76, 115, 104, 47, 101, 87, 67, 73, 100, 90, 52, 100, 52, 111, 116, 119, 107, 118, 102, 113, 57, 48, 53, 116, 55, 112, 69, 103, 108, 112, 53, 102, 99, 82, 51, 122, 109, 43, 104, 53, 120, 99, 79, 47, 114, 81, 122, 118, 51, 105, 74, 99, 47, 111,' + '56, 88, 79, 54, 50, 99, 116, 88, 67, 77, 106, 106, 69, 81, 97, 82, 67, 104, 43, 116, 48, 70, 81, 117, 116, 103, 74, 55, 113, 79, 108, 114, 49, 100, 108, 51, 83, 82, 57, 90, 118, 49, 48, 100, 67, 76, 79, 99, 80, 106, 51, 67, 100, 99, 116, 82, 67, 56, 113, 114, 71, 112, 121, 101, 112, 43, 107, 110, 97, 104, 108, 51, 107, 85, 99, 116, 113, 101, 73, 68, 100, 73, 112, 76, 67, 52, 116, 89, 107, 119, 114, 80, 106, 100, 73, 79, 85, 82, 120, 72,' + '50, 105, 108, 114, 54, 57, 72, 56, 86, 101, 74, 120, 108, 118, 108, 113, 120, 55, 70, 98, 66, 68, 98, 69, 73, 82, 102, 70, 75, 101, 88, 51, 122, 86, 51, 107, 49, 103, 88, 52, 118, 122, 107, 87, 68, 118, 68, 50, 105, 68, 67, 89, 90, 79, 57, 72, 66, 107, 113, 89, 52, 79, 77, 76, 43, 65, 84, 114, 113, 116, 119, 105, 104, 99, 73, 47, 107, 112, 119, 97, 70, 66, 80, 117, 67, 116, 69, 122, 121, 89, 82, 114, 120, 71, 70, 106, 117, 89, 112, 51,' + '88, 83, 106, 113, 103, 88, 105, 111, 115, 76, 57, 69, 73, 110, 78, 102, 73, 102, 85, 101, 69, 80, 118, 89, 66, 98, 43, 69, 107, 118, 69, 48, 119, 80, 57, 110, 69, 109, 73, 98, 79, 89, 67, 56, 82, 90, 70, 106, 77, 110, 97, 77, 113, 73, 50, 121, 74, 115, 116, 89, 66, 81, 85, 49, 112, 80, 82, 113, 52, 81, 100, 55, 105, 72, 111, 115, 101, 84, 48, 53, 83, 70, 88, 121, 97, 69, 101, 117, 101, 83, 87, 47, 107, 120, 99, 100, 119, 110, 104, 87,' + '88, 122, 101, 48, 104, 108, 83, 55, 104, 77, 71, 104, 110, 66, 76, 112, 52, 111, 82, 82, 53, 49, 74, 57, 83, 54, 104, 122, 52, 53, 65, 47, 71, 107, 71, 121, 103, 88, 105, 51, 106, 83, 122, 107, 72, 83, 75, 98, 73, 78, 52, 65, 67, 55, 76, 65, 113, 72, 77, 77, 76, 108, 65, 99, 115, 82, 73, 111, 122, 105, 57, 97, 89, 109, 69, 76, 87, 47, 71, 73, 97, 116, 47, 106, 57, 105, 43, 100, 83, 98, 57, 97, 98, 107, 114, 53, 74, 105, 115, 57,' + '89, 55, 80, 53, 103, 89, 120, 53, 80, 119, 52, 119, 102, 75, 47, 76, 98, 86, 65, 80, 65, 84, 84, 66, 56, 102, 121, 103, 116, 108, 114, 103, 47, 121, 56, 47, 115, 109, 102, 73, 70, 57, 110, 49, 118, 57, 117, 48, 80, 48, 100, 72, 119, 65, 65, 65, 65, 66, 74, 82, 85, 53, 69, 114, 107, 74, 103, 103, 103, 61, 61, 34, 62, 60, 47, 116, 100, 62, 10, 60, 47, 116, 114, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 47, 116, 97, 98, 108, 101,' + '62, 10, 32, 32, 32, 32, 10, 32, 32, 32, 32, 60, 47, 98, 111, 100, 121, 62, 10, 60, 47, 104, 116, 109, 108, 62]'; + final byteData = Uint8List.fromList((jsonDecode(htmlFileByteString) as List).cast().toList()); + + final file = XFile.fromData(byteData, name: 'auth_pro_plain.html'); + + group('fileIsValid', () { + test('isTrue', () async { + // Act + final fileIsValid = await processor.fileIsValid(file); + // Assert + expect(fileIsValid, isTrue); + }); + test('isFalse', () async { + // Arrange + final byteData = Uint8List.fromList((jsonDecode(htmlFileByteString) as List).cast().toList()..removeAt(0)); + + final file = XFile.fromData(byteData, name: 'auth_pro_plain_invalid.html'); + // Act + final fileIsValid = await processor.fileIsValid(file); + // Assert + expect(fileIsValid, isFalse); + }); + }); + + test('fileNeedsPassword', () async { + // Act + final result = await processor.fileNeedsPassword(file); + // Assert + expect(result, false); + }); + + test('processFile', () async { + // Act + final results = await processor.processFile(file); + // Assert + _assertSuccessResults(results); + }); + }); + group('otpauth link list plain', () { + const uriListBytes = + '[111, 116, 112, 97, 117, 116, 104, 58, 47, 47, 116, 111, 116, 112, 47, 84, 101, 115, 116, 49, 37, 51, 65, 84, 101, 115, 116, 49, 63, 115, 101, 99, 114, 101, 116, 61, 65, 65, 65, 65, 65, 65, 65, 65, 38, 105, 115, 115, 117, 101, 114, 61, 84, 101, 115, 116, 49, 10, 111, 116, 112, 97, 117, 116, 104, 58, 47, 47, 104, 111, 116, 112, 47, 84, 101, 115, 116, 50, 37, 51, 65, 84, 101, 115, 116, 50, 63, 115, 101, 99, 114, 101, 116, 61, 66, 66, 66, 66, 66, 66,' + '66, 66, 38, 105, 115, 115, 117, 101, 114, 61, 84, 101, 115, 116, 50, 38, 99, 111, 117, 110, 116, 101, 114, 61, 48, 10]'; + final byteData = Uint8List.fromList((jsonDecode(uriListBytes) as List).cast().toList()); + + final file = XFile.fromData(byteData, name: 'auth_pro_plain.txt'); + + group('fileIsValid', () { + test('isTrue', () async { + // Act + final fileIsValid = await processor.fileIsValid(file); + // Assert + expect(fileIsValid, isTrue); + }); + test('isFalse', () async { + // Act + final byteData = Uint8List.fromList((jsonDecode(uriListBytes) as List).cast().toList()..removeWhere((uint) => uint == 58)); // 58 is ':' + + final file = XFile.fromData(byteData, name: 'auth_pro_plain_invalid.txt'); + final fileIsValid = await processor.fileIsValid(file); + // Assert + expect(fileIsValid, isFalse); + }); + }); + + test('fileNeedsPassword', () async { + // Act + final result = await processor.fileNeedsPassword(file); + // Assert + expect(result, isFalse); + }); + test('processFile', () async { + // Act + final results = await processor.processFile(file); + // Assert + _assertSuccessResults(results); + }); + }); + }); +} diff --git a/test/unit_test/processors/token_import_file_processor/free_otp_plus_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/free_otp_plus_import_file_processor_test.dart new file mode 100644 index 000000000..c3e4a2582 --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/free_otp_plus_import_file_processor_test.dart @@ -0,0 +1,138 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/enums/token_types.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +import 'package:privacyidea_authenticator/processors/token_import_file_processor/free_otp_plus_import_file_processor.dart'; +import 'package:privacyidea_authenticator/utils/token_import_origins.dart'; + +void main() { + _testFreeOtpPlusImportFileProcessor(); +} + +void _assertSuccessResults(List> results) { + expect(results.length, equals(2)); + + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.label, 'Test2'); + expect(token0.issuer, 'Test2'); + expect(token0.type, TokenTypes.HOTP.name); + expect(token0, isA()); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, TokenImportOrigins.freeOtpPlus.appName); + expect(token0.origin!.source, TokenOriginSourceType.backupFile); + final hotpToken = token0 as HOTPToken; + expect(hotpToken.secret, 'BBBBBBBB'); + expect(hotpToken.algorithm, Algorithms.SHA1); + expect(hotpToken.digits, 8); + expect(hotpToken.counter, 5); + expect(hotpToken.otpValue, equals('83718223')); + final result1 = results[1]; + expect(result1, isA()); + final token1 = result1.asSuccess!.resultData; + expect(token1.label, 'Test1'); + expect(token1.issuer, 'Test1'); + expect(token1.type, TokenTypes.TOTP.name); + expect(token1, isA()); + expect(token1.origin, isNotNull); + expect(token1.origin!.appName, TokenImportOrigins.freeOtpPlus.appName); + expect(token1.origin!.source, TokenOriginSourceType.backupFile); + final totpToken = token1 as TOTPToken; + expect(totpToken.secret, 'AAAAAAAA'); + expect(totpToken.algorithm, Algorithms.SHA256); + expect(totpToken.digits, 8); + expect(totpToken.period, 60); + expect(totpToken.otpFromTime(DateTime.fromMillisecondsSinceEpoch(1713519600602)), equals('46107496')); +} + +void _testFreeOtpPlusImportFileProcessor() { + group('Free Otp Plus File Processor Test', () { + const processor = FreeOtpPlusImportFileProcessor(); + // No encryption or password protection is used in the FreeOTP+ App at all. + group('JSON plain', () { + // Arrange + const jsonFileBytesString = + '[123, 34, 116, 111, 107, 101, 110, 79, 114, 100, 101, 114, 34, 58, 91, 34, 84, 101, 115, 116, 50, 58, 84, 101, 115, 116, 50, 34, 44, 34, 84, 101, 115, 116, 49, 58, 84, 101, 115, 116, 49, 34, 93, 44, 34, 116, 111, 107, 101, 110, 115, 34, 58, 91, 123, 34, 97, 108, 103, 111, 34, 58, 34, 83, 72, 65, 49, 34, 44, 34, 99, 111, 117, 110, 116, 101, 114, 34, 58, 52, 44, 34, 100, 105, 103, 105, 116, 115, 34, 58, 56, 44, 34, 105, 115, 115, 117, 101, 114, 69,' + '120, 116, 34, 58, 34, 84, 101, 115, 116, 50, 34, 44, 34, 108, 97, 98, 101, 108, 34, 58, 34, 84, 101, 115, 116, 50, 34, 44, 34, 112, 101, 114, 105, 111, 100, 34, 58, 51, 48, 44, 34, 115, 101, 99, 114, 101, 116, 34, 58, 91, 56, 44, 54, 54, 44, 49, 54, 44, 45, 49, 50, 52, 44, 51, 51, 93, 44, 34, 116, 121, 112, 101, 34, 58, 34, 72, 79, 84, 80, 34, 125, 44, 123, 34, 97, 108, 103, 111, 34, 58, 34, 83, 72, 65, 50, 53, 54, 34, 44, 34,' + '99, 111, 117, 110, 116, 101, 114, 34, 58, 48, 44, 34, 100, 105, 103, 105, 116, 115, 34, 58, 56, 44, 34, 105, 115, 115, 117, 101, 114, 69, 120, 116, 34, 58, 34, 84, 101, 115, 116, 49, 34, 44, 34, 108, 97, 98, 101, 108, 34, 58, 34, 84, 101, 115, 116, 49, 34, 44, 34, 112, 101, 114, 105, 111, 100, 34, 58, 54, 48, 44, 34, 115, 101, 99, 114, 101, 116, 34, 58, 91, 48, 44, 48, 44, 48, 44, 48, 44, 48, 93, 44, 34, 116, 121, 112, 101, 34, 58, 34, 84,' + '79, 84, 80, 34, 125, 93, 125]'; + final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast(); + final jsonFile = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Free_OTP_Plus_plain.json'); + + group('fileIsValid', () { + test('isTrue', () async { + // Act + final fileIsValid = await processor.fileIsValid(jsonFile); + // Assert + expect(fileIsValid, isTrue); + }); + test('isFalse', () async { + // Arrange + final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast()..removeLast(); + final jsonFileInvalid = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Free_OTP_Plus_plain_invalid.json'); + // Act + final fileIsValid = await processor.fileIsValid(jsonFileInvalid); + // Assert + expect(fileIsValid, isFalse); + }); + }); + test('fileNeedsPassword', () async { + // Act + final fileNeedsPassword = await processor.fileNeedsPassword(jsonFile); + // Assert + expect(fileNeedsPassword, isFalse); + }); + test('processFile', () async { + // Act + final results = await processor.processFile(jsonFile); + // Assert + _assertSuccessResults(results); + }); + }); + group('Uri List plain', () { + const uriListByteString = + '[111, 116, 112, 97, 117, 116, 104, 58, 47, 47, 104, 111, 116, 112, 47, 84, 101, 115, 116, 50, 37, 51, 65, 84, 101, 115, 116, 50, 63, 115, 101, 99, 114, 101, 116, 61, 66, 66, 66, 66, 66, 66, 66, 66, 38, 97, 108, 103, 111, 114, 105, 116, 104, 109, 61, 83, 72, 65, 49, 38, 100, 105, 103, 105, 116, 115, 61, 56, 38, 112, 101, 114, 105, 111, 100, 61, 51, 48, 38, 99, 111, 117, 110, 116, 101, 114, 61, 53, 10, 111, 116, 112, 97, 117, 116, 104, 58, 47, 47, 116,' + '111, 116, 112, 47, 84, 101, 115, 116, 49, 37, 51, 65, 84, 101, 115, 116, 49, 63, 115, 101, 99, 114, 101, 116, 61, 65, 65, 65, 65, 65, 65, 65, 65, 38, 97, 108, 103, 111, 114, 105, 116, 104, 109, 61, 83, 72, 65, 50, 53, 54, 38, 100, 105, 103, 105, 116, 115, 61, 56, 38, 112, 101, 114, 105, 111, 100, 61, 54, 48, 10]'; + final uriListBytes = (jsonDecode(uriListByteString) as List).cast(); + final uriListFile = XFile.fromData(Uint8List.fromList(uriListBytes), name: 'Free_OTP_Plus_uri_list_plain.txt'); + group('fileIsValid', () { + test('isTrue', () async { + // Act + final fileIsValid = await processor.fileIsValid(uriListFile); + // Assert + expect(fileIsValid, isTrue); + }); + test('isFalse', () async { + // Arrange + final uriListBytes = (jsonDecode(uriListByteString) as List).cast()..removeAt(0); + final uriListFileInvalid = XFile.fromData(Uint8List.fromList(uriListBytes), name: 'Free_OTP_Plus_uri_list_plain_invalid.txt'); + // Act + final fileIsValid = await processor.fileIsValid(uriListFileInvalid); + // Assert + expect(fileIsValid, isFalse); + }); + }); + test('fileNeedsPassword', () async { + // Act + final fileNeedsPassword = await processor.fileNeedsPassword(uriListFile); + // Assert + expect(fileNeedsPassword, isFalse); + }); + test('processFile', () async { + // Act + final results = await processor.processFile(uriListFile); + // Assert + _assertSuccessResults(results); + }); + }); + }); +} diff --git a/test/unit_test/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor_test.dart new file mode 100644 index 000000000..283240836 --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/privacyidea_authenticator_import_file_processor_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + _testPrivacyideaAuthenticatorImportFileProcessor(); +} + +void _testPrivacyideaAuthenticatorImportFileProcessor() { + group('Privacyidea Authenticator Import File Processor', () { + test('', () {}); + }); +} diff --git a/test/unit_test/processors/token_import_file_processor/two_fas_authenticator_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/two_fas_authenticator_import_file_processor_test.dart new file mode 100644 index 000000000..aa8fe6077 --- /dev/null +++ b/test/unit_test/processors/token_import_file_processor/two_fas_authenticator_import_file_processor_test.dart @@ -0,0 +1,186 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/enums/token_types.dart'; +import 'package:privacyidea_authenticator/model/processor_result.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/steam_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +import 'package:privacyidea_authenticator/processors/token_import_file_processor/two_fas_import_file_processor.dart'; +import 'package:privacyidea_authenticator/utils/token_import_origins.dart'; + +void main() { + _testTwoFasImportFileProcessor(); +} + +void _assertSuccessResults(List> results) { + expect(results.length, equals(3)); + + final result0 = results[0]; + expect(result0, isA()); + final token0 = result0.asSuccess!.resultData; + expect(token0.label, ''); + expect(token0.issuer, 'Test1'); + expect(token0.type, TokenTypes.TOTP.name); + expect(token0, isA()); + expect(token0.origin, isNotNull); + expect(token0.origin!.appName, TokenImportOrigins.twoFasAuthenticator.appName); + expect(token0.origin!.source, TokenOriginSourceType.backupFile); + final totpToken = token0 as TOTPToken; + expect(totpToken.secret, equals('AAAAAAAA')); + expect(totpToken.algorithm, Algorithms.SHA256); + expect(totpToken.digits, 8); + expect(totpToken.period, 60); + expect(totpToken.otpFromTime(DateTime.fromMillisecondsSinceEpoch(1713519600602)), equals('46107496')); + final result1 = results[1]; + expect(result1, isA()); + final token1 = result1.asSuccess!.resultData; + expect(token1.label, ''); + expect(token1.issuer, 'Test2'); + expect(token1.type, TokenTypes.HOTP.name); + expect(token1, isA()); + expect(token1.origin, isNotNull); + expect(token1.origin!.appName, TokenImportOrigins.twoFasAuthenticator.appName); + expect(token1.origin!.source, TokenOriginSourceType.backupFile); + final hotpToken = token1 as HOTPToken; + expect(hotpToken.secret, equals('BBBBBBBB')); + expect(hotpToken.algorithm, Algorithms.SHA1); + expect(hotpToken.digits, 6); + expect(hotpToken.counter, 5); + expect(hotpToken.otpValue, equals('718223')); + final result2 = results[2]; + expect(result2, isA()); + final token2 = result2.asSuccess!.resultData; + expect(token2.label, ''); + expect(token2.issuer, 'SteamTest'); + expect(token2.type, TokenTypes.STEAM.name); + expect(token2, isA()); + expect(token2.origin, isNotNull); + expect(token2.origin!.appName, TokenImportOrigins.twoFasAuthenticator.appName); + expect(token2.origin!.source, TokenOriginSourceType.backupFile); + final steamToken = token2 as SteamToken; + expect(steamToken.secret, equals('CCCCCCCC')); +} + +void _testTwoFasImportFileProcessor() { + group('Two Fas Import File Processor Test', () { + // Arrange + const processor = TwoFasAuthenticatorImportFileProcessor(); + group('JSON', () { + const jsonFileBytesString = + '[123, 34, 115, 101, 114, 118, 105, 99, 101, 115, 34, 58, 91, 123, 34, 110, 97, 109, 101, 34, 58, 34, 84, 101, 115, 116, 49, 34, 44, 34, 115, 101, 99, 114, 101, 116, 34, 58, 34, 65, 65, 65, 65, 65, 65, 65, 65, 34, 44, 34, 117, 112, 100, 97, 116, 101, 100, 65, 116, 34, 58, 49, 55, 49, 51, 53, 50, 54, 55, 55, 48, 56, 55, 57, 44, 34, 111, 116, 112, 34, 58, 123, 34, 108, 97, 98, 101, 108, 34, 58, 34, 34, 44, 34, 97, 99, 99, 111, 117, 110,' + '116, 34, 58, 34, 34, 44, 34, 100, 105, 103, 105, 116, 115, 34, 58, 56, 44, 34, 112, 101, 114, 105, 111, 100, 34, 58, 54, 48, 44, 34, 97, 108, 103, 111, 114, 105, 116, 104, 109, 34, 58, 34, 83, 72, 65, 50, 53, 54, 34, 44, 34, 116, 111, 107, 101, 110, 84, 121, 112, 101, 34, 58, 34, 84, 79, 84, 80, 34, 44, 34, 115, 111, 117, 114, 99, 101, 34, 58, 34, 77, 97, 110, 117, 97, 108, 34, 125, 44, 34, 111, 114, 100, 101, 114, 34, 58, 123, 34, 112, 111,' + '115, 105, 116, 105, 111, 110, 34, 58, 48, 125, 44, 34, 105, 99, 111, 110, 34, 58, 123, 34, 115, 101, 108, 101, 99, 116, 101, 100, 34, 58, 34, 76, 97, 98, 101, 108, 34, 44, 34, 108, 97, 98, 101, 108, 34, 58, 123, 34, 116, 101, 120, 116, 34, 58, 34, 84, 69, 34, 44, 34, 98, 97, 99, 107, 103, 114, 111, 117, 110, 100, 67, 111, 108, 111, 114, 34, 58, 34, 66, 114, 111, 119, 110, 34, 125, 44, 34, 105, 99, 111, 110, 67, 111, 108, 108, 101, 99, 116, 105, 111,' + '110, 34, 58, 123, 34, 105, 100, 34, 58, 34, 97, 53, 98, 51, 102, 98, 54, 53, 45, 52, 101, 99, 53, 45, 52, 51, 101, 54, 45, 56, 101, 99, 49, 45, 52, 57, 101, 50, 52, 99, 97, 57, 101, 55, 97, 100, 34, 125, 125, 125, 44, 123, 34, 110, 97, 109, 101, 34, 58, 34, 84, 101, 115, 116, 50, 34, 44, 34, 115, 101, 99, 114, 101, 116, 34, 58, 34, 66, 66, 66, 66, 66, 66, 66, 66, 34, 44, 34, 117, 112, 100, 97, 116, 101, 100, 65, 116, 34, 58, 49,' + '55, 49, 51, 53, 50, 54, 57, 57, 54, 48, 55, 57, 44, 34, 111, 116, 112, 34, 58, 123, 34, 108, 97, 98, 101, 108, 34, 58, 34, 34, 44, 34, 97, 99, 99, 111, 117, 110, 116, 34, 58, 34, 34, 44, 34, 100, 105, 103, 105, 116, 115, 34, 58, 54, 44, 34, 112, 101, 114, 105, 111, 100, 34, 58, 51, 48, 44, 34, 97, 108, 103, 111, 114, 105, 116, 104, 109, 34, 58, 34, 83, 72, 65, 49, 34, 44, 34, 99, 111, 117, 110, 116, 101, 114, 34, 58, 53, 44, 34, 116,' + '111, 107, 101, 110, 84, 121, 112, 101, 34, 58, 34, 72, 79, 84, 80, 34, 44, 34, 115, 111, 117, 114, 99, 101, 34, 58, 34, 77, 97, 110, 117, 97, 108, 34, 125, 44, 34, 111, 114, 100, 101, 114, 34, 58, 123, 34, 112, 111, 115, 105, 116, 105, 111, 110, 34, 58, 49, 125, 44, 34, 105, 99, 111, 110, 34, 58, 123, 34, 115, 101, 108, 101, 99, 116, 101, 100, 34, 58, 34, 76, 97, 98, 101, 108, 34, 44, 34, 108, 97, 98, 101, 108, 34, 58, 123, 34, 116, 101, 120, 116,' + '34, 58, 34, 84, 69, 34, 44, 34, 98, 97, 99, 107, 103, 114, 111, 117, 110, 100, 67, 111, 108, 111, 114, 34, 58, 34, 79, 114, 97, 110, 103, 101, 34, 125, 44, 34, 105, 99, 111, 110, 67, 111, 108, 108, 101, 99, 116, 105, 111, 110, 34, 58, 123, 34, 105, 100, 34, 58, 34, 97, 53, 98, 51, 102, 98, 54, 53, 45, 52, 101, 99, 53, 45, 52, 51, 101, 54, 45, 56, 101, 99, 49, 45, 52, 57, 101, 50, 52, 99, 97, 57, 101, 55, 97, 100, 34, 125, 125, 125, 44,' + '123, 34, 110, 97, 109, 101, 34, 58, 34, 83, 116, 101, 97, 109, 84, 101, 115, 116, 34, 44, 34, 115, 101, 99, 114, 101, 116, 34, 58, 34, 67, 67, 67, 67, 67, 67, 67, 67, 34, 44, 34, 117, 112, 100, 97, 116, 101, 100, 65, 116, 34, 58, 49, 55, 49, 51, 53, 50, 56, 55, 52, 56, 56, 48, 53, 44, 34, 111, 116, 112, 34, 58, 123, 34, 108, 97, 98, 101, 108, 34, 58, 34, 34, 44, 34, 97, 99, 99, 111, 117, 110, 116, 34, 58, 34, 34, 44, 34, 105, 115,' + '115, 117, 101, 114, 34, 58, 34, 83, 116, 101, 97, 109, 34, 44, 34, 100, 105, 103, 105, 116, 115, 34, 58, 53, 44, 34, 112, 101, 114, 105, 111, 100, 34, 58, 51, 48, 44, 34, 97, 108, 103, 111, 114, 105, 116, 104, 109, 34, 58, 34, 83, 72, 65, 49, 34, 44, 34, 116, 111, 107, 101, 110, 84, 121, 112, 101, 34, 58, 34, 83, 84, 69, 65, 77, 34, 44, 34, 115, 111, 117, 114, 99, 101, 34, 58, 34, 77, 97, 110, 117, 97, 108, 34, 125, 44, 34, 111, 114, 100, 101,' + '114, 34, 58, 123, 34, 112, 111, 115, 105, 116, 105, 111, 110, 34, 58, 50, 125, 44, 34, 105, 99, 111, 110, 34, 58, 123, 34, 115, 101, 108, 101, 99, 116, 101, 100, 34, 58, 34, 76, 97, 98, 101, 108, 34, 44, 34, 108, 97, 98, 101, 108, 34, 58, 123, 34, 116, 101, 120, 116, 34, 58, 34, 83, 84, 34, 44, 34, 98, 97, 99, 107, 103, 114, 111, 117, 110, 100, 67, 111, 108, 111, 114, 34, 58, 34, 76, 105, 103, 104, 116, 66, 108, 117, 101, 34, 125, 44, 34, 105, 99,' + '111, 110, 67, 111, 108, 108, 101, 99, 116, 105, 111, 110, 34, 58, 123, 34, 105, 100, 34, 58, 34, 97, 53, 98, 51, 102, 98, 54, 53, 45, 52, 101, 99, 53, 45, 52, 51, 101, 54, 45, 56, 101, 99, 49, 45, 52, 57, 101, 50, 52, 99, 97, 57, 101, 55, 97, 100, 34, 125, 125, 125, 93, 44, 34, 103, 114, 111, 117, 112, 115, 34, 58, 91, 93, 44, 34, 117, 112, 100, 97, 116, 101, 100, 65, 116, 34, 58, 49, 55, 49, 51, 53, 50, 56, 56, 49, 57, 54, 54, 48,' + '44, 34, 115, 99, 104, 101, 109, 97, 86, 101, 114, 115, 105, 111, 110, 34, 58, 52, 44, 34, 97, 112, 112, 86, 101, 114, 115, 105, 111, 110, 67, 111, 100, 101, 34, 58, 53, 48, 48, 48, 48, 49, 57, 44, 34, 97, 112, 112, 86, 101, 114, 115, 105, 111, 110, 78, 97, 109, 101, 34, 58, 34, 53, 46, 52, 46, 48, 34, 44, 34, 97, 112, 112, 79, 114, 105, 103, 105, 110, 34, 58, 34, 97, 110, 100, 114, 111, 105, 100, 34, 125]'; + final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast(); + final jsonFile = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Two_Fas_plain.json'); + group('plain', () { + group('fileIsValid', () { + test('isTrue', () async { + // Act + final fileIsValid = await processor.fileIsValid(jsonFile); + // Assert + expect(fileIsValid, isTrue); + }); + test('isFalse', () async { + // Arrange + final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast()..removeLast(); + final jsonFileInvalid = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Two_Fas_plain_invalid.json'); + // Act + final fileIsValid = await processor.fileIsValid(jsonFileInvalid); + // Assert + expect(fileIsValid, isFalse); + }); + }); + test('fileNeedsPassword', () async { + // Act + final fileNeedsPassword = await processor.fileNeedsPassword(jsonFile); + // Assert + expect(fileNeedsPassword, isFalse); + }); + test('processFile', () async { + // Act + final results = await processor.processFile(jsonFile); + // Assert + _assertSuccessResults(results); + }); + }); + group('encrypted', () { + const jsonFileBytesString = + '[123, 34, 115, 101, 114, 118, 105, 99, 101, 115, 34, 58, 91, 93, 44, 34, 103, 114, 111, 117, 112, 115, 34, 58, 91, 93, 44, 34, 117, 112, 100, 97, 116, 101, 100, 65, 116, 34, 58, 49, 55, 49, 51, 53, 50, 56, 56, 49, 50, 55, 48, 51, 44, 34, 115, 99, 104, 101, 109, 97, 86, 101, 114, 115, 105, 111, 110, 34, 58, 52, 44, 34, 97, 112, 112, 86, 101, 114, 115, 105, 111, 110, 67, 111, 100, 101, 34, 58, 53, 48, 48, 48, 48, 49, 57, 44, 34, 97, 112, 112,' + '86, 101, 114, 115, 105, 111, 110, 78, 97, 109, 101, 34, 58, 34, 53, 46, 52, 46, 48, 34, 44, 34, 97, 112, 112, 79, 114, 105, 103, 105, 110, 34, 58, 34, 97, 110, 100, 114, 111, 105, 100, 34, 44, 34, 115, 101, 114, 118, 105, 99, 101, 115, 69, 110, 99, 114, 121, 112, 116, 101, 100, 34, 58, 34, 110, 121, 115, 88, 84, 84, 50, 86, 51, 78, 122, 111, 54, 48, 110, 113, 108, 118, 90, 99, 89, 83, 80, 108, 102, 65, 54, 57, 81, 54, 52, 86, 78, 105, 101, 113,' + '76, 100, 107, 53, 112, 53, 75, 88, 104, 66, 104, 106, 65, 68, 99, 80, 109, 71, 120, 102, 99, 83, 71, 122, 66, 100, 107, 116, 109, 52, 114, 53, 71, 114, 107, 103, 67, 116, 107, 113, 115, 86, 97, 121, 71, 48, 116, 78, 116, 122, 54, 67, 79, 106, 70, 71, 57, 119, 43, 108, 43, 70, 119, 88, 84, 78, 89, 108, 71, 115, 55, 56, 73, 113, 67, 51, 86, 102, 68, 81, 72, 52, 72, 75, 104, 76, 48, 97, 83, 107, 106, 51, 86, 55, 100, 76, 90, 76, 101, 70,' + '86, 78, 106, 75, 69, 76, 68, 56, 90, 111, 97, 109, 89, 49, 106, 107, 75, 52, 65, 79, 86, 121, 57, 97, 107, 49, 98, 122, 56, 106, 82, 117, 119, 115, 53, 100, 54, 66, 103, 103, 109, 47, 121, 122, 105, 119, 49, 102, 111, 57, 77, 72, 56, 101, 90, 101, 108, 47, 74, 111, 78, 68, 72, 74, 117, 68, 78, 43, 105, 78, 86, 83, 99, 66, 119, 120, 80, 119, 53, 55, 67, 52, 105, 113, 68, 79, 48, 70, 83, 103, 76, 85, 121, 69, 68, 103, 70, 56, 48, 80,' + '121, 43, 114, 57, 66, 52, 54, 83, 51, 85, 72, 101, 116, 66, 43, 99, 113, 57, 76, 68, 122, 69, 55, 55, 122, 68, 67, 111, 48, 111, 66, 114, 112, 48, 86, 80, 70, 113, 50, 72, 68, 120, 43, 85, 81, 108, 68, 55, 65, 50, 120, 114, 115, 72, 90, 108, 54, 90, 80, 66, 89, 83, 75, 108, 90, 117, 70, 83, 100, 55, 65, 99, 54, 89, 105, 108, 98, 98, 113, 82, 69, 57, 80, 108, 110, 113, 89, 112, 103, 116, 69, 117, 50, 108, 51, 43, 43, 78, 56, 56,' + '112, 55, 116, 101, 74, 109, 50, 120, 114, 89, 110, 90, 102, 67, 103, 107, 72, 48, 104, 57, 99, 116, 51, 55, 55, 116, 74, 70, 73, 76, 100, 111, 88, 54, 119, 68, 108, 112, 113, 117, 56, 43, 113, 47, 119, 102, 121, 121, 48, 112, 107, 105, 102, 109, 101, 48, 80, 77, 109, 76, 122, 69, 52, 87, 101, 80, 122, 121, 122, 85, 82, 105, 84, 83, 101, 83, 99, 121, 76, 71, 66, 100, 107, 119, 66, 48, 70, 110, 100, 66, 98, 122, 111, 122, 109, 82, 87, 52, 75, 108,' + '54, 112, 71, 50, 121, 82, 107, 120, 72, 53, 55, 77, 75, 103, 109, 120, 70, 72, 114, 103, 114, 49, 54, 49, 53, 119, 83, 72, 67, 106, 119, 122, 56, 74, 89, 73, 118, 120, 43, 99, 49, 77, 47, 119, 106, 85, 70, 56, 108, 71, 85, 74, 113, 48, 89, 82, 109, 97, 84, 47, 87, 108, 74, 77, 121, 50, 73, 86, 108, 97, 43, 83, 121, 72, 52, 80, 50, 80, 116, 100, 47, 113, 77, 85, 51, 106, 107, 47, 80, 115, 69, 52, 77, 55, 113, 90, 55, 107, 98, 104,' + '98, 98, 112, 47, 100, 100, 48, 89, 113, 50, 99, 73, 90, 75, 87, 102, 85, 72, 75, 66, 72, 52, 48, 110, 74, 67, 107, 72, 81, 100, 51, 56, 43, 103, 73, 112, 114, 48, 67, 104, 77, 88, 85, 65, 120, 114, 115, 80, 104, 74, 83, 71, 89, 119, 97, 105, 52, 122, 105, 97, 83, 98, 49, 54, 52, 117, 85, 114, 85, 104, 66, 43, 97, 105, 73, 51, 80, 48, 47, 74, 76, 99, 82, 116, 78, 121, 99, 79, 100, 97, 105, 71, 102, 85, 53, 72, 98, 105, 47, 81,' + '108, 72, 121, 51, 121, 82, 111, 74, 50, 107, 118, 102, 69, 52, 66, 47, 80, 54, 70, 57, 78, 106, 71, 89, 57, 105, 113, 83, 107, 97, 114, 54, 114, 121, 76, 75, 110, 104, 116, 87, 68, 74, 86, 81, 69, 99, 105, 78, 111, 52, 68, 117, 76, 72, 75, 49, 86, 88, 118, 101, 67, 106, 72, 76, 50, 76, 97, 43, 87, 73, 84, 65, 83, 78, 84, 110, 100, 70, 85, 112, 43, 52, 79, 87, 111, 118, 43, 98, 75, 70, 85, 113, 115, 106, 43, 100, 85, 75, 81, 106,' + '116, 83, 99, 102, 110, 70, 111, 77, 86, 90, 118, 86, 71, 69, 98, 121, 113, 53, 100, 76, 74, 90, 102, 72, 90, 105, 90, 72, 111, 111, 116, 87, 47, 103, 87, 82, 57, 65, 107, 52, 122, 65, 66, 54, 51, 87, 120, 104, 80, 79, 105, 51, 81, 82, 79, 68, 109, 101, 112, 115, 56, 86, 122, 70, 79, 50, 105, 81, 83, 98, 75, 80, 101, 90, 118, 112, 79, 88, 89, 102, 43, 89, 84, 84, 99, 73, 57, 84, 57, 109, 98, 70, 109, 99, 49, 102, 72, 87, 87, 114,' + '55, 56, 66, 50, 119, 76, 119, 88, 110, 107, 102, 107, 83, 49, 116, 52, 114, 78, 57, 75, 100, 116, 120, 118, 97, 67, 48, 120, 88, 68, 99, 65, 89, 101, 103, 121, 85, 121, 110, 101, 119, 48, 110, 120, 120, 52, 53, 111, 52, 78, 121, 121, 82, 55, 115, 88, 85, 111, 112, 71, 47, 57, 48, 119, 110, 55, 57, 87, 77, 66, 112, 104, 119, 86, 109, 116, 56, 117, 88, 118, 108, 47, 51, 79, 83, 73, 56, 51, 120, 122, 114, 71, 48, 111, 90, 66, 52, 71, 103, 99,' + '85, 74, 101, 47, 84, 118, 115, 47, 56, 43, 66, 73, 56, 106, 115, 43, 68, 106, 50, 83, 55, 99, 68, 49, 98, 83, 78, 48, 121, 86, 117, 85, 53, 48, 56, 55, 120, 104, 54, 100, 107, 48, 79, 115, 87, 110, 121, 102, 88, 54, 119, 111, 43, 71, 71, 119, 89, 116, 84, 86, 106, 104, 67, 55, 82, 53, 115, 74, 65, 53, 88, 52, 112, 89, 114, 117, 51, 72, 88, 56, 72, 50, 68, 116, 71, 76, 112, 108, 112, 80, 107, 72, 90, 54, 119, 74, 49, 75, 56, 72,' + '72, 103, 53, 111, 108, 119, 102, 68, 48, 122, 87, 101, 102, 107, 53, 99, 47, 77, 51, 54, 110, 90, 85, 73, 100, 51, 52, 86, 121, 86, 80, 88, 74, 67, 76, 119, 72, 99, 47, 122, 106, 71, 68, 89, 77, 83, 77, 117, 75, 72, 122, 69, 43, 103, 107, 71, 53, 81, 86, 55, 76, 54, 53, 100, 99, 89, 111, 75, 116, 83, 87, 84, 51, 83, 116, 87, 101, 88, 49, 47, 107, 88, 67, 115, 101, 100, 50, 48, 108, 106, 102, 87, 101, 82, 109, 109, 52, 97, 112, 51,' + '68, 119, 43, 88, 100, 69, 47, 68, 81, 101, 75, 99, 43, 119, 83, 81, 76, 114, 104, 101, 122, 65, 72, 119, 84, 57, 53, 120, 109, 70, 108, 76, 89, 110, 74, 52, 50, 82, 90, 54, 74, 89, 97, 87, 68, 76, 68, 54, 57, 69, 108, 82, 90, 86, 104, 121, 75, 75, 84, 112, 99, 116, 71, 103, 85, 114, 83, 87, 119, 77, 50, 54, 114, 77, 72, 52, 83, 68, 117, 103, 65, 77, 56, 118, 76, 87, 118, 114, 74, 55, 50, 51, 108, 75, 100, 83, 109, 121, 47, 88,' + '99, 86, 112, 67, 119, 69, 107, 78, 65, 100, 77, 51, 87, 109, 77, 84, 100, 84, 47, 98, 89, 106, 97, 68, 103, 86, 117, 56, 77, 88, 86, 89, 74, 98, 49, 122, 70, 50, 57, 66, 106, 65, 117, 66, 110, 113, 99, 81, 50, 48, 85, 106, 108, 120, 50, 100, 52, 115, 99, 70, 56, 57, 105, 114, 101, 84, 87, 77, 52, 65, 112, 57, 47, 119, 101, 114, 66, 119, 104, 43, 119, 100, 43, 53, 57, 68, 99, 112, 117, 84, 71, 66, 79, 86, 110, 97, 109, 48, 90, 77,' + '99, 67, 111, 105, 118, 88, 105, 103, 75, 65, 52, 100, 87, 51, 107, 100, 69, 65, 50, 71, 43, 81, 113, 110, 65, 108, 50, 65, 78, 84, 117, 97, 79, 104, 90, 43, 100, 83, 68, 70, 105, 116, 83, 90, 49, 103, 104, 55, 47, 114, 90, 97, 112, 68, 106, 73, 119, 49, 53, 106, 81, 52, 117, 103, 55, 106, 107, 88, 48, 118, 99, 52, 108, 97, 116, 113, 90, 66, 65, 79, 110, 114, 118, 68, 72, 119, 61, 61, 58, 54, 68, 114, 79, 56, 88, 72, 77, 111, 87, 57,' + '117, 77, 120, 75, 57, 122, 112, 122, 57, 103, 122, 53, 81, 86, 78, 122, 104, 78, 90, 83, 90, 77, 106, 89, 50, 81, 75, 72, 113, 114, 118, 109, 57, 47, 69, 57, 67, 119, 101, 54, 104, 81, 121, 108, 55, 73, 70, 51, 109, 43, 109, 52, 66, 87, 53, 87, 49, 101, 117, 102, 108, 105, 66, 81, 109, 119, 97, 115, 98, 76, 51, 77, 86, 65, 84, 79, 65, 88, 109, 57, 97, 84, 98, 111, 89, 119, 102, 86, 86, 105, 50, 71, 87, 98, 74, 119, 105, 120, 54, 47,' + '99, 116, 70, 48, 52, 101, 43, 102, 120, 90, 87, 119, 77, 80, 115, 82, 109, 53, 82, 51, 56, 102, 67, 122, 68, 54, 56, 82, 78, 67, 54, 86, 68, 51, 101, 122, 114, 54, 43, 52, 56, 85, 68, 120, 116, 112, 66, 72, 117, 69, 71, 47, 78, 78, 101, 77, 66, 70, 51, 43, 122, 69, 86, 89, 74, 98, 112, 114, 116, 79, 79, 75, 113, 102, 101, 43, 101, 50, 84, 66, 85, 100, 106, 77, 84, 74, 72, 108, 52, 57, 70, 70, 83, 89, 83, 50, 73, 102, 68, 48,' + '105, 90, 73, 117, 77, 102, 105, 68, 53, 87, 76, 54, 67, 99, 69, 114, 76, 113, 54, 75, 98, 86, 73, 49, 65, 43, 74, 115, 98, 115, 80, 87, 108, 86, 109, 111, 79, 84, 112, 53, 43, 57, 116, 98, 67, 101, 83, 99, 56, 112, 52, 87, 110, 72, 49, 57, 70, 55, 81, 56, 105, 84, 103, 72, 89, 77, 88, 52, 89, 83, 108, 111, 121, 47, 68, 90, 70, 108, 119, 52, 88, 79, 70, 100, 110, 102, 77, 98, 53, 70, 70, 57, 69, 79, 49, 115, 54, 48, 87, 77,' + '116, 76, 56, 65, 72, 109, 98, 107, 121, 53, 56, 57, 116, 97, 105, 43, 108, 78, 72, 88, 52, 77, 102, 55, 112, 80, 57, 107, 121, 89, 119, 61, 61, 58, 97, 54, 111, 108, 99, 101, 88, 80, 56, 110, 84, 52, 87, 71, 48, 84, 34, 44, 34, 114, 101, 102, 101, 114, 101, 110, 99, 101, 34, 58, 34, 113, 100, 49, 49, 78, 83, 79, 65, 54, 115, 72, 52, 106, 70, 67, 66, 78, 75, 122, 48, 117, 43, 86, 48, 82, 120, 85, 86, 86, 56, 90, 57, 73, 102, 113,' + '55, 54, 65, 97, 100, 121, 77, 89, 104, 69, 121, 101, 121, 90, 109, 100, 73, 119, 110, 48, 99, 50, 101, 103, 87, 43, 86, 55, 73, 66, 73, 114, 76, 75, 57, 66, 43, 117, 98, 90, 76, 117, 53, 86, 83, 68, 77, 100, 103, 66, 90, 108, 97, 73, 47, 52, 54, 103, 98, 48, 84, 79, 53, 67, 88, 102, 79, 118, 113, 55, 47, 66, 97, 112, 76, 87, 107, 71, 68, 111, 114, 65, 108, 110, 71, 75, 116, 113, 100, 84, 97, 70, 109, 109, 70, 86, 65, 80, 47, 79,' + '48, 118, 57, 84, 122, 73, 53, 78, 56, 113, 98, 102, 98, 79, 120, 110, 84, 113, 86, 66, 48, 65, 43, 82, 73, 106, 50, 85, 50, 99, 79, 121, 66, 80, 108, 90, 52, 89, 82, 97, 105, 113, 88, 79, 89, 86, 65, 68, 57, 105, 76, 74, 73, 113, 68, 43, 66, 104, 100, 103, 121, 108, 83, 82, 105, 50, 89, 100, 118, 113, 84, 48, 106, 84, 51, 71, 88, 52, 113, 90, 65, 116, 104, 111, 70, 78, 76, 54, 55, 120, 86, 83, 112, 71, 106, 118, 53, 85, 57, 80,' + '110, 50, 114, 76, 80, 85, 111, 73, 112, 66, 49, 75, 113, 117, 121, 101, 104, 104, 106, 66, 86, 70, 67, 121, 99, 67, 72, 83, 115, 66, 56, 119, 55, 80, 83, 76, 115, 118, 51, 82, 89, 100, 77, 71, 82, 55, 110, 106, 72, 79, 103, 57, 114, 86, 71, 86, 79, 89, 71, 77, 51, 109, 52, 76, 80, 112, 89, 118, 88, 50, 103, 76, 73, 66, 55, 118, 99, 98, 100, 49, 65, 104, 111, 55, 54, 100, 106, 119, 111, 89, 79, 122, 49, 66, 101, 99, 102, 67, 114, 105,' + '87, 47, 101, 80, 113, 78, 52, 101, 84, 79, 77, 51, 51, 107, 43, 106, 69, 101, 52, 43, 86, 76, 90, 89, 77, 101, 43, 56, 61, 58, 54, 68, 114, 79, 56, 88, 72, 77, 111, 87, 57, 117, 77, 120, 75, 57, 122, 112, 122, 57, 103, 122, 53, 81, 86, 78, 122, 104, 78, 90, 83, 90, 77, 106, 89, 50, 81, 75, 72, 113, 114, 118, 109, 57, 47, 69, 57, 67, 119, 101, 54, 104, 81, 121, 108, 55, 73, 70, 51, 109, 43, 109, 52, 66, 87, 53, 87, 49, 101, 117,' + '102, 108, 105, 66, 81, 109, 119, 97, 115, 98, 76, 51, 77, 86, 65, 84, 79, 65, 88, 109, 57, 97, 84, 98, 111, 89, 119, 102, 86, 86, 105, 50, 71, 87, 98, 74, 119, 105, 120, 54, 47, 99, 116, 70, 48, 52, 101, 43, 102, 120, 90, 87, 119, 77, 80, 115, 82, 109, 53, 82, 51, 56, 102, 67, 122, 68, 54, 56, 82, 78, 67, 54, 86, 68, 51, 101, 122, 114, 54, 43, 52, 56, 85, 68, 120, 116, 112, 66, 72, 117, 69, 71, 47, 78, 78, 101, 77, 66, 70, 51,' + '43, 122, 69, 86, 89, 74, 98, 112, 114, 116, 79, 79, 75, 113, 102, 101, 43, 101, 50, 84, 66, 85, 100, 106, 77, 84, 74, 72, 108, 52, 57, 70, 70, 83, 89, 83, 50, 73, 102, 68, 48, 105, 90, 73, 117, 77, 102, 105, 68, 53, 87, 76, 54, 67, 99, 69, 114, 76, 113, 54, 75, 98, 86, 73, 49, 65, 43, 74, 115, 98, 115, 80, 87, 108, 86, 109, 111, 79, 84, 112, 53, 43, 57, 116, 98, 67, 101, 83, 99, 56, 112, 52, 87, 110, 72, 49, 57, 70, 55, 81,' + '56, 105, 84, 103, 72, 89, 77, 88, 52, 89, 83, 108, 111, 121, 47, 68, 90, 70, 108, 119, 52, 88, 79, 70, 100, 110, 102, 77, 98, 53, 70, 70, 57, 69, 79, 49, 115, 54, 48, 87, 77, 116, 76, 56, 65, 72, 109, 98, 107, 121, 53, 56, 57, 116, 97, 105, 43, 108, 78, 72, 88, 52, 77, 102, 55, 112, 80, 57, 107, 121, 89, 119, 61, 61, 58, 71, 103, 119, 105, 68, 99, 73, 79, 86, 84, 117, 68, 76, 80, 75, 75, 34, 125]'; + final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast(); + final jsonFile = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Two_Fas_encrypted.json'); + group('fileIsValid', () { + test('isTrue', () async { + // Act + final fileIsValid = await processor.fileIsValid(jsonFile); + // Assert + expect(fileIsValid, isTrue); + }); + test('isFalse', () async { + // Arrange + final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast()..removeLast(); + final jsonFileInvalid = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Two_Fas_encrypted_invalid.json'); + // Act + final fileIsValid = await processor.fileIsValid(jsonFileInvalid); + // Assert + expect(fileIsValid, isFalse); + }); + }); + test('fileNeedsPassword', () async { + // Act + final fileNeedsPassword = await processor.fileNeedsPassword(jsonFile); + // Assert + expect(fileNeedsPassword, isTrue); + }); + test('processFile', () async { + // Arrange + const password = 'test123'; + // Act + final results = await processor.processFile(jsonFile, password: password); + // Assert + _assertSuccessResults(results); + }); + }); + }); + }); +} diff --git a/test/unit_test/state_notifiers/deeplink_notifier_test.dart b/test/unit_test/state_notifiers/deeplink_notifier_test.dart new file mode 100644 index 000000000..1ffb2a5f9 --- /dev/null +++ b/test/unit_test/state_notifiers/deeplink_notifier_test.dart @@ -0,0 +1,91 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/state_notifiers/deeplink_notifier.dart'; +import 'package:privacyidea_authenticator/utils/riverpod_state_listener.dart'; + +void main() { + _testDeeplinkNotifier(); +} + +void _testDeeplinkNotifier() { + group('Deeplink Notifier Test', () { + test('initUri', () { + final container = ProviderContainer(); + final initUri = Uri.parse('otpauth://hotp/issuer?secret=secret&counter=0&digits=6&algorithm=SHA1'); + final deeplinkProvider = StateNotifierProvider( + (ref) => DeeplinkNotifier(sources: [DeeplinkSource(name: 'test', stream: const Stream.empty(), initialUri: Future.value(initUri))]), + ); + container.listen(deeplinkProvider, (prev, next) { + expect(prev, isNull); + expect(next, isNotNull); + expect(next!.uri, equals(initUri)); + }); + }); + test('initUri multible', () async { + final container = ProviderContainer(); + final initUri = Uri.parse('otpauth://hotp/issuer?secret=secret&counter=0&digits=6&algorithm=SHA1'); + final initUri2 = Uri.parse('otpauth://totp/issuer?secret=secret&period=30&digits=6&algorithm=SHA1'); + final deeplinkProvider = StateNotifierProvider( + (ref) => DeeplinkNotifier(sources: [ + DeeplinkSource(name: 'test', stream: const Stream.empty(), initialUri: Future.value(initUri)), + DeeplinkSource(name: 'test2', stream: const Stream.empty(), initialUri: Future.value(initUri2)), + ]), + ); + container.listen(deeplinkProvider, (prev, next) { + // There should be only one initial uri, others will be ignored + expect(prev, isNull); + expect(next, isNotNull); + expect(next!.uri, equals(initUri)); + }); + }); + test('stream uri', () { + final container = ProviderContainer(); + final uri1 = Uri.parse('otpauth://hotp/issuer?secret=secret&counter=0&digits=6&algorithm=SHA1'); + final uri2 = Uri.parse('otpauth://totp/issuer?secret=secret&period=30&digits=6&algorithm=SHA1'); + final uri3 = Uri.parse('otpauth://totp/issuer?secret=secret&period=60&digits=6&algorithm=SHA1'); + final uri4 = Uri.parse('otpauth://totp/issuer?secret=secret&period=90&digits=6&algorithm=SHA1'); + final list = [uri1, uri2, uri3, uri4]; + Stream stream = Stream.fromIterable([...list]); + final deeplinkProvider = StateNotifierProvider( + (ref) => DeeplinkNotifier(sources: [ + DeeplinkSource(name: 'test', stream: stream, initialUri: Future.value(null)), + ]), + ); + container.listen(deeplinkProvider, (prev, next) { + print('prev: $prev, next: $next'); + expect(next?.uri, equals(list.removeAt(0))); + expect(next?.fromInit, isFalse); + }); + }); + test('stream uri multible', () { + final container = ProviderContainer(); + final hotp1 = Uri.parse('otpauth://hotp/issuer?secret=secret&counter=1&digits=6&algorithm=SHA1'); + final hotp2 = Uri.parse('otpauth://hotp/issuer?secret=secret&counter=2&digits=6&algorithm=SHA1'); + final hotp3 = Uri.parse('otpauth://hotp/issuer?secret=secret&counter=3&digits=6&algorithm=SHA1'); + final hotp4 = Uri.parse('otpauth://hotp/issuer?secret=secret&counter=4&digits=6&algorithm=SHA1'); + final totp1 = Uri.parse('otpauth://totp/issuer?secret=secret&period=15&digits=6&algorithm=SHA1'); + final totp2 = Uri.parse('otpauth://totp/issuer?secret=secret&period=30&digits=6&algorithm=SHA1'); + final totp3 = Uri.parse('otpauth://totp/issuer?secret=secret&period=60&digits=6&algorithm=SHA1'); + final totp4 = Uri.parse('otpauth://totp/issuer?secret=secret&period=90&digits=6&algorithm=SHA1'); + final hotpList = [hotp1, hotp2, hotp3, hotp4]; + final totpList = [totp1, totp2, totp3, totp4]; + + Stream hotpStream = Stream.fromIterable([...hotpList]); + Stream totpStream = Stream.fromIterable([...totpList]); + final deeplinkProvider = StateNotifierProvider( + (ref) => DeeplinkNotifier(sources: [ + DeeplinkSource(name: 'HOTPs', stream: hotpStream, initialUri: Future.value(null)), + DeeplinkSource(name: 'TOTPs', stream: totpStream, initialUri: Future.value(null)), + ]), + ); + container.listen(deeplinkProvider, (prev, next) { + if (hotpList.length == totpList.length) { + expect(next?.uri, equals(hotpList.removeAt(0))); + } else { + expect(next?.uri, equals(totpList.removeAt(0))); + } + expect(next?.fromInit, isFalse); + }); + }); + }); +} diff --git a/test/unit_test/state_notifiers/push_request_notifier_test.dart b/test/unit_test/state_notifiers/push_request_notifier_test.dart index f083b58d3..892435616 100644 --- a/test/unit_test/state_notifiers/push_request_notifier_test.dart +++ b/test/unit_test/state_notifiers/push_request_notifier_test.dart @@ -2,11 +2,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart'; import 'package:mockito/mockito.dart'; +import 'package:privacyidea_authenticator/interfaces/repo/push_request_repository.dart'; import 'package:privacyidea_authenticator/model/push_request.dart'; -import 'package:privacyidea_authenticator/model/push_request_queue.dart'; +import 'package:privacyidea_authenticator/model/states/push_request_state.dart'; import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; import 'package:privacyidea_authenticator/state_notifiers/push_request_notifier.dart'; -import 'package:privacyidea_authenticator/utils/firebase_utils.dart'; +import 'package:privacyidea_authenticator/utils/custom_int_buffer.dart'; import 'package:privacyidea_authenticator/utils/network_utils.dart'; import 'package:privacyidea_authenticator/utils/push_provider.dart'; import 'package:privacyidea_authenticator/utils/rsa_utils.dart'; @@ -14,123 +15,196 @@ import 'package:mockito/annotations.dart'; import 'push_request_notifier_test.mocks.dart'; -class _MockPushProvider extends Mock implements PushProvider { - @override - PushRequestNotifier? pushSubscriber; - @override - Future initialize({required PushRequestNotifier pushSubscriber, required FirebaseUtils firebaseUtils}) async { - this.pushSubscriber = pushSubscriber; - } - - void simulatePush(PushRequest pushRequest) { - pushSubscriber?.newRequest(pushRequest); - } -} - -@GenerateMocks([RsaUtils, PrivacyIdeaIOClient, FirebaseUtils]) +@GenerateMocks([RsaUtils, PrivacyIdeaIOClient, PushProvider, PushRequestRepository]) void main() { _testPushRequestNotifier(); } void _testPushRequestNotifier() { group('PushRequestNotifier', () { - test('newRequest', () async { + test('accept', () async { final container = ProviderContainer(); - final mockPushProvider = _MockPushProvider(); - final mockFirebaseUtils = MockFirebaseUtils(); - final notifier = PushRequestNotifier( - pushProvider: mockPushProvider, - firebaseUtils: mockFirebaseUtils, - ioClient: MockPrivacyIdeaIOClient(), - rsaUtils: MockRsaUtils(), - ); - final testProvider = StateNotifierProvider((ref) => notifier); - await mockPushProvider.initialize(pushSubscriber: notifier, firebaseUtils: mockFirebaseUtils); + final mockIoClient = MockPrivacyIdeaIOClient(); + final mockPushProvider = MockPushProvider(); + final mockRsaUtils = MockRsaUtils(); + final mockPushRepo = MockPushRequestRepository(); + final provider = StateNotifierProvider((ref) => PushRequestNotifier( + ioClient: mockIoClient, + pushProvider: mockPushProvider, + rsaUtils: mockRsaUtils, + pushRepo: mockPushRepo, + )); final pr = PushRequest( title: 'title', question: 'question', - uri: Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100'), + uri: Uri.parse('http://example.com'), nonce: 'nonce', sslVerify: false, id: 1, - expirationDate: DateTime.now().add(const Duration(minutes: 10)), + expirationDate: DateTime.now().add(const Duration(minutes: 5)), + signature: 'signature', + serial: 'serial', + accepted: null, ); - mockPushProvider.simulatePush(pr); - expect(container.read(testProvider), pr); + final before = PushRequestState(pushRequests: [pr], knownPushRequests: CustomIntBuffer(list: [pr.id])); + final after = PushRequestState(pushRequests: [], knownPushRequests: CustomIntBuffer(list: [pr.id])); + when(mockPushRepo.loadState()).thenAnswer((_) async => before); + when(mockRsaUtils.trySignWithToken(any, any)).thenAnswer((_) async => 'signature'); + when(mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + )).thenAnswer((_) async => Response('', 200)); + when(mockPushRepo.saveState(any)).thenAnswer((_) async {}); + when(mockPushRepo.loadState()).thenAnswer((_) async => before); + final initState = await container.read(provider.notifier).initState; + expect(initState, before); + when(mockRsaUtils.trySignWithToken(any, any)).thenAnswer((_) async => 'signature'); + when(mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + )).thenAnswer((_) async => Response('', 200)); + when(mockPushRepo.saveState(any)).thenAnswer((_) async {}); + + await container.read(provider.notifier).accept(PushToken(serial: 'serial', id: 'id'), pr); + + expect(container.read(provider), after); + verify(mockPushRepo.loadState()).called(1); + verify(mockRsaUtils.trySignWithToken(any, any)).called(1); + verify(mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + )).called(1); + verify(mockPushRepo.saveState(any)).called(2); }); - test('accept', () async { + test('decline', () async { final container = ProviderContainer(); - final mockPushProvider = _MockPushProvider(); final mockIoClient = MockPrivacyIdeaIOClient(); + final mockPushProvider = MockPushProvider(); final mockRsaUtils = MockRsaUtils(); - final mockFirebaseUtils = MockFirebaseUtils(); + final mockPushRepo = MockPushRequestRepository(); + final provider = StateNotifierProvider((ref) => PushRequestNotifier( + ioClient: mockIoClient, + pushProvider: mockPushProvider, + rsaUtils: mockRsaUtils, + pushRepo: mockPushRepo, + )); final pr = PushRequest( title: 'title', - serial: 'serial', question: 'question', - uri: Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100'), + uri: Uri.parse('http://example.com'), nonce: 'nonce', sslVerify: false, id: 1, - expirationDate: DateTime.now().add(const Duration(minutes: 10)), + expirationDate: DateTime.now().add(const Duration(minutes: 5)), + signature: 'signature', + serial: 'serial', + accepted: null, ); - final pushToken = PushToken(serial: 'serial', label: 'label', issuer: 'issuer', id: 'id', pushRequests: PushRequestQueue()..add(pr)); - when(mockRsaUtils.trySignWithToken(pushToken, any)).thenAnswer((_) async => 'signature'); + final before = PushRequestState(pushRequests: [pr], knownPushRequests: CustomIntBuffer(list: [pr.id])); + final after = PushRequestState(pushRequests: [], knownPushRequests: CustomIntBuffer(list: [pr.id])); + when(mockPushRepo.loadState()).thenAnswer((_) async => before); + when(mockRsaUtils.trySignWithToken(any, any)).thenAnswer((_) async => 'signature'); + when(mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + )).thenAnswer((_) async => Response('', 200)); + when(mockPushRepo.saveState(any)).thenAnswer((_) async {}); + when(mockPushRepo.loadState()).thenAnswer((_) async => before); + final initState = await container.read(provider.notifier).initState; + expect(initState, before); + when(mockRsaUtils.trySignWithToken(any, any)).thenAnswer((_) async => 'signature'); when(mockIoClient.doPost( - url: Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100'), - body: {'nonce': 'nonce', 'serial': 'serial', 'signature': 'signature'}, - sslVerify: false)) - .thenAnswer((_) async => Response('', 200)); - final testProvider = StateNotifierProvider((ref) { - final notifier = PushRequestNotifier( - pushProvider: mockPushProvider, - ioClient: mockIoClient, - rsaUtils: mockRsaUtils, - firebaseUtils: mockFirebaseUtils, - ); - mockPushProvider.initialize(pushSubscriber: notifier, firebaseUtils: mockFirebaseUtils); - return notifier; - }); - final notifier = container.read(testProvider.notifier); - await notifier.acceptPop(pushToken); - expect(container.read(testProvider)!.accepted, isTrue); + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + )).thenAnswer((_) async => Response('', 200)); + when(mockPushRepo.saveState(any)).thenAnswer((_) async {}); + await container.read(provider.notifier).decline(PushToken(serial: 'serial', id: 'id'), pr); + expect(container.read(provider), after); + verify(mockPushRepo.loadState()).called(1); + verify(mockRsaUtils.trySignWithToken(any, any)).called(1); + verify(mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + )).called(1); + verify(mockPushRepo.saveState(any)).called(2); }); - test('decline', () async { + + test('add', () async { final container = ProviderContainer(); - final mockPushProvider = _MockPushProvider(); final mockIoClient = MockPrivacyIdeaIOClient(); + final mockPushProvider = MockPushProvider(); final mockRsaUtils = MockRsaUtils(); - final mockFirebaseUtils = MockFirebaseUtils(); + final mockPushRepo = MockPushRequestRepository(); + final provider = StateNotifierProvider((ref) => PushRequestNotifier( + ioClient: mockIoClient, + pushProvider: mockPushProvider, + rsaUtils: mockRsaUtils, + pushRepo: mockPushRepo, + )); final pr = PushRequest( title: 'title', + question: 'question', + uri: Uri.parse('http://example.com'), + nonce: 'nonce', + sslVerify: false, + id: 1, + expirationDate: DateTime.now().add(const Duration(minutes: 5)), + signature: 'signature', serial: 'serial', + accepted: null, + ); + final pr2 = pr.copyWith(id: 2); + final before = PushRequestState(pushRequests: [pr], knownPushRequests: CustomIntBuffer(list: [pr.id])); + final after = PushRequestState(pushRequests: [pr, pr2], knownPushRequests: CustomIntBuffer(list: [pr.id, pr2.id])); + when(mockPushRepo.loadState()).thenAnswer((_) async => before); + when(mockPushRepo.saveState(any)).thenAnswer((_) async {}); + + final initState = await container.read(provider.notifier).initState; + expect(initState, before); + await container.read(provider.notifier).add(pr2); + expect(container.read(provider), after); + }); + test('remove', () async { + final container = ProviderContainer(); + final mockIoClient = MockPrivacyIdeaIOClient(); + final mockPushProvider = MockPushProvider(); + final mockRsaUtils = MockRsaUtils(); + final mockPushRepo = MockPushRequestRepository(); + final provider = StateNotifierProvider((ref) => PushRequestNotifier( + ioClient: mockIoClient, + pushProvider: mockPushProvider, + rsaUtils: mockRsaUtils, + pushRepo: mockPushRepo, + )); + final pr = PushRequest( + title: 'title', question: 'question', - uri: Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100'), + uri: Uri.parse('http://example.com'), nonce: 'nonce', sslVerify: false, id: 1, - expirationDate: DateTime.now().add(const Duration(minutes: 10)), + expirationDate: DateTime.now().add(const Duration(minutes: 5)), + signature: 'signature', + serial: 'serial', + accepted: null, ); - final pushToken = PushToken(serial: 'serial', label: 'label', issuer: 'issuer', id: 'id', pushRequests: PushRequestQueue()..add(pr)); - when(mockRsaUtils.trySignWithToken(pushToken, any)).thenAnswer((_) async => 'signature'); - when(mockIoClient.doPost( - url: Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100'), - body: {'nonce': 'nonce', 'serial': 'serial', 'signature': 'signature', 'decline': '1'}, - sslVerify: false)) - .thenAnswer((_) async => Response('', 200)); - final testProvider = StateNotifierProvider((ref) { - final notifier = PushRequestNotifier( - pushProvider: mockPushProvider, - ioClient: mockIoClient, - rsaUtils: mockRsaUtils, - firebaseUtils: mockFirebaseUtils, - ); - mockPushProvider.initialize(pushSubscriber: notifier, firebaseUtils: mockFirebaseUtils); - return notifier; - }); - final notifier = container.read(testProvider.notifier); - await notifier.declinePop(pushToken); - expect(container.read(testProvider)!.accepted, isFalse); + final pr2 = pr.copyWith(id: 2); + final before = PushRequestState(pushRequests: [pr, pr2], knownPushRequests: CustomIntBuffer(list: [pr.id, pr2.id])); + final after = PushRequestState(pushRequests: [pr], knownPushRequests: CustomIntBuffer(list: [pr.id, pr2.id])); + when(mockPushRepo.loadState()).thenAnswer((_) async => before); + when(mockPushRepo.saveState(any)).thenAnswer((_) async {}); + + final initState = await container.read(provider.notifier).initState; + expect(initState, before); + final success = await container.read(provider.notifier).remove(pr2); + expect(success, true); + expect(container.read(provider), after); }); }); } diff --git a/test/unit_test/state_notifiers/push_request_notifier_test.mocks.dart b/test/unit_test/state_notifiers/push_request_notifier_test.mocks.dart index fc7a4d4a1..2bc14bd06 100644 --- a/test/unit_test/state_notifiers/push_request_notifier_test.mocks.dart +++ b/test/unit_test/state_notifiers/push_request_notifier_test.mocks.dart @@ -3,18 +3,22 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i7; -import 'dart:typed_data' as _i6; +import 'dart:async' as _i8; +import 'dart:typed_data' as _i7; -import 'package:firebase_messaging/firebase_messaging.dart' as _i11; import 'package:http/http.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i5; +import 'package:mockito/src/dummies.dart' as _i6; import 'package:pointycastle/export.dart' as _i2; -import 'package:privacyidea_authenticator/model/tokens/push_token.dart' as _i8; -import 'package:privacyidea_authenticator/utils/firebase_utils.dart' as _i10; -import 'package:privacyidea_authenticator/utils/network_utils.dart' as _i9; -import 'package:privacyidea_authenticator/utils/rsa_utils.dart' as _i4; +import 'package:privacyidea_authenticator/interfaces/repo/push_request_repository.dart' + as _i13; +import 'package:privacyidea_authenticator/model/push_request.dart' as _i12; +import 'package:privacyidea_authenticator/model/states/push_request_state.dart' + as _i4; +import 'package:privacyidea_authenticator/model/tokens/push_token.dart' as _i9; +import 'package:privacyidea_authenticator/utils/network_utils.dart' as _i10; +import 'package:privacyidea_authenticator/utils/push_provider.dart' as _i11; +import 'package:privacyidea_authenticator/utils/rsa_utils.dart' as _i5; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -71,10 +75,21 @@ class _FakeResponse_3 extends _i1.SmartFake implements _i3.Response { ); } +class _FakePushRequestState_4 extends _i1.SmartFake + implements _i4.PushRequestState { + _FakePushRequestState_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [RsaUtils]. /// /// See the documentation for Mockito's code generation for more information. -class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { +class MockRsaUtils extends _i1.Mock implements _i5.RsaUtils { MockRsaUtils() { _i1.throwOnMissingStub(this); } @@ -102,7 +117,7 @@ class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { #serializeRSAPublicKeyPKCS1, [publicKey], ), - returnValue: _i5.dummyValue( + returnValue: _i6.dummyValue( this, Invocation.method( #serializeRSAPublicKeyPKCS1, @@ -134,7 +149,7 @@ class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { #serializeRSAPublicKeyPKCS8, [key], ), - returnValue: _i5.dummyValue( + returnValue: _i6.dummyValue( this, Invocation.method( #serializeRSAPublicKeyPKCS8, @@ -150,7 +165,7 @@ class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { #serializeRSAPrivateKeyPKCS1, [key], ), - returnValue: _i5.dummyValue( + returnValue: _i6.dummyValue( this, Invocation.method( #serializeRSAPrivateKeyPKCS1, @@ -178,8 +193,8 @@ class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { @override bool verifyRSASignature( _i2.RSAPublicKey? publicKey, - _i6.Uint8List? signedMessage, - _i6.Uint8List? signature, + _i7.Uint8List? signedMessage, + _i7.Uint8List? signature, ) => (super.noSuchMethod( Invocation.method( @@ -194,8 +209,8 @@ class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { ) as bool); @override - _i7.Future trySignWithToken( - _i8.PushToken? token, + _i8.Future trySignWithToken( + _i9.PushToken? token, String? message, ) => (super.noSuchMethod( @@ -206,17 +221,17 @@ class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { message, ], ), - returnValue: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i8.Future.value(), + ) as _i8.Future); @override - _i7.Future<_i2.AsymmetricKeyPair<_i2.RSAPublicKey, _i2.RSAPrivateKey>> + _i8.Future<_i2.AsymmetricKeyPair<_i2.RSAPublicKey, _i2.RSAPrivateKey>> generateRSAKeyPair() => (super.noSuchMethod( Invocation.method( #generateRSAKeyPair, [], ), - returnValue: _i7.Future< + returnValue: _i8.Future< _i2.AsymmetricKeyPair<_i2.RSAPublicKey, _i2.RSAPrivateKey>>.value( _FakeAsymmetricKeyPair_2<_i2.RSAPublicKey, _i2.RSAPrivateKey>( @@ -226,13 +241,13 @@ class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { [], ), )), - ) as _i7.Future< + ) as _i8.Future< _i2.AsymmetricKeyPair<_i2.RSAPublicKey, _i2.RSAPrivateKey>>); @override String createBase32Signature( _i2.RSAPrivateKey? privateKey, - _i6.Uint8List? dataToSign, + _i7.Uint8List? dataToSign, ) => (super.noSuchMethod( Invocation.method( @@ -242,7 +257,7 @@ class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { dataToSign, ], ), - returnValue: _i5.dummyValue( + returnValue: _i6.dummyValue( this, Invocation.method( #createBase32Signature, @@ -255,9 +270,9 @@ class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { ) as String); @override - _i6.Uint8List createRSASignature( + _i7.Uint8List createRSASignature( _i2.RSAPrivateKey? privateKey, - _i6.Uint8List? dataToSign, + _i7.Uint8List? dataToSign, ) => (super.noSuchMethod( Invocation.method( @@ -267,21 +282,21 @@ class MockRsaUtils extends _i1.Mock implements _i4.RsaUtils { dataToSign, ], ), - returnValue: _i6.Uint8List(0), - ) as _i6.Uint8List); + returnValue: _i7.Uint8List(0), + ) as _i7.Uint8List); } /// A class which mocks [PrivacyIdeaIOClient]. /// /// See the documentation for Mockito's code generation for more information. class MockPrivacyIdeaIOClient extends _i1.Mock - implements _i9.PrivacyIdeaIOClient { + implements _i10.PrivacyIdeaIOClient { MockPrivacyIdeaIOClient() { _i1.throwOnMissingStub(this); } @override - _i7.Future triggerNetworkAccessPermission({ + _i8.Future triggerNetworkAccessPermission({ required Uri? url, bool? sslVerify = true, bool? isRetry = false, @@ -296,11 +311,11 @@ class MockPrivacyIdeaIOClient extends _i1.Mock #isRetry: isRetry, }, ), - returnValue: _i7.Future.value(false), - ) as _i7.Future); + returnValue: _i8.Future.value(false), + ) as _i8.Future); @override - _i7.Future<_i3.Response> doPost({ + _i8.Future<_i3.Response> doPost({ required Uri? url, required Map? body, bool? sslVerify = true, @@ -315,7 +330,7 @@ class MockPrivacyIdeaIOClient extends _i1.Mock #sslVerify: sslVerify, }, ), - returnValue: _i7.Future<_i3.Response>.value(_FakeResponse_3( + returnValue: _i8.Future<_i3.Response>.value(_FakeResponse_3( this, Invocation.method( #doPost, @@ -327,10 +342,10 @@ class MockPrivacyIdeaIOClient extends _i1.Mock }, ), )), - ) as _i7.Future<_i3.Response>); + ) as _i8.Future<_i3.Response>); @override - _i7.Future<_i3.Response> doGet({ + _i8.Future<_i3.Response> doGet({ required Uri? url, required Map? parameters, bool? sslVerify = true, @@ -345,7 +360,7 @@ class MockPrivacyIdeaIOClient extends _i1.Mock #sslVerify: sslVerify, }, ), - returnValue: _i7.Future<_i3.Response>.value(_FakeResponse_3( + returnValue: _i8.Future<_i3.Response>.value(_FakeResponse_3( this, Invocation.method( #doGet, @@ -357,43 +372,188 @@ class MockPrivacyIdeaIOClient extends _i1.Mock }, ), )), - ) as _i7.Future<_i3.Response>); + ) as _i8.Future<_i3.Response>); } -/// A class which mocks [FirebaseUtils]. +/// A class which mocks [PushProvider]. /// /// See the documentation for Mockito's code generation for more information. -class MockFirebaseUtils extends _i1.Mock implements _i10.FirebaseUtils { - MockFirebaseUtils() { +class MockPushProvider extends _i1.Mock implements _i11.PushProvider { + MockPushProvider() { _i1.throwOnMissingStub(this); } @override - _i7.Future initFirebase({ - required _i7.Future Function(_i11.RemoteMessage)? foregroundHandler, - required _i7.Future Function(_i11.RemoteMessage)? backgroundHandler, - required dynamic Function(String?)? updateFirebaseToken, + bool get pollingIsEnabled => (super.noSuchMethod( + Invocation.getter(#pollingIsEnabled), + returnValue: false, + ) as bool); + + @override + set pollingIsEnabled(bool? _pollingIsEnabled) => super.noSuchMethod( + Invocation.setter( + #pollingIsEnabled, + _pollingIsEnabled, + ), + returnValueForMissingStub: null, + ); + + @override + void setPollingEnabled(bool? enablePolling) => super.noSuchMethod( + Invocation.method( + #setPollingEnabled, + [enablePolling], + ), + returnValueForMissingStub: null, + ); + + @override + _i8.Future pollForChallenges({required bool? isManually}) => + (super.noSuchMethod( + Invocation.method( + #pollForChallenges, + [], + {#isManually: isManually}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + + @override + _i8.Future pollForChallenge( + _i9.PushToken? token, { + bool? isManually = true, }) => (super.noSuchMethod( Invocation.method( - #initFirebase, + #pollForChallenge, + [token], + {#isManually: isManually}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + + @override + _i8.Future<(List<_i9.PushToken>, List<_i9.PushToken>)?> updateFirebaseToken( + [String? firebaseToken]) => + (super.noSuchMethod( + Invocation.method( + #updateFirebaseToken, + [firebaseToken], + ), + returnValue: + _i8.Future<(List<_i9.PushToken>, List<_i9.PushToken>)?>.value(), + ) as _i8.Future<(List<_i9.PushToken>, List<_i9.PushToken>)?>); + + @override + void unsubscribe(void Function(_i12.PushRequest)? newRequest) => + super.noSuchMethod( + Invocation.method( + #unsubscribe, + [newRequest], + ), + returnValueForMissingStub: null, + ); + + @override + void subscribe(void Function(_i12.PushRequest)? newRequest) => + super.noSuchMethod( + Invocation.method( + #subscribe, + [newRequest], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PushRequestRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPushRequestRepository extends _i1.Mock + implements _i13.PushRequestRepository { + MockPushRequestRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i8.Future<_i4.PushRequestState> loadState() => (super.noSuchMethod( + Invocation.method( + #loadState, [], - { - #foregroundHandler: foregroundHandler, - #backgroundHandler: backgroundHandler, - #updateFirebaseToken: updateFirebaseToken, - }, ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + returnValue: + _i8.Future<_i4.PushRequestState>.value(_FakePushRequestState_4( + this, + Invocation.method( + #loadState, + [], + ), + )), + ) as _i8.Future<_i4.PushRequestState>); + + @override + _i8.Future saveState(_i4.PushRequestState? pushRequestState) => + (super.noSuchMethod( + Invocation.method( + #saveState, + [pushRequestState], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); @override - _i7.Future getFBToken() => (super.noSuchMethod( + _i8.Future clearState() => (super.noSuchMethod( Invocation.method( - #getFBToken, + #clearState, [], ), - returnValue: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + + @override + _i8.Future<_i4.PushRequestState> add( + _i12.PushRequest? pushRequest, { + _i4.PushRequestState? state, + }) => + (super.noSuchMethod( + Invocation.method( + #add, + [pushRequest], + {#state: state}, + ), + returnValue: + _i8.Future<_i4.PushRequestState>.value(_FakePushRequestState_4( + this, + Invocation.method( + #add, + [pushRequest], + {#state: state}, + ), + )), + ) as _i8.Future<_i4.PushRequestState>); + + @override + _i8.Future<_i4.PushRequestState> remove( + _i12.PushRequest? pushRequest, { + _i4.PushRequestState? state, + }) => + (super.noSuchMethod( + Invocation.method( + #remove, + [pushRequest], + {#state: state}, + ), + returnValue: + _i8.Future<_i4.PushRequestState>.value(_FakePushRequestState_4( + this, + Invocation.method( + #remove, + [pushRequest], + {#state: state}, + ), + )), + ) as _i8.Future<_i4.PushRequestState>); } diff --git a/test/unit_test/state_notifiers/token_folder_notifier_test.dart b/test/unit_test/state_notifiers/token_folder_notifier_test.dart index 163830bbc..b698ba594 100644 --- a/test/unit_test/state_notifiers/token_folder_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_folder_notifier_test.dart @@ -22,17 +22,16 @@ void _testTokenFolderNotifier() { const before = []; const after = [TokenFolder(label: 'test', folderId: 1, isExpanded: true, isLocked: false, sortIndex: null)]; when(mockRepo.loadFolders()).thenAnswer((_) async => before); - when(mockRepo.saveOrReplaceFolders(after)).thenAnswer((_) async => []); + when(mockRepo.saveReplaceList(after)).thenAnswer((_) async => true); final testProvider = StateNotifierProvider((ref) => TokenFolderNotifier( repository: mockRepo, )); final notifier = container.read(testProvider.notifier); - await notifier.isLoading; - notifier.addFolder('test'); - await notifier.isLoading; + await notifier.initState; + await notifier.addNewFolder('test'); final state = container.read(testProvider); expect(state.folders, after); - verify(mockRepo.saveOrReplaceFolders(after)).called(1); + verify(mockRepo.saveReplaceList(after)).called(1); }); test('removeFolder', () async { @@ -41,17 +40,16 @@ void _testTokenFolderNotifier() { const before = [TokenFolder(label: 'test', folderId: 1, isExpanded: true, isLocked: false, sortIndex: null)]; const after = []; when(mockRepo.loadFolders()).thenAnswer((_) async => before); - when(mockRepo.saveOrReplaceFolders(after)).thenAnswer((_) async => []); + when(mockRepo.saveReplaceList(after)).thenAnswer((_) async => true); final testProvider = StateNotifierProvider((ref) => TokenFolderNotifier( repository: mockRepo, )); final notifier = container.read(testProvider.notifier); - await notifier.isLoading; - notifier.removeFolder(const TokenFolder(label: 'test', folderId: 1)); - await notifier.isLoading; + await notifier.initState; + await notifier.removeFolder(const TokenFolder(label: 'test', folderId: 1)); final state = container.read(testProvider); expect(state.folders, after); - verify(mockRepo.saveOrReplaceFolders(after)).called(1); + verify(mockRepo.saveReplaceList(after)).called(1); }); test('updateFolder', () async { final mockRepo = MockTokenFolderRepository(); @@ -59,17 +57,16 @@ void _testTokenFolderNotifier() { const before = [TokenFolder(label: 'test', folderId: 1, isExpanded: true, isLocked: false, sortIndex: null)]; const after = [TokenFolder(label: 'testUpdated', folderId: 1, isExpanded: true, isLocked: false, sortIndex: null)]; when(mockRepo.loadFolders()).thenAnswer((_) async => before); - when(mockRepo.saveOrReplaceFolders(after)).thenAnswer((_) async => []); + when(mockRepo.saveReplaceList(after)).thenAnswer((_) async => true); final testProvider = StateNotifierProvider((ref) => TokenFolderNotifier( repository: mockRepo, )); final notifier = container.read(testProvider.notifier); - await notifier.isLoading; - notifier.updateFolder(after.first); - await notifier.isLoading; + await notifier.initState; + await notifier.updateFolder(before.first, (p0) => after.first); final state = container.read(testProvider); expect(state.folders, after); - verify(mockRepo.saveOrReplaceFolders(after)).called(1); + verify(mockRepo.saveReplaceList(after)).called(1); }); test('updateFolders', () async { final mockRepo = MockTokenFolderRepository(); @@ -83,17 +80,16 @@ void _testTokenFolderNotifier() { TokenFolder(label: 'test2Updated', folderId: 2, isExpanded: true, isLocked: false, sortIndex: null), ]; when(mockRepo.loadFolders()).thenAnswer((_) async => before); - when(mockRepo.saveOrReplaceFolders(after)).thenAnswer((_) async => []); + when(mockRepo.saveReplaceList(after)).thenAnswer((_) async => true); final testProvider = StateNotifierProvider((ref) => TokenFolderNotifier( repository: mockRepo, )); final notifier = container.read(testProvider.notifier); - await notifier.isLoading; - notifier.updateFolders(after); - await notifier.isLoading; + await notifier.initState; + await notifier.addOrReplaceFolders(after); final state = container.read(testProvider); expect(state.folders, after); - verify(mockRepo.saveOrReplaceFolders(after)).called(1); + verify(mockRepo.saveReplaceList(after)).called(1); }); }); } diff --git a/test/unit_test/state_notifiers/token_folder_notifier_test.mocks.dart b/test/unit_test/state_notifiers/token_folder_notifier_test.mocks.dart index a03f84137..aed7f00ea 100644 --- a/test/unit_test/state_notifiers/token_folder_notifier_test.mocks.dart +++ b/test/unit_test/state_notifiers/token_folder_notifier_test.mocks.dart @@ -33,16 +33,14 @@ class MockTokenFolderRepository extends _i1.Mock } @override - _i3.Future> saveOrReplaceFolders( - List<_i4.TokenFolder>? folders) => + _i3.Future saveReplaceList(List<_i4.TokenFolder>? folders) => (super.noSuchMethod( Invocation.method( - #saveOrReplaceFolders, + #saveReplaceList, [folders], ), - returnValue: - _i3.Future>.value(<_i4.TokenFolder>[]), - ) as _i3.Future>); + returnValue: _i3.Future.value(false), + ) as _i3.Future); @override _i3.Future> loadFolders() => (super.noSuchMethod( diff --git a/test/unit_test/state_notifiers/token_notifier_test.dart b/test/unit_test/state_notifiers/token_notifier_test.dart index 1112b3237..19caaa919 100644 --- a/test/unit_test/state_notifiers/token_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_notifier_test.dart @@ -8,8 +8,8 @@ import 'package:pointycastle/export.dart'; import 'package:privacyidea_authenticator/interfaces/repo/token_repository.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; import 'package:privacyidea_authenticator/model/enums/push_token_rollout_state.dart'; -import 'package:privacyidea_authenticator/model/push_request.dart'; -import 'package:privacyidea_authenticator/model/push_request_queue.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/token_origin_source_type.dart'; import 'package:privacyidea_authenticator/model/states/token_state.dart'; import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; @@ -37,29 +37,22 @@ void main() { void _testTokenNotifier() { group('TokenNotifier', () { - test('refreshRolledOutPushTokens', () async { + test('loadStateFromRepo', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); - final before = [ - PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true, pushRequests: PushRequestQueue()), - ]; - final queue = PushRequestQueue() - ..add(PushRequest( - title: 'title', - question: 'question', - uri: Uri.parse('https://example.com'), - nonce: 'nonce', - sslVerify: false, - id: 1, - expirationDate: DateTime.now().add(const Duration(minutes: 3)))); - final after = [ - PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true, pushRequests: queue), + final mockFirebaseUtils = MockFirebaseUtils(); + final before = [PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true)]; + final after = [ + PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true), + PushToken(label: 'label2', issuer: 'issuer2', id: 'id2', serial: 'serial2', isRolledOut: true) ]; final responses = [before, after]; when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); when(mockRepo.loadTokens()).thenAnswer((_) async => responses.removeAt(0)); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); expect((await notifier.loadStateFromRepo())?.tokens, after); @@ -71,18 +64,19 @@ void _testTokenNotifier() { test('getTokenFromId', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); - final before = [ - HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), - ]; + final mockFirebaseUtils = MockFirebaseUtils(); + final before = [HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret')]; final after = before; when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); when(mockRepo.loadTokens()).thenAnswer((_) async => before); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); - await notifier.loadingRepo; - expect(notifier.getTokenFromId(before.first.id), before.first); + await notifier.initState; + expect(notifier.getTokenById(before.first.id), before.first); final state = container.read(testProvider); expect(state, isNotNull); expect(state.tokens, after); @@ -90,30 +84,33 @@ void _testTokenNotifier() { test('incrementCounter', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); - final before = [ + final mockFirebaseUtils = MockFirebaseUtils(); + final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', counter: 522), ]; - final after = [ + final after = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', counter: 523), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); + when(mockRepo.saveOrReplaceToken(after.first)).thenAnswer((_) async => true); when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); - when(mockRepo.saveOrReplaceTokens([after.first])).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier( - repository: mockRepo, - ), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); + final initState = await notifier.initState; + expect(initState.tokens, before); await notifier.incrementCounter(before.first); final state = container.read(testProvider); expect(state, isNotNull); expect(state.tokens, after); - verify(mockRepo.saveOrReplaceTokens(after)).called(1); + verify(mockRepo.saveOrReplaceToken(after.first)).called(1); }); test('removeToken', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), HOTPToken(label: 'label2', issuer: 'issuer2', id: 'id2', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2'), @@ -122,22 +119,26 @@ void _testTokenNotifier() { HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); + when(mockRepo.deleteToken(before.last)).thenAnswer((_) async => true); when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); - when(mockRepo.deleteTokens([before.last])).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); + final initState = await notifier.initState; + expect(initState.tokens, before); await notifier.removeToken(before.last); final state = container.read(testProvider); expect(state, isNotNull); expect(state.tokens, after); - verify(mockRepo.deleteTokens([before.last])).called(1); + verify(mockRepo.deleteToken(before.last)).called(1); }); group('addOrReplaceToken', () { test('add Token', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; @@ -146,23 +147,25 @@ void _testTokenNotifier() { HOTPToken(label: 'label2', issuer: 'issuer2', id: 'id2', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2'), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); + when(mockRepo.saveOrReplaceToken(after.last)).thenAnswer((_) async => true); when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); - when(mockRepo.saveOrReplaceTokens([after.last])).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier( - repository: mockRepo, - ), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); + final initState = await notifier.initState; + expect(initState.tokens, before); await notifier.addOrReplaceToken(after.last); final state = container.read(testProvider); expect(state, isNotNull); expect(state.tokens, after); - verify(mockRepo.saveOrReplaceTokens([after.last])).called(1); + verify(mockRepo.saveOrReplaceToken(after.last)).called(1); }); test('replace Token', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), HOTPToken(label: 'label2', issuer: 'issuer2', id: 'id2', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2'), @@ -172,24 +175,26 @@ void _testTokenNotifier() { HOTPToken(label: 'labelUpdated', issuer: 'issuer2Updated', id: 'id2', algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2Updated'), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); + when(mockRepo.saveOrReplaceToken(after.last)).thenAnswer((_) async => true); when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); - when(mockRepo.saveOrReplaceTokens([after.last])).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier( - repository: mockRepo, - ), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); + final initState = await notifier.initState; + expect(initState.tokens, before); await notifier.addOrReplaceToken(after.last); final state = container.read(testProvider); expect(state, isNotNull); expect(state.tokens, after); - verify(mockRepo.saveOrReplaceTokens([after.last])).called(1); + verify(mockRepo.saveOrReplaceToken(after.last)).called(1); }); }); test('addOrReplaceTokens', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; @@ -201,10 +206,9 @@ void _testTokenNotifier() { when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); when(mockRepo.saveOrReplaceTokens([...after])).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier( - repository: mockRepo, - ), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); await notifier.addOrReplaceTokens([...after]); @@ -215,6 +219,7 @@ void _testTokenNotifier() { test('addTokenFromOtpAuth', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; @@ -225,7 +230,7 @@ void _testTokenNotifier() { when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo), + (ref) => TokenNotifier(repository: mockRepo, firebaseUtils: mockFirebaseUtils), ); final notifier = container.read(testProvider.notifier); await notifier.handleQrCode('otpauth://totp/issuer2:label2?secret=secret2&issuer=issuer2&algorithm=SHA256&digits=6&period=30'); @@ -233,7 +238,7 @@ void _testTokenNotifier() { expect(state, isNotNull); after.last = after.last.copyWith(id: state.tokens.last.id); expect(state.tokens, after); - verify(mockRepo.saveOrReplaceTokens(any)).called(1); + verify(mockRepo.saveOrReplaceTokens(any)).called(greaterThan(0)); }); test('addTokenFromOtpAuth: rolloutPushToken', () async { final container = ProviderContainer(); @@ -254,7 +259,6 @@ void _testTokenNotifier() { final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; - final pushTokenShouldBe = PushToken( label: 'PIPU0006BF18', issuer: 'privacyIDEA', @@ -273,7 +277,7 @@ void _testTokenNotifier() { publicServerKey: publicServerKeyString, publicTokenKey: publicTokenKeyString, privateTokenKey: privateTokenKeyString, - pushRequests: PushRequestQueue(), + origin: TokenOriginSourceType.qrScan.toTokenOrigin(), ); final after = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), @@ -281,7 +285,6 @@ void _testTokenNotifier() { ]; const otpAuth = 'otpauth://pipush/PIPU0006BF18?url=https%3A//192.168.178.30/ttype/push&ttl=10&issuer=privacyIDEA&enrollment_credential=ae60d4744ac5384515574b85f538c6a4e0c7bc82&v=1&serial=PIPU0006BF18&sslverify=0'; - when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'fbToken'); when(mockRepo.loadTokens()).thenAnswer((_) async => before); when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); @@ -291,7 +294,9 @@ void _testTokenNotifier() { when(mockRsaUtils.deserializeRSAPublicKeyPKCS1(publicServerKeyString)).thenReturn(publicServerKey); when(mockRsaUtils.deserializeRSAPublicKeyPKCS1(publicTokenKeyString)).thenReturn(publicTokenKey); when(mockRsaUtils.deserializeRSAPrivateKeyPKCS1(privateTokenKeyString)).thenReturn(privateTokenKey); - when(mockRepo.saveOrReplaceTokens([after.last])).thenAnswer((_) async => []); + when(mockRepo.saveOrReplaceTokens([after.last])).thenAnswer((_) async => []); // QrCode can contain multiple tokens + when(mockRepo.saveOrReplaceToken(after.last)).thenAnswer((_) async => true); // Rollout one by one + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); when(mockIOClient.doPost( url: anyNamed('url'), body: anyNamed('body'), @@ -304,9 +309,15 @@ void _testTokenNotifier() { ), ), ); - - final notifier = TokenNotifier(repository: mockRepo, rsaUtils: mockRsaUtils, ioClient: mockIOClient, firebaseUtils: mockFirebaseUtils); - final testProvider = StateNotifierProvider((ref) => notifier); + final testProvider = StateNotifierProvider((ref) => TokenNotifier( + repository: mockRepo, + rsaUtils: mockRsaUtils, + ioClient: mockIOClient, + firebaseUtils: mockFirebaseUtils, + )); + final notifier = container.read(testProvider.notifier); + final initState = await notifier.initState; + expect(initState.tokens, before); await notifier.handleQrCode(otpAuth); final tokenState = container.read(testProvider); expect(tokenState, isNotNull); @@ -325,7 +336,6 @@ void _testTokenNotifier() { expect(pushToken.rolloutState, pushTokenShouldBe.rolloutState); expect(pushToken.serial, pushTokenShouldBe.serial); expect(pushToken.isRolledOut, pushTokenShouldBe.isRolledOut); - expect(pushToken.pushRequests, pushTokenShouldBe.pushRequests); expect(pushToken.url, pushTokenShouldBe.url); expect(pushToken.label, pushTokenShouldBe.label); expect(pushToken.issuer, pushTokenShouldBe.issuer); @@ -336,82 +346,6 @@ void _testTokenNotifier() { expect(pushToken.folderId, pushTokenShouldBe.folderId); expect(pushToken.sslVerify, pushTokenShouldBe.sslVerify); }); - test('addPushRequestToToken', () async { - final container = ProviderContainer(); - final mockRepo = MockTokenRepository(); - final mockRsaUtils = MockRsaUtils(); - final before = [ - PushToken( - label: 'label', - issuer: 'issuer', - id: 'id', - serial: 'serial', - isRolledOut: true, - pushRequests: PushRequestQueue(), - url: Uri.parse('https://example.com'), - privateTokenKey: 'privateTokenKey', - ).withPublicServerKey(RSAPublicKey(BigInt.one, BigInt.one)), - ]; - final pr = PushRequest( - title: 'title', - question: 'question', - uri: Uri.parse('https://example.com'), - nonce: 'nonce', - serial: 'serial', - sslVerify: false, - id: 1, - expirationDate: DateTime.now().add(const Duration(minutes: 3)), - ); - final after = [ - PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true, pushRequests: PushRequestQueue()..add(pr)), - ]; - when(mockRepo.loadTokens()).thenAnswer((_) async => before); - when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); - when(mockRepo.saveOrReplaceTokens([after.first])).thenAnswer((_) async => []); - when(mockRsaUtils.verifyRSASignature(any, any, any)).thenReturn(true); - final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo, rsaUtils: mockRsaUtils), - ); - final notifier = container.read(testProvider.notifier); - expect(await notifier.addPushRequestToToken(pr), true); - final state = container.read(testProvider); - expect(state, isNotNull); - expect(state.tokens, after); - verify(mockRepo.saveOrReplaceTokens([after.first])).called(1); - verify(mockRsaUtils.verifyRSASignature(any, any, any)).called(1); - }); - test('removePushRequest', () async { - final container = ProviderContainer(); - final mockRepo = MockTokenRepository(); - final pr = PushRequest( - title: 'title', - question: 'question', - uri: Uri.parse('https://example.com'), - nonce: 'nonce', - serial: 'serial', - sslVerify: false, - id: 1, - expirationDate: DateTime.now().add(const Duration(minutes: 3))); - final before = [ - PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true, pushRequests: PushRequestQueue()..add(pr)), - ]; - final after = [ - PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true, pushRequests: PushRequestQueue()), - ]; - when(mockRepo.loadTokens()).thenAnswer((_) async => before); - when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); - when(mockRepo.saveOrReplaceTokens([after.first])).thenAnswer((_) async => []); - final testProvider = StateNotifierProvider( - (ref) => TokenNotifier(repository: mockRepo), - ); - final notifier = container.read(testProvider.notifier); - await notifier.removePushRequest(pr); - final state = container.read(testProvider); - expect(state, isNotNull); - expect(state.tokens, after); - verify(mockRepo.saveOrReplaceTokens([after.first])).called(1); - }); - test('rolloutPushToken', () async { final container = ProviderContainer(); final mockRepo = MockTokenRepository(); @@ -426,8 +360,8 @@ void _testTokenNotifier() { PushToken(label: 'label', issuer: 'issuer', id: 'id', serial: 'serial', isRolledOut: true, url: uri), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); + when(mockRepo.saveOrReplaceToken(after.first)).thenAnswer((_) async => true); when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); - when(mockRepo.saveOrReplaceTokens([after.first])).thenAnswer((_) async => []); when(mockRsaUtils.serializeRSAPublicKeyPKCS8(any)).thenAnswer((_) => 'publicKey'); when(mockRsaUtils.generateRSAKeyPair()).thenAnswer((_) => const RsaUtils() .generateRSAKeyPair()); // We get here a random result anyway and is it more likely to make errors by mocking it than by using the real method @@ -446,15 +380,16 @@ void _testTokenNotifier() { firebaseUtils: mockFirebaseUtils, ), ); - final notifier = container.read(testProvider.notifier); + final initState = await notifier.initState; + expect(initState.tokens, before); Logger.info('before rolloutPushToken'); expect(await notifier.rolloutPushToken(before.first), true); Logger.info('after rolloutPushToken'); final state = container.read(testProvider); expect(state, isNotNull); expect(state.tokens, after); - verify(mockRepo.saveOrReplaceTokens([after.first])).called(greaterThan(0)); + verify(mockRepo.saveOrReplaceToken(after.first)).called(greaterThan(0)); verify(mockRsaUtils.serializeRSAPublicKeyPKCS8(any)).called(greaterThan(0)); verify(mockFirebaseUtils.getFBToken()).called(greaterThan(0)); verify(mockRsaUtils.deserializeRSAPublicKeyPKCS1('publicKey')).called(greaterThan(0)); @@ -466,11 +401,14 @@ void _testTokenNotifier() { }); test('loadFromRepo', () async { final mockRepo = MockTokenRepository(); + final mockFirebaseUtils = MockFirebaseUtils(); final before = [ HOTPToken(label: 'label', issuer: 'issuer', id: 'id', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret'), ]; when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); when(mockRepo.loadTokens()).thenAnswer((_) => Future.value(before)); + when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockFirebaseUtils.getFBToken()).thenAnswer((_) async => 'mockFbToken'); final notifier = TokenNotifier(repository: mockRepo); Logger.info('before loadFromRepo'); final newState = await notifier.loadStateFromRepo(); diff --git a/test/unit_test/state_notifiers/token_notifier_test.mocks.dart b/test/unit_test/state_notifiers/token_notifier_test.mocks.dart index c2d5f5f5c..db9937fb0 100644 --- a/test/unit_test/state_notifiers/token_notifier_test.mocks.dart +++ b/test/unit_test/state_notifiers/token_notifier_test.mocks.dart @@ -12,7 +12,8 @@ import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i8; import 'package:pi_authenticator_legacy/pi_authenticator_legacy.dart' as _i14; import 'package:pointycastle/export.dart' as _i2; -import 'package:privacyidea_authenticator/interfaces/repo/token_repository.dart' as _i4; +import 'package:privacyidea_authenticator/interfaces/repo/token_repository.dart' + as _i4; import 'package:privacyidea_authenticator/model/tokens/push_token.dart' as _i10; import 'package:privacyidea_authenticator/model/tokens/token.dart' as _i6; import 'package:privacyidea_authenticator/utils/firebase_utils.dart' as _i12; @@ -52,7 +53,9 @@ class _FakeRSAPrivateKey_1 extends _i1.SmartFake implements _i2.RSAPrivateKey { ); } -class _FakeAsymmetricKeyPair_2 extends _i1.SmartFake implements _i2.AsymmetricKeyPair { +class _FakeAsymmetricKeyPair_2 extends _i1.SmartFake + implements _i2.AsymmetricKeyPair { _FakeAsymmetricKeyPair_2( Object parent, Invocation parentInvocation, @@ -81,13 +84,13 @@ class MockTokenRepository extends _i1.Mock implements _i4.TokenRepository { } @override - _i5.Future> saveOrReplaceTokens(List<_i6.Token>? tokens) => (super.noSuchMethod( + _i5.Future<_i6.Token?> loadToken(String? id) => (super.noSuchMethod( Invocation.method( - #saveNewState, - [tokens], + #loadToken, + [id], ), - returnValue: _i5.Future>.value(<_i6.Token>[]), - ) as _i5.Future>); + returnValue: _i5.Future<_i6.Token?>.value(), + ) as _i5.Future<_i6.Token?>); @override _i5.Future> loadTokens() => (super.noSuchMethod( @@ -99,13 +102,43 @@ class MockTokenRepository extends _i1.Mock implements _i4.TokenRepository { ) as _i5.Future>); @override - _i5.Future> deleteTokens(List<_i6.Token>? tokens) => (super.noSuchMethod( + _i5.Future saveOrReplaceToken(_i6.Token? token) => (super.noSuchMethod( + Invocation.method( + #saveOrReplaceToken, + [token], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future> saveOrReplaceTokens( + List? tokens) => + (super.noSuchMethod( + Invocation.method( + #saveOrReplaceTokens, + [tokens], + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + + @override + _i5.Future deleteToken(_i6.Token? token) => (super.noSuchMethod( + Invocation.method( + #deleteToken, + [token], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future> deleteTokens(List? tokens) => + (super.noSuchMethod( Invocation.method( #deleteTokens, [tokens], ), - returnValue: _i5.Future>.value(<_i6.Token>[]), - ) as _i5.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); } /// A class which mocks [RsaUtils]. @@ -117,7 +150,8 @@ class MockRsaUtils extends _i1.Mock implements _i7.RsaUtils { } @override - _i2.RSAPublicKey deserializeRSAPublicKeyPKCS1(String? keyStr) => (super.noSuchMethod( + _i2.RSAPublicKey deserializeRSAPublicKeyPKCS1(String? keyStr) => + (super.noSuchMethod( Invocation.method( #deserializeRSAPublicKeyPKCS1, [keyStr], @@ -132,7 +166,8 @@ class MockRsaUtils extends _i1.Mock implements _i7.RsaUtils { ) as _i2.RSAPublicKey); @override - String serializeRSAPublicKeyPKCS1(_i2.RSAPublicKey? publicKey) => (super.noSuchMethod( + String serializeRSAPublicKeyPKCS1(_i2.RSAPublicKey? publicKey) => + (super.noSuchMethod( Invocation.method( #serializeRSAPublicKeyPKCS1, [publicKey], @@ -147,7 +182,8 @@ class MockRsaUtils extends _i1.Mock implements _i7.RsaUtils { ) as String); @override - _i2.RSAPublicKey deserializeRSAPublicKeyPKCS8(String? keyStr) => (super.noSuchMethod( + _i2.RSAPublicKey deserializeRSAPublicKeyPKCS8(String? keyStr) => + (super.noSuchMethod( Invocation.method( #deserializeRSAPublicKeyPKCS8, [keyStr], @@ -162,7 +198,8 @@ class MockRsaUtils extends _i1.Mock implements _i7.RsaUtils { ) as _i2.RSAPublicKey); @override - String serializeRSAPublicKeyPKCS8(_i2.RSAPublicKey? key) => (super.noSuchMethod( + String serializeRSAPublicKeyPKCS8(_i2.RSAPublicKey? key) => + (super.noSuchMethod( Invocation.method( #serializeRSAPublicKeyPKCS8, [key], @@ -177,7 +214,8 @@ class MockRsaUtils extends _i1.Mock implements _i7.RsaUtils { ) as String); @override - String serializeRSAPrivateKeyPKCS1(_i2.RSAPrivateKey? key) => (super.noSuchMethod( + String serializeRSAPrivateKeyPKCS1(_i2.RSAPrivateKey? key) => + (super.noSuchMethod( Invocation.method( #serializeRSAPrivateKeyPKCS1, [key], @@ -192,7 +230,8 @@ class MockRsaUtils extends _i1.Mock implements _i7.RsaUtils { ) as String); @override - _i2.RSAPrivateKey deserializeRSAPrivateKeyPKCS1(String? keyStr) => (super.noSuchMethod( + _i2.RSAPrivateKey deserializeRSAPrivateKeyPKCS1(String? keyStr) => + (super.noSuchMethod( Invocation.method( #deserializeRSAPrivateKeyPKCS1, [keyStr], @@ -241,19 +280,24 @@ class MockRsaUtils extends _i1.Mock implements _i7.RsaUtils { ) as _i5.Future); @override - _i5.Future<_i2.AsymmetricKeyPair<_i2.RSAPublicKey, _i2.RSAPrivateKey>> generateRSAKeyPair() => (super.noSuchMethod( - Invocation.method( - #generateRSAKeyPair, - [], - ), - returnValue: _i5.Future<_i2.AsymmetricKeyPair<_i2.RSAPublicKey, _i2.RSAPrivateKey>>.value(_FakeAsymmetricKeyPair_2<_i2.RSAPublicKey, _i2.RSAPrivateKey>( - this, - Invocation.method( - #generateRSAKeyPair, - [], - ), - )), - ) as _i5.Future<_i2.AsymmetricKeyPair<_i2.RSAPublicKey, _i2.RSAPrivateKey>>); + _i5.Future<_i2.AsymmetricKeyPair<_i2.RSAPublicKey, _i2.RSAPrivateKey>> + generateRSAKeyPair() => (super.noSuchMethod( + Invocation.method( + #generateRSAKeyPair, + [], + ), + returnValue: _i5.Future< + _i2.AsymmetricKeyPair<_i2.RSAPublicKey, + _i2.RSAPrivateKey>>.value( + _FakeAsymmetricKeyPair_2<_i2.RSAPublicKey, _i2.RSAPrivateKey>( + this, + Invocation.method( + #generateRSAKeyPair, + [], + ), + )), + ) as _i5.Future< + _i2.AsymmetricKeyPair<_i2.RSAPublicKey, _i2.RSAPrivateKey>>); @override String createBase32Signature( @@ -300,7 +344,8 @@ class MockRsaUtils extends _i1.Mock implements _i7.RsaUtils { /// A class which mocks [PrivacyIdeaIOClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockPrivacyIdeaIOClient extends _i1.Mock implements _i11.PrivacyIdeaIOClient { +class MockPrivacyIdeaIOClient extends _i1.Mock + implements _i11.PrivacyIdeaIOClient { MockPrivacyIdeaIOClient() { _i1.throwOnMissingStub(this); } @@ -421,6 +466,53 @@ class MockFirebaseUtils extends _i1.Mock implements _i12.FirebaseUtils { ), returnValue: _i5.Future.value(), ) as _i5.Future); + + @override + _i5.Future deleteFirebaseToken() => (super.noSuchMethod( + Invocation.method( + #deleteFirebaseToken, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future setCurrentFirebaseToken(String? str) => (super.noSuchMethod( + Invocation.method( + #setCurrentFirebaseToken, + [str], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future getCurrentFirebaseToken() => (super.noSuchMethod( + Invocation.method( + #getCurrentFirebaseToken, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setNewFirebaseToken(String? str) => (super.noSuchMethod( + Invocation.method( + #setNewFirebaseToken, + [str], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future getNewFirebaseToken() => (super.noSuchMethod( + Invocation.method( + #getNewFirebaseToken, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [LegacyUtils]. diff --git a/test/unit_test/utils/crypto_utils_test.dart b/test/unit_test/utils/crypto_utils_test.dart index 3dc2b7472..925f8722c 100644 --- a/test/unit_test/utils/crypto_utils_test.dart +++ b/test/unit_test/utils/crypto_utils_test.dart @@ -22,6 +22,7 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; import 'package:privacyidea_authenticator/utils/crypto_utils.dart'; void main() { @@ -398,28 +399,28 @@ void _testPbkdf2() { void _testDecodeSecretToUint8() { group('decodeSecretToUint8', () { test('Test non hex secret', () { - expect(() => decodeSecretToUint8('oo', Encodings.hex), throwsFormatException); - expect(() => decodeSecretToUint8('1Aö', Encodings.hex), throwsFormatException); + expect(() => Encodings.hex.decode('oo'), throwsFormatException); + expect(() => Encodings.hex.decode('1Aö'), throwsFormatException); }); test('Test hex secret', () { - expect(decodeSecretToUint8('ABCD', Encodings.hex), Uint8List.fromList([171, 205])); - expect(decodeSecretToUint8('0FF8', Encodings.hex), Uint8List.fromList([15, 248])); + expect(Encodings.hex.decode('ABCD'), Uint8List.fromList([171, 205])); + expect(Encodings.hex.decode('0FF8'), Uint8List.fromList([15, 248])); }); test('Test non base32 secret', () { - expect(() => decodeSecretToUint8('p', Encodings.base32), throwsFormatException); - expect(() => decodeSecretToUint8('AAAAAAöA', Encodings.base32), throwsFormatException); + expect(() => Encodings.base32.decode('p'), throwsFormatException); + expect(() => Encodings.base32.decode('AAAAAAöA'), throwsFormatException); }); test('Test base32 secret', () { - expect(decodeSecretToUint8('OBZGS5TBMN4Q====', Encodings.base32), Uint8List.fromList([112, 114, 105, 118, 97, 99, 121])); - expect(decodeSecretToUint8('JFCEKQI=', Encodings.base32), Uint8List.fromList([73, 68, 69, 65])); + expect(Encodings.base32.decode('OBZGS5TBMN4Q===='), Uint8List.fromList([112, 114, 105, 118, 97, 99, 121])); + expect(Encodings.base32.decode('JFCEKQI='), Uint8List.fromList([73, 68, 69, 65])); }); test('Test utf-8 secret', () { - expect(decodeSecretToUint8('ABCD', Encodings.none), Uint8List.fromList([65, 66, 67, 68])); - expect(decodeSecretToUint8('DEG3', Encodings.none), Uint8List.fromList([68, 69, 71, 51])); + expect(Encodings.none.decode('ABCD'), Uint8List.fromList([65, 66, 67, 68])); + expect(Encodings.none.decode('DEG3'), Uint8List.fromList([68, 69, 71, 51])); }); }); } @@ -427,18 +428,18 @@ void _testDecodeSecretToUint8() { void _testEncodeSecretAs() { group('encodeSecretAs', () { test('Test hex secret', () { - expect(encodeSecretAs(Uint8List.fromList([171, 205]), Encodings.hex), 'abcd'); - expect(encodeSecretAs(Uint8List.fromList([15, 248]), Encodings.hex), '0ff8'); + expect(Encodings.hex.encode(Uint8List.fromList([171, 205])), 'abcd'); + expect(Encodings.hex.encode(Uint8List.fromList([15, 248])), '0ff8'); }); test('Test base32 secret', () { - expect(encodeSecretAs(Uint8List.fromList([112, 114, 105, 118, 97, 99, 121]), Encodings.base32), 'OBZGS5TBMN4Q===='); - expect(encodeSecretAs(Uint8List.fromList([73, 68, 69, 65]), Encodings.base32), 'JFCEKQI='); + expect(Encodings.base32.encode(Uint8List.fromList([112, 114, 105, 118, 97, 99, 121])), 'OBZGS5TBMN4Q===='); + expect(Encodings.base32.encode(Uint8List.fromList([73, 68, 69, 65])), 'JFCEKQI='); }); test('Test utf-8 secret', () { - expect(encodeSecretAs(Uint8List.fromList([65, 66, 67, 68]), Encodings.none), 'ABCD'); - expect(encodeSecretAs(Uint8List.fromList([68, 69, 71, 51]), Encodings.none), 'DEG3'); + expect(Encodings.none.encode(Uint8List.fromList([65, 66, 67, 68])), 'ABCD'); + expect(Encodings.none.encode(Uint8List.fromList([68, 69, 71, 51])), 'DEG3'); }); }); } @@ -446,13 +447,13 @@ void _testEncodeSecretAs() { void _testIsValidEncoding() { group('isValidEncoding', () { group('valid encodings', () { - test('valid hex', () => expect(isValidEncoding('abcd', Encodings.hex), true)); - test('valid base32', () => expect(isValidEncoding('OBZGS5TBMN4Q====', Encodings.base32), true)); + test('valid hex', () => expect(Encodings.hex.isValidEncoding('abcd'), true)); + test('valid base32', () => expect(Encodings.base32.isValidEncoding('OBZGS5TBMN4Q===='), true)); }); group('invalid encodings', () { - test('invalid hex', () => expect(isValidEncoding('RXYZ', Encodings.hex), false)); - test('invalid base32', () => expect(isValidEncoding('????', Encodings.base32), false)); + test('invalid hex', () => expect(Encodings.hex.isValidEncoding('RXYZ'), false)); + test('invalid base32', () => expect(Encodings.base32.isValidEncoding('????'), false)); }); }); } diff --git a/test/unit_test/utils/custom_int_buffer_test.dart b/test/unit_test/utils/custom_int_buffer_test.dart index 8c7a204e1..954b8921e 100644 --- a/test/unit_test/utils/custom_int_buffer_test.dart +++ b/test/unit_test/utils/custom_int_buffer_test.dart @@ -28,36 +28,33 @@ void main() { void verifyCustomStringBufferWorks() { group('test custom string buffer', () { test('put elements in', () { - CustomIntBuffer buffer = CustomIntBuffer(); - buffer.list = []; + const buffer0_30 = CustomIntBuffer(maxSize: 30); - expect(buffer.maxSize, 30); - expect(buffer.length, 0); + expect(buffer0_30.maxSize, 30); + expect(buffer0_30.length, 0); - buffer.put(1); - buffer.put(2); - buffer.put(3); + final buffer3_30 = buffer0_30.putList([1, 2, 3]); - expect(buffer.length, 3); + expect(buffer3_30.length, 3); - expect(buffer.contains(1), true); - expect(buffer.contains(2), true); - expect(buffer.contains(3), true); - expect(buffer.contains(4), false); + expect(buffer3_30.contains(1), true); + expect(buffer3_30.contains(2), true); + expect(buffer3_30.contains(3), true); + expect(buffer3_30.contains(4), false); - for (int i = 3; i < buffer.maxSize; i++) { - buffer.put(-1); - } + final values = List.generate(buffer3_30.maxSize - buffer3_30.length, (index) => 0 - index); // 27 elements - buffer.put(4); + final buffer30_30 = buffer3_30.putList(values); // full buffer 30/30 - expect(buffer.length, 30); - expect(buffer.maxSize, 30); + final overflowBuffer = buffer30_30.put(4); // 4 is added, 1 is removed - expect(buffer.contains(1), false); - expect(buffer.contains(2), true); - expect(buffer.contains(3), true); - expect(buffer.contains(4), true); + expect(overflowBuffer.length, 30); + expect(overflowBuffer.maxSize, 30); + + expect(overflowBuffer.contains(1), false); + expect(overflowBuffer.contains(2), true); + expect(overflowBuffer.contains(3), true); + expect(overflowBuffer.contains(4), true); }); }); } diff --git a/test/unit_test/utils/customization/application_customization_test.dart b/test/unit_test/utils/customization/application_customization_test.dart new file mode 100644 index 000000000..d3c99c563 --- /dev/null +++ b/test/unit_test/utils/customization/application_customization_test.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/app_feature.dart'; +import 'package:privacyidea_authenticator/utils/customization/application_customization.dart'; + +void main() { + _testAppCustomizer(); +} + +void _testAppCustomizer() { + group('App Customizer Test', () { + // Arrange + final customization = ApplicationCustomization( + appName: 'test', + websiteLink: 'https://test', + appIconUint8List: defaultIconUint8List, + appImageUint8List: defaultImageUint8List, + lightTheme: ApplicationCustomization.defaultCustomization.lightTheme, + darkTheme: ApplicationCustomization.defaultCustomization.darkTheme, + disabledFeatures: {AppFeature.patchNotes}, + ); + test('constructor', () { + // Assert + expect(customization.appName, equals('test')); + expect(customization.websiteLink, equals('https://test')); + expect(customization.appIconUint8List, equals(defaultIconUint8List)); + expect(customization.appImageUint8List, equals(defaultImageUint8List)); + expect(customization.lightTheme, equals(ApplicationCustomization.defaultCustomization.lightTheme)); + expect(customization.darkTheme, equals(ApplicationCustomization.defaultCustomization.darkTheme)); + expect(customization.disabledFeatures, equals({AppFeature.patchNotes})); + }); + test('copyWith', () { + // Act + final newCustomization = customization.copyWith( + appName: 'test2', + websiteLink: 'https://test2', + appIconUint8List: defaultImageUint8List, + appImageUint8List: defaultIconUint8List, + lightTheme: ApplicationCustomization.defaultCustomization.darkTheme, + darkTheme: ApplicationCustomization.defaultCustomization.lightTheme, + disabledFeatures: {}, + ); + // Assert + expect(newCustomization.appName, equals('test2')); + expect(newCustomization.websiteLink, equals('https://test2')); + expect(newCustomization.appIconUint8List, equals(defaultImageUint8List)); + expect(newCustomization.appImageUint8List, equals(defaultIconUint8List)); + }); + group('serialization', () { + test('toJson', () { + // Act + final json = customization.toJson(); + // Assert + expect(json['appName'], equals('test')); + expect(json['websiteLink'], equals('https://test')); + expect(json['appIconBASE64'], equals(base64Encode(defaultIconUint8List))); + expect(json['appImageBASE64'], equals(base64Encode(defaultImageUint8List))); + expect(json['lightTheme'], equals(ApplicationCustomization.defaultCustomization.lightTheme.toJson())); + expect(json['darkTheme'], equals(ApplicationCustomization.defaultCustomization.darkTheme.toJson())); + expect(json['disabledFeatures'], equals({AppFeature.patchNotes.name})); + }); + test('fromJson', () { + // Act + final newCustomization = ApplicationCustomization.fromJson({ + 'appName': 'test2', + 'websiteLink': 'https://test2', + 'appIconBASE64': base64Encode(defaultImageUint8List), + 'appImageBASE64': base64Encode(defaultIconUint8List), + 'lightTheme': ApplicationCustomization.defaultCustomization.lightTheme.toJson(), + 'darkTheme': ApplicationCustomization.defaultCustomization.darkTheme.toJson(), + 'disabledFeatures': [], + }); + // Assert + expect(newCustomization.appName, equals('test2')); + expect(newCustomization.websiteLink, equals('https://test2')); + expect(newCustomization.appIconUint8List, equals(defaultImageUint8List)); + expect(newCustomization.appImageUint8List, equals(defaultIconUint8List)); + expect(newCustomization.lightTheme, equals(ApplicationCustomization.defaultCustomization.lightTheme)); + expect(newCustomization.darkTheme, equals(ApplicationCustomization.defaultCustomization.darkTheme)); + expect(newCustomization.disabledFeatures, isA()); + expect(newCustomization.disabledFeatures, isEmpty); + }); + }); + }); +} diff --git a/test/unit_test/utils/customization/theme_customization_test.dart b/test/unit_test/utils/customization/theme_customization_test.dart new file mode 100644 index 000000000..a23dc69b1 --- /dev/null +++ b/test/unit_test/utils/customization/theme_customization_test.dart @@ -0,0 +1,248 @@ +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/utils/customization/theme_customization.dart'; + +void main() { + _testThemeCustomization(); +} + +void _testThemeCustomization() { + group('Theme Customization Test', () { + // Arrange + const customization = ThemeCustomization( + brightness: Brightness.dark, + primaryColor: Color(0xFF000000), + onPrimary: Color(0xFF000001), + subtitleColor: Color(0xFF000002), + backgroundColor: Color(0xFF000003), + foregroundColor: Color(0xFF000004), + shadowColor: Color(0xFF000005), + deleteColor: Color(0xFF000006), + renameColor: Color(0xFF000007), + lockColor: Color(0xFF000008), + tileIconColor: Color(0xFF000009), + navigationBarColor: Color(0xFF00000A), + actionButtonsForegroundColor: Color(0xFF00000B), + tilePrimaryColor: Color(0xFF00000C), + tileSubtitleColor: Color(0xFF00000D), + navigationBarIconColor: Color(0xFF00000E), + qrButtonBackgroundColor: Color(0xFF00000F), + qrButtonIconColor: Color(0xFF000010), + ); + test('constructor', () { + // Assert + expect(customization.brightness, equals(Brightness.dark)); + expect(customization.primaryColor, equals(const Color(0xFF000000))); + expect(customization.onPrimary, equals(const Color(0xFF000001))); + expect(customization.subtitleColor, equals(const Color(0xFF000002))); + expect(customization.backgroundColor, equals(const Color(0xFF000003))); + expect(customization.foregroundColor, equals(const Color(0xFF000004))); + expect(customization.shadowColor, equals(const Color(0xFF000005))); + expect(customization.deleteColor, equals(const Color(0xFF000006))); + expect(customization.renameColor, equals(const Color(0xFF000007))); + expect(customization.lockColor, equals(const Color(0xFF000008))); + expect(customization.tileIconColor, equals(const Color(0xFF000009))); + expect(customization.navigationBarColor, equals(const Color(0xFF00000A))); + expect(customization.actionButtonsForegroundColor, equals(const Color(0xFF00000B))); + expect(customization.tilePrimaryColor, equals(const Color(0xFF00000C))); + expect(customization.tileSubtitleColor, equals(const Color(0xFF00000D))); + expect(customization.navigationBarIconColor, equals(const Color(0xFF00000E))); + expect(customization.qrButtonBackgroundColor, equals(const Color(0xFF00000F))); + expect(customization.qrButtonIconColor, equals(const Color(0xFF000010))); + }); + test('copyWith', () { + // Act + final newCustomization = customization.copyWith( + brightness: Brightness.light, + primaryColor: const Color(0xFFFFFFFF), + onPrimary: const Color(0xFFFFFFFE), + subtitleColor: const Color(0xFFFFFFFD), + backgroundColor: const Color(0xFFFFFFFC), + foregroundColor: const Color(0xFFFFFFFB), + shadowColor: const Color(0xFFFFFFFA), + deleteColor: const Color(0xFFFFFFF9), + renameColor: const Color(0xFFFFFFF8), + lockColor: const Color(0xFFFFFFF7), + tileIconColor: const Color(0xFFFFFFF6), + navigationBarColor: const Color(0xFFFFFFF5), + actionButtonsForegroundColor: () => const Color(0xFFFFFFF4), + tilePrimaryColor: () => const Color(0xFFFFFFF3), + tileSubtitleColor: () => const Color(0xFFFFFFF2), + navigationBarIconColor: () => const Color(0xFFFFFFF1), + qrButtonBackgroundColor: () => const Color(0xFFFFFFF0), + qrButtonIconColor: () => const Color(0xFFFFFFEF), + ); + // Assert + expect(newCustomization.brightness, equals(Brightness.light)); + expect(newCustomization.primaryColor, equals(const Color(0xFFFFFFFF))); + expect(newCustomization.onPrimary, equals(const Color(0xFFFFFFFE))); + expect(newCustomization.subtitleColor, equals(const Color(0xFFFFFFFD))); + expect(newCustomization.backgroundColor, equals(const Color(0xFFFFFFFC))); + expect(newCustomization.foregroundColor, equals(const Color(0xFFFFFFFB))); + expect(newCustomization.shadowColor, equals(const Color(0xFFFFFFFA))); + expect(newCustomization.deleteColor, equals(const Color(0xFFFFFFF9))); + expect(newCustomization.renameColor, equals(const Color(0xFFFFFFF8))); + expect(newCustomization.lockColor, equals(const Color(0xFFFFFFF7))); + expect(newCustomization.tileIconColor, equals(const Color(0xFFFFFFF6))); + expect(newCustomization.navigationBarColor, equals(const Color(0xFFFFFFF5))); + expect(newCustomization.actionButtonsForegroundColor, equals(const Color(0xFFFFFFF4))); + expect(newCustomization.tilePrimaryColor, equals(const Color(0xFFFFFFF3))); + expect(newCustomization.tileSubtitleColor, equals(const Color(0xFFFFFFF2))); + expect(newCustomization.navigationBarIconColor, equals(const Color(0xFFFFFFF1))); + expect(newCustomization.qrButtonBackgroundColor, equals(const Color(0xFFFFFFF0))); + expect(newCustomization.qrButtonIconColor, equals(const Color(0xFFFFFFEF))); + }); + group('default themes', () { + test('defaultLightWith', () { + // Act + const newCustomization = ThemeCustomization.defaultLightWith( + primaryColor: Color(0xFFFFFFFF), + onPrimary: Color(0xFFFFFFFE), + subtitleColor: Color(0xFFFFFFFD), + backgroundColor: Color(0xFFFFFFFC), + foregroundColor: Color(0xFFFFFFFB), + shadowColor: Color(0xFFFFFFFA), + deleteColor: Color(0xFFFFFFF9), + renameColor: Color(0xFFFFFFF8), + lockColor: Color(0xFFFFFFF7), + tileIconColor: Color(0xFFFFFFF6), + navigationBarColor: Color(0xFFFFFFF5), + actionButtonsForegroundColor: Color(0xFFFFFFF4), + tilePrimaryColor: Color(0xFFFFFFF3), + tileSubtitleColor: Color(0xFFFFFFF2), + navigationBarIconColor: Color(0xFFFFFFF1), + qrButtonBackgroundColor: Color(0xFFFFFFF0), + qrButtonIconColor: Color(0xFFFFFFEF), + ); + // Assert + expect(newCustomization.brightness, equals(Brightness.light)); + expect(newCustomization.primaryColor, equals(const Color(0xFFFFFFFF))); + expect(newCustomization.onPrimary, equals(const Color(0xFFFFFFFE))); + expect(newCustomization.subtitleColor, equals(const Color(0xFFFFFFFD))); + expect(newCustomization.backgroundColor, equals(const Color(0xFFFFFFFC))); + expect(newCustomization.foregroundColor, equals(const Color(0xFFFFFFFB))); + expect(newCustomization.shadowColor, equals(const Color(0xFFFFFFFA))); + expect(newCustomization.deleteColor, equals(const Color(0xFFFFFFF9))); + expect(newCustomization.renameColor, equals(const Color(0xFFFFFFF8))); + expect(newCustomization.lockColor, equals(const Color(0xFFFFFFF7))); + expect(newCustomization.tileIconColor, equals(const Color(0xFFFFFFF6))); + expect(newCustomization.navigationBarColor, equals(const Color(0xFFFFFFF5))); + expect(newCustomization.actionButtonsForegroundColor, equals(const Color(0xFFFFFFF4))); + expect(newCustomization.tilePrimaryColor, equals(const Color(0xFFFFFFF3))); + expect(newCustomization.tileSubtitleColor, equals(const Color(0xFFFFFFF2))); + expect(newCustomization.navigationBarIconColor, equals(const Color(0xFFFFFFF1))); + expect(newCustomization.qrButtonBackgroundColor, equals(const Color(0xFFFFFFF0))); + expect(newCustomization.qrButtonIconColor, equals(const Color(0xFFFFFFEF))); + }); + test('defaultDarkWith', () { + // Act + const newCustomization = ThemeCustomization.defaultDarkWith( + primaryColor: Color(0xFFFFFFFF), + onPrimary: Color(0xFFFFFFFE), + subtitleColor: Color(0xFFFFFFFD), + backgroundColor: Color(0xFFFFFFFC), + foregroundColor: Color(0xFFFFFFFB), + shadowColor: Color(0xFFFFFFFA), + deleteColor: Color(0xFFFFFFF9), + renameColor: Color(0xFFFFFFF8), + lockColor: Color(0xFFFFFFF7), + tileIconColor: Color(0xFFFFFFF6), + navigationBarColor: Color(0xFFFFFFF5), + actionButtonsForegroundColor: Color(0xFFFFFFF4), + tilePrimaryColor: Color(0xFFFFFFF3), + tileSubtitleColor: Color(0xFFFFFFF2), + navigationBarIconColor: Color(0xFFFFFFF1), + qrButtonBackgroundColor: Color(0xFFFFFFF0), + qrButtonIconColor: Color(0xFFFFFFEF), + ); + // Assert + expect(newCustomization.brightness, equals(Brightness.dark)); + expect(newCustomization.primaryColor, equals(const Color(0xFFFFFFFF))); + expect(newCustomization.onPrimary, equals(const Color(0xFFFFFFFE))); + expect(newCustomization.subtitleColor, equals(const Color(0xFFFFFFFD))); + expect(newCustomization.backgroundColor, equals(const Color(0xFFFFFFFC))); + expect(newCustomization.foregroundColor, equals(const Color(0xFFFFFFFB))); + expect(newCustomization.shadowColor, equals(const Color(0xFFFFFFFA))); + expect(newCustomization.deleteColor, equals(const Color(0xFFFFFFF9))); + expect(newCustomization.renameColor, equals(const Color(0xFFFFFFF8))); + expect(newCustomization.lockColor, equals(const Color(0xFFFFFFF7))); + expect(newCustomization.tileIconColor, equals(const Color(0xFFFFFFF6))); + expect(newCustomization.navigationBarColor, equals(const Color(0xFFFFFFF5))); + expect(newCustomization.actionButtonsForegroundColor, equals(const Color(0xFFFFFFF4))); + expect(newCustomization.tilePrimaryColor, equals(const Color(0xFFFFFFF3))); + expect(newCustomization.tileSubtitleColor, equals(const Color(0xFFFFFFF2))); + expect(newCustomization.navigationBarIconColor, equals(const Color(0xFFFFFFF1))); + expect(newCustomization.qrButtonBackgroundColor, equals(const Color(0xFFFFFFF0))); + expect(newCustomization.qrButtonIconColor, equals(const Color(0xFFFFFFEF))); + }); + }); + group('serialization', () { + test('fromJson', () { + // Act + final newCustomization = ThemeCustomization.fromJson({ + 'brightness': 'light', + 'primaryColor': 0xFFFFFFFF, + 'onPrimary': 0xFFFFFFFE, + 'subtitleColor': 0xFFFFFFFD, + 'backgroundColor': 0xFFFFFFFC, + 'foregroundColor': 0xFFFFFFFB, + 'shadowColor': 0xFFFFFFFA, + 'deleteColor': 0xFFFFFFF9, + 'renameColor': 0xFFFFFFF8, + 'lockColor': 0xFFFFFFF7, + 'tileIconColor': 0xFFFFFFF6, + 'navigationBarColor': 0xFFFFFFF5, + '_actionButtonsForegroundColor': 0xFFFFFFF4, + '_tilePrimaryColor': 0xFFFFFFF3, + '_tileSubtitleColor': 0xFFFFFFF2, + '_navigationBarIconColor': 0xFFFFFFF1, + '_qrButtonBackgroundColor': 0xFFFFFFF0, + '_qrButtonIconColor': 0xFFFFFFEF, + }); + // Assert + expect(newCustomization.brightness, equals(Brightness.light)); + expect(newCustomization.primaryColor, equals(const Color(0xFFFFFFFF))); + expect(newCustomization.onPrimary, equals(const Color(0xFFFFFFFE))); + expect(newCustomization.subtitleColor, equals(const Color(0xFFFFFFFD))); + expect(newCustomization.backgroundColor, equals(const Color(0xFFFFFFFC))); + expect(newCustomization.foregroundColor, equals(const Color(0xFFFFFFFB))); + expect(newCustomization.shadowColor, equals(const Color(0xFFFFFFFA))); + expect(newCustomization.deleteColor, equals(const Color(0xFFFFFFF9))); + expect(newCustomization.renameColor, equals(const Color(0xFFFFFFF8))); + expect(newCustomization.lockColor, equals(const Color(0xFFFFFFF7))); + expect(newCustomization.tileIconColor, equals(const Color(0xFFFFFFF6))); + expect(newCustomization.navigationBarColor, equals(const Color(0xFFFFFFF5))); + expect(newCustomization.actionButtonsForegroundColor, equals(const Color(0xFFFFFFF4))); + expect(newCustomization.tilePrimaryColor, equals(const Color(0xFFFFFFF3))); + expect(newCustomization.tileSubtitleColor, equals(const Color(0xFFFFFFF2))); + expect(newCustomization.navigationBarIconColor, equals(const Color(0xFFFFFFF1))); + expect(newCustomization.qrButtonBackgroundColor, equals(const Color(0xFFFFFFF0))); + expect(newCustomization.qrButtonIconColor, equals(const Color(0xFFFFFFEF))); + }); + test('toJson', () { + // Act + final json = customization.toJson(); + // Assert + expect(json['brightness'], equals('dark')); + expect(json['primaryColor'], equals(0xFF000000)); + expect(json['onPrimary'], equals(0xFF000001)); + expect(json['subtitleColor'], equals(0xFF000002)); + expect(json['backgroundColor'], equals(0xFF000003)); + expect(json['foregroundColor'], equals(0xFF000004)); + expect(json['shadowColor'], equals(0xFF000005)); + expect(json['deleteColor'], equals(0xFF000006)); + expect(json['renameColor'], equals(0xFF000007)); + expect(json['lockColor'], equals(0xFF000008)); + expect(json['tileIconColor'], equals(0xFF000009)); + expect(json['navigationBarColor'], equals(0xFF00000A)); + expect(json['_actionButtonsForegroundColor'], equals(0xFF00000B)); + expect(json['_tilePrimaryColor'], equals(0xFF00000C)); + expect(json['_tileSubtitleColor'], equals(0xFF00000D)); + expect(json['_navigationBarIconColor'], equals(0xFF00000E)); + expect(json['_qrButtonBackgroundColor'], equals(0xFF00000F)); + expect(json['_qrButtonIconColor'], equals(0xFF000010)); + }); + }); + }); +} diff --git a/test/unit_test/utils/push_request_queue_test.dart b/test/unit_test/utils/push_request_queue_test.dart deleted file mode 100644 index 50c1090c5..000000000 --- a/test/unit_test/utils/push_request_queue_test.dart +++ /dev/null @@ -1,207 +0,0 @@ -/* - privacyIDEA Authenticator - - Authors: Timo Sturm - Frank Merkel - Copyright (c) 2017-2023 NetKnights GmbH - - Licensed under the Apache License, Version 2.0 (the 'License'); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an 'AS IS' BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -import 'dart:collection'; -import 'dart:convert'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:privacyidea_authenticator/model/push_request.dart'; -import 'package:privacyidea_authenticator/model/push_request_queue.dart'; - -import 'package:uuid/uuid.dart'; - -void main() { - verifyCustomListBehavesLikeQueue(); -} - -void verifyCustomListBehavesLikeQueue() { - group('Test custom queue', () { - Uri uri = Uri.parse('http://www.example.com'); - - test('isEmpty', () { - PushRequestQueue fifo = PushRequestQueue(); - var pushRequest = PushRequest( - title: 'title', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - - expect(fifo.isNotEmpty, false); - expect(fifo.isEmpty, true); - - fifo.add(pushRequest); - - expect(fifo.isNotEmpty, true); - expect(fifo.isEmpty, false); - - var peek = fifo.peek(); - expect(peek, pushRequest); - - expect(fifo.isNotEmpty, true); - expect(fifo.isEmpty, false); - - var tryPop = fifo.tryPop(); - expect(tryPop, pushRequest); - - expect(fifo.isNotEmpty, false); - expect(fifo.isEmpty, true); - }); - - test('behaves like queue', () { - Queue queue = Queue(); - PushRequestQueue fifo = PushRequestQueue(); - - var one = PushRequest( - title: 'one', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - var two = PushRequest( - title: 'two', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - var three = PushRequest( - title: 'three', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - var four = PushRequest( - title: 'four', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - var five = PushRequest( - title: 'five', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - - queue.addLast(one); - fifo.add(one); - queue.addLast(two); - fifo.add(two); - queue.addLast(three); - fifo.add(three); - - expect(fifo.peek(), queue.first); - fifo.tryPop(); - queue.removeFirst(); - expect(fifo.peek(), queue.first); - - queue.addLast(four); - fifo.add(four); - queue.addLast(five); - fifo.add(five); - expect(fifo.tryPop(), queue.removeFirst()); - expect(fifo.tryPop(), queue.removeFirst()); - expect(fifo.tryPop(), queue.removeFirst()); - expect(fifo.tryPop(), queue.removeFirst()); - - expect(fifo.isEmpty, true); - expect(queue.isEmpty, true); - }); - - test('serialization', () { - PushRequestQueue fifo = PushRequestQueue(); - - var one = PushRequest( - title: 'one', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - var two = PushRequest( - title: 'two', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - var three = PushRequest( - title: 'three', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - var four = PushRequest( - title: 'four', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - var five = PushRequest( - title: 'five', - question: 'question', - uri: uri, - nonce: 'nonce', - sslVerify: false, - id: const Uuid().v4().hashCode, - expirationDate: DateTime.utc(3333), - ); - - fifo.add(one); - fifo.add(two); - fifo.add(three); - fifo.add(four); - fifo.add(five); - - var encoded = jsonEncode(fifo); - var decoded = PushRequestQueue.fromJson(jsonDecode(encoded) as Map); - - expect(decoded, fifo); - }); - }); -} diff --git a/test/unit_test/utils/utils_test.dart b/test/unit_test/utils/utils_test.dart deleted file mode 100644 index e2e28fd3f..000000000 --- a/test/unit_test/utils/utils_test.dart +++ /dev/null @@ -1,72 +0,0 @@ -/* - privacyIDEA Authenticator - - Authors: Timo Sturm - Frank Merkel - Copyright (c) 2017-2023 NetKnights GmbH - - Licensed under the Apache License, Version 2.0 (the 'License'); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an 'AS IS' BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -import 'package:flutter_test/flutter_test.dart'; -import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; -import 'package:privacyidea_authenticator/utils/utils.dart'; - -void main() { - _testInsertCharAt(); - _testSplitPeriodically(); - _testMapStringToAlgorithm(); - _testEnumAsString(); -} - -void _testInsertCharAt() { - const String str = 'ABCD'; - - group('insertCharAt', () { - test('Insert at start', () => expect('XABCD', insertCharAt(str, 'X', 0))); - - test('Insert at end', () => expect('ABCDX', insertCharAt(str, 'X', str.length))); - - test('Insert at end', () => expect('ABXCD', insertCharAt(str, 'X', 2))); - }); -} - -void _testSplitPeriodically() { - const String str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - - group('splitPeriodically', () { - test('Split every -1', () => expect(splitPeriodically(str, -1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')); - test('Split every 0', () => expect(splitPeriodically(str, 0), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')); - test('Split every 1', () => expect(splitPeriodically(str, 1), 'A B C D E F G H I J K L M N O P Q R S T U V W X Y Z')); - test('Split every 2', () => expect('AB CD EF GH IJ KL MN OP QR ST UV WX YZ', splitPeriodically(str, 2))); - test('Split every 3', () => expect('ABC DEF GHI JKL MNO PQR STU VWX YZ', splitPeriodically(str, 3))); - test('Split every 7', () => expect('ABCDEFG HIJKLMN OPQRSTU VWXYZ', splitPeriodically(str, 7))); - test('Split every 12', () => expect('ABCDEFGHIJKL MNOPQRSTUVWX YZ', splitPeriodically(str, 12))); - }); -} - -void _testMapStringToAlgorithm() { - group('mapStringToAlgorithm', () { - test('Test SHA1', () => expect(mapStringToAlgorithm('SHA1'), Algorithms.SHA1)); - test('Test SHA256', () => expect(mapStringToAlgorithm('SHA256'), Algorithms.SHA256)); - test('Test SHA512', () => expect(mapStringToAlgorithm('SHA512'), Algorithms.SHA512)); - test('Test invalid', () => expect(() => mapStringToAlgorithm('invalid'), throwsArgumentError)); - }); -} - -void _testEnumAsString() { - group('enumAsString', () { - test('Test SHA1', () => expect(enumAsString(Algorithms.SHA1), 'SHA1')); - test('Test SHA256', () => expect(enumAsString(Algorithms.SHA256), 'SHA256')); - test('Test SHA512', () => expect(enumAsString(Algorithms.SHA512), 'SHA512')); - }); -}