Compare commits

..

1203 Commits

Author SHA1 Message Date
J-Jamet
3b793a72b8 fix: Autofill username detection #2276 2025-11-17 17:28:08 +01:00
J-Jamet
f19afbdb2e Manual change of app language #1884 #1990 2025-11-17 12:08:26 +01:00
J-Jamet
622e9cefdd Merge tag '4.2.4' into develop
4.2.4
2025-11-14 11:53:15 +01:00
J-Jamet
3ba56677ba Merge branch 'release/4.2.4' 2025-11-14 11:52:56 +01:00
J-Jamet
39b4b4df70 fix: Tags 2025-11-14 11:42:18 +01:00
J-Jamet
4180ca92b0 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-11-14 11:38:55 +01:00
Artyom Rybakov
bc9d00a1e1 Translated using Weblate (Russian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-11-14 06:51:17 +01:00
J-Jamet
5bdc72aa67 fix: Remembering database location #2262 2025-11-13 10:59:16 +01:00
J-Jamet
2be32e6884 fix: Upgrade to 4.2.4 2025-11-13 10:31:11 +01:00
Tadas L
612db4a6fc Translated using Weblate (Lithuanian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/lt/
2025-11-11 22:13:02 +01:00
Tadas L
e74176f3bc Translated using Weblate (Lithuanian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2025-11-10 23:18:05 +01:00
Cirno
af1fba42a0 Translated using Weblate (Vietnamese)
Currently translated at 92.8% (653 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2025-11-10 19:47:33 +01:00
Mateus Moretto
bebf30aec1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2025-11-08 03:51:14 +01:00
Arthur Zamarin
321bb46df5 Translated using Weblate (Hebrew)
Currently translated at 99.2% (698 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-11-06 10:51:44 +01:00
Joonas Reinholm
429f6db93f Translated using Weblate (Finnish)
Currently translated at 64.4% (453 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2025-11-06 10:51:25 +01:00
gryhgyjh
fc5a13160a Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (702 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2025-11-04 17:55:57 +01:00
Random
c6eee8d449 Translated using Weblate (Italian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/it/
2025-11-04 11:51:39 +00:00
Alonso González Chaves
7d227f372f Translated using Weblate (Spanish)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-11-04 11:51:33 +00:00
VfBFan
3ac56b974f Translated using Weblate (German)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-11-04 11:51:12 +00:00
solokot
2e85ea401b Translated using Weblate (Russian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-11-02 08:01:45 +01:00
solokot
fd080fb952 Translated using Weblate (Russian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-11-01 21:44:30 +01:00
Ice Fairy ❄️
cc8e07366a Translated using Weblate (Vietnamese)
Currently translated at 92.8% (653 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2025-10-30 09:03:14 +00:00
109247019824
c21bcbdbc2 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-30 09:03:12 +00:00
Daniel Bencze
e2ee17dae7 Translated using Weblate (Romanian)
Currently translated at 95.4% (671 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2025-10-30 09:03:10 +00:00
Masowick
e68830fa25 Translated using Weblate (German)
Currently translated at 99.5% (700 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-30 09:03:04 +00:00
J-Jamet
9ddd66ce85 Merge tag '4.2.3' into develop
4.2.3
2025-10-29 18:37:28 +01:00
J-Jamet
e3b69789bf Merge branch 'release/4.2.3' 2025-10-29 18:37:19 +01:00
J-Jamet
54f2ed9fab fix: Small fixes 2025-10-29 18:27:54 +01:00
J-Jamet
2fea019b95 fix: Save search info if URL present #2255 2025-10-29 18:12:44 +01:00
J-Jamet
9ac7ef2d22 fix: Credential allow action during orientation change #2253 2025-10-29 17:12:27 +01:00
J-Jamet
6d452fa49c fix: Credential allow action #2253 2025-10-29 16:44:48 +01:00
J-Jamet
d99edb6b4d fix: database dialog subtitle #2254 2025-10-29 16:18:51 +01:00
J-Jamet
cb679f0d59 fix: Multiple Passkey selection #2253 2025-10-29 15:15:44 +01:00
VfBFan
2e237fba2d Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-10-28 20:03:10 +01:00
John Doe
e68863a154 Translated using Weblate (German)
Currently translated at 97.5% (686 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-28 20:03:08 +01:00
J-Jamet
5dd9f75095 Merge tag '4.2.2' into develop
4.2.2
2025-10-28 17:44:51 +01:00
J-Jamet
403021d38b Merge branch 'release/4.2.2' 2025-10-28 17:44:43 +01:00
J-Jamet
fea7b30d6f Merge branch 'develop' into release/4.2.2 2025-10-28 17:19:08 +01:00
J-Jamet
ab5c859db4 fix: Tags in translations 2025-10-28 17:18:41 +01:00
J-Jamet
3fcbc65de0 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-28 17:16:39 +01:00
J-Jamet
3f1ee6bbea fix: Upgrade CHANGELOG 2025-10-28 17:14:44 +01:00
J-Jamet
37ce2ab781 fix: Snackbar error 2025-10-28 15:59:20 +01:00
J-Jamet
ffaf4a761a fix: Service stop 2025-10-28 15:48:07 +01:00
J-Jamet
56b7cc9118 fix: Progress message 2025-10-28 14:49:10 +01:00
J-Jamet
987f3f9047 fix: Revert fragment view 2025-10-28 14:15:19 +01:00
J-Jamet
3039efc67c fix: Cancelable during save 2025-10-28 14:07:32 +01:00
J-Jamet
26daac4637 fix: Dialog progress tasks 2025-10-28 13:28:20 +01:00
J-Jamet
88a93829a9 fix: Lock finish 2025-10-27 21:28:29 +01:00
J-Jamet
7923a63d36 fix: Database dialog stopped 2025-10-27 20:53:30 +01:00
J-Jamet
9a5c782d5d fix: Small fix, info dialog 2025-10-27 20:45:40 +01:00
J-Jamet
c39e4ba693 fix: Play service as privileged app for Passkey Cross Device Authentication #2244 2025-10-27 20:15:02 +01:00
J-Jamet
7db3d0502f fix: Save search info #2243 2025-10-27 19:32:00 +01:00
J-Jamet
d557e8b516 fix: merge algorithm #2223 2025-10-27 18:51:19 +01:00
Priit Jõerüüt
d6ae17657b Translated using Weblate (Estonian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-10-27 18:11:18 +01:00
solokot
3468b0f6f5 Translated using Weblate (Russian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-10-27 18:11:16 +01:00
J-Jamet
79777801e8 fix: merge algorithm 2025-10-27 16:34:38 +01:00
안세훈
a202f66d48 Translated using Weblate (Korean)
Currently translated at 52.6% (370 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2025-10-27 03:02:44 +00:00
Kunzisoft
ba58d5d47c Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/zh_Hant/
2025-10-25 16:03:05 +00:00
Kunzisoft
46685592df Translated using Weblate (Croatian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/hr/
2025-10-25 16:03:03 +00:00
Kunzisoft
ba9e2892ef Translated using Weblate (Indonesian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/id/
2025-10-25 16:03:02 +00:00
Kunzisoft
a1da3b4fbd Translated using Weblate (Italian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/it/
2025-10-25 16:03:01 +00:00
Kunzisoft
8bee0ec220 Translated using Weblate (Macedonian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/mk/
2025-10-25 16:02:59 +00:00
Kunzisoft
aebf6b21de Translated using Weblate (Arabic)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-10-25 16:02:58 +00:00
Kunzisoft
0cf9253ea4 Translated using Weblate (Spanish)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/es/
2025-10-25 16:02:55 +00:00
Kunzisoft
b63ceb37a4 Translated using Weblate (Turkish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/tr/
2025-10-25 16:02:54 +00:00
solokot
c462dae6f5 Translated using Weblate (Russian)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-10-25 16:02:52 +00:00
Kunzisoft
ddf890b861 Translated using Weblate (Russian)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-10-25 16:02:51 +00:00
Besnik Bleta
252eb30b13 Translated using Weblate (Albanian)
Currently translated at 92.0% (647 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2025-10-25 16:02:43 +00:00
J-Jamet
62ab11cc56 Merge tag '4.2.1' into develop
4.2.1
2025-10-24 18:16:25 +02:00
J-Jamet
e19ad3a8cc Merge branch 'release/4.2.1' 2025-10-24 18:16:16 +02:00
J-Jamet
51fd8a77eb fix: translations tags 2025-10-24 18:05:52 +02:00
J-Jamet
5ee0c2eb13 Merge branch 'translations' into develop 2025-10-24 18:03:58 +02:00
Hosted Weblate
6d0ef8265c Merge branch 'origin/develop' into Weblate. 2025-10-24 17:43:35 +02:00
J-Jamet
ea69d5acb2 fix: Add brave version code for Autofill #2235 2025-10-24 17:40:38 +02:00
J-Jamet
1fb9595ec3 fix: Update CHANGELOG 2025-10-24 17:21:54 +02:00
J-Jamet
88e0bd51dc fix: Dialog database action #2234 2025-10-24 17:20:02 +02:00
J-Jamet
67477cc53b fix: unused import 2025-10-24 14:51:19 +02:00
J-Jamet
d2549d61d6 fix: Autofill pending intent bypass #2238 2025-10-24 14:43:42 +02:00
J-Jamet
d6dc75961b fix: lastmodificationTime equals #2223 2025-10-24 12:47:11 +02:00
Random
f40c83812a Translated using Weblate (Italian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/it/
2025-10-23 23:02:48 +00:00
jonnysemon
b29c638d20 Translated using Weblate (Arabic)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-10-23 23:02:46 +00:00
Priit Jõerüüt
5bb03c2eef Translated using Weblate (Estonian)
Currently translated at 94.7% (666 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-10-23 23:02:43 +00:00
J-Jamet
a76b1195e5 fix: Small bug 2025-10-22 15:36:07 +02:00
Random
64da26f42c Translated using Weblate (Italian)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/it/
2025-10-22 12:03:05 +02:00
Random
ef82552a0f Translated using Weblate (Italian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-10-22 12:03:03 +02:00
Masowick
d61b27ccd0 Translated using Weblate (German)
Currently translated at 97.5% (686 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-22 12:02:43 +02:00
J-Jamet
910ba99056 fix: Remove unused comment 2025-10-21 12:42:21 +02:00
J-Jamet
3de2a9acfd fix: Update CHANGELOG 2025-10-21 12:37:40 +02:00
J-Jamet
a48dccf27a fix: Entries missing after database merge #2223 2025-10-21 12:36:23 +02:00
Ghost of Sparta
2a561fb37e Translated using Weblate (Hungarian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/hu/
2025-10-21 08:02:57 +00:00
Fjuro
e27a329ac5 Translated using Weblate (Czech)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/cs/
2025-10-21 08:02:51 +00:00
solokot
8e06a2a7cb Translated using Weblate (Russian)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-10-21 08:02:49 +00:00
Ihor Hordiichuk
ace82852af Translated using Weblate (Ukrainian)
Currently translated at 99.8% (702 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-10-21 08:02:47 +00:00
Fjuro
73369974b8 Translated using Weblate (Czech)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-10-21 08:02:46 +00:00
J-Jamet
332eda8a7a fix: Upgrade to 4.2.1 2025-10-20 19:43:20 +02:00
J-Jamet
e5ea1e35aa fix: Magikeyboard Auto search #2233 2025-10-20 19:41:00 +02:00
Sketch6580
86aae9635a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/zh_Hans/
2025-10-20 04:55:09 +00:00
Fjuro
db3ccae87d Translated using Weblate (Czech)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/cs/
2025-10-20 04:55:02 +00:00
jonnysemon
4cec26967c Translated using Weblate (Arabic)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-10-20 04:55:00 +00:00
Oğuz Ersen
a0368a4981 Translated using Weblate (Turkish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/tr/
2025-10-20 04:54:59 +00:00
Priit Jõerüüt
a1c7fe1e99 Translated using Weblate (Estonian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/et/
2025-10-20 04:54:58 +00:00
solokot
bf247ddeb7 Translated using Weblate (Russian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-10-20 04:54:56 +00:00
Liner Seven
1d2bc0fbfb Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ja/
2025-10-20 04:54:54 +00:00
Besnik Bleta
85a12fe4ee Translated using Weblate (Albanian)
Currently translated at 91.7% (645 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2025-10-20 04:54:36 +00:00
109247019824
a443ef996b Translated using Weblate (Bulgarian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-20 04:54:15 +00:00
Oğuz Ersen
c6995ad403 Translated using Weblate (Turkish)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-10-20 04:53:29 +00:00
Telaneo
9018807eb8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-10-20 04:53:15 +00:00
Stephan Paternotte
b463106dd5 Translated using Weblate (Dutch)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-10-20 04:52:13 +00:00
Liner Seven
a23d28e1fa Translated using Weblate (Japanese)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-10-20 04:52:06 +00:00
Ghost of Sparta
a0454f42d0 Translated using Weblate (Hungarian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-10-20 04:51:53 +00:00
Masowick
1c2ac88f47 Translated using Weblate (German)
Currently translated at 97.2% (684 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-20 04:51:10 +00:00
Fjuro
11eb1bae45 Translated using Weblate (Czech)
Currently translated at 97.5% (686 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-10-20 04:50:53 +00:00
Hosted Weblate
089d86165a Merge branch 'origin/develop' into Weblate. 2025-10-18 12:49:16 +02:00
109247019824
6a7362ad35 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-18 12:49:16 +02:00
Telaneo
d2c10e2e4e Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.1% (697 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-10-18 12:49:15 +02:00
jonnysemon
0c20a14e67 Translated using Weblate (Arabic)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-10-18 12:49:14 +02:00
Sketch6580
acccf290de Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-10-18 12:49:13 +02:00
Viktor Varvaruk
6ebe0f78af Translated using Weblate (Ukrainian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-10-18 12:49:12 +02:00
Matthaiks
935c09ccd2 Translated using Weblate (Polish)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-10-18 12:48:22 +02:00
Stephan Paternotte
1eb10ad5bd Translated using Weblate (Dutch)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-10-18 12:48:22 +02:00
Liner Seven
ca4283151e Translated using Weblate (Japanese)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-10-18 12:48:22 +02:00
Masowick
8fb98ca4e7 Translated using Weblate (German)
Currently translated at 97.2% (684 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-18 12:48:21 +02:00
Fjuro
be74c9710f Translated using Weblate (Czech)
Currently translated at 93.8% (660 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-10-18 12:48:21 +02:00
J-Jamet
24fb3c4c30 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-18 12:45:46 +02:00
solokot
3bdc5fe600 Translated using Weblate (Russian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-10-17 20:32:30 +02:00
Kunzisoft
c30884d6d0 Translated using Weblate (French)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-10-17 20:32:30 +02:00
VfBFan
5d26c3bd09 Translated using Weblate (German)
Currently translated at 97.2% (684 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-17 20:32:30 +02:00
J-Jamet
02e35cf5b7 Merge branch 'celenityy-master' into develop 2025-10-17 12:36:07 +02:00
Artyom Rybakov
085aefd2b9 Translated using Weblate (Russian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-10-17 12:33:01 +02:00
Kunzisoft
1ea5b7a50c Translated using Weblate (French)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/fr/
2025-10-17 12:33:00 +02:00
Kunzisoft
6cba96dd42 Translated using Weblate (French)
Currently translated at 99.7% (701 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-10-17 12:32:45 +02:00
Kunzisoft
46238a76bc Translated using Weblate (English)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2025-10-17 12:32:42 +02:00
Wilker Santana da Silva
d5a9b664a1 Translated using Weblate (English)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2025-10-17 12:32:42 +02:00
J-Jamet
6fdc4504d5 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-17 12:30:13 +02:00
J-Jamet
8a7c411a35 fix: title and description 2025-10-17 12:25:34 +02:00
Hosted Weblate
5ff9d5fa2f Merge branch 'origin/develop' into Weblate. 2025-10-17 11:35:46 +02:00
109247019824
bb0f3c80d3 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-17 11:35:45 +02:00
jonnysemon
597d9c8274 Translated using Weblate (Arabic)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-10-17 11:35:45 +02:00
Viktor Varvaruk
4dd8c06fd2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-10-17 11:35:44 +02:00
Wilker Santana da Silva
72c66b3cd9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.1% (676 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2025-10-17 11:35:43 +02:00
Liner Seven
c2223afa6f Translated using Weblate (Japanese)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-10-17 11:35:42 +02:00
J-Jamet
d338d1340f fix: App title and description 2025-10-17 11:33:06 +02:00
celenity
ed4423666b Add support for IronFox Nightly
Signed-off-by: celenity <celenity@celenity.dev>
2025-10-16 21:16:06 -04:00
Sketch6580
d21fe662ff Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/zh_Hans/
2025-10-16 12:52:52 +02:00
solokot
4da1c5bd92 Translated using Weblate (Russian)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-10-16 12:52:52 +02:00
Ghost of Sparta
18c18605fb Translated using Weblate (Hungarian)
Currently translated at 96.1% (676 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-10-16 12:52:52 +02:00
VfBFan
988cb1a8d0 Translated using Weblate (German)
Currently translated at 97.2% (684 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-16 12:52:51 +02:00
Milo Ivir
b6e01767e0 Translated using Weblate (Croatian)
Currently translated at 0.0% (0 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/hr/
2025-10-16 08:35:10 +02:00
Matthaiks
5414854e9c Translated using Weblate (Polish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/pl/
2025-10-16 08:35:10 +02:00
Martin Milchevski
ae7f0732c6 Translated using Weblate (Macedonian)
Currently translated at 0.0% (0 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/mk/
2025-10-16 08:35:10 +02:00
Stephan Paternotte
d49d33fe3a Translated using Weblate (Dutch)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/nl/
2025-10-16 08:35:09 +02:00
Fjuro
5e7fc2d468 Translated using Weblate (Czech)
Currently translated at 0.0% (0 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/cs/
2025-10-16 08:35:09 +02:00
jonnysemon
0d26e6a870 Translated using Weblate (Arabic)
Currently translated at 0.0% (0 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-10-16 08:35:09 +02:00
Priit Jõerüüt
dd92f9ceb6 Translated using Weblate (Estonian)
Currently translated at 0.0% (0 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/et/
2025-10-16 08:35:08 +02:00
Liner Seven
ff9239b9c4 Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ja/
2025-10-16 08:35:08 +02:00
Anonymous
319d35e485 Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ja/
2025-10-16 08:35:08 +02:00
VfBFan
28e65a4601 Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-10-16 08:35:07 +02:00
Masowick
eb626e5bfe Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-10-16 08:35:07 +02:00
VfBFan
e1decf9a23 Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-10-16 08:35:07 +02:00
109247019824
fff0e84b95 Translated using Weblate (Bulgarian)
Currently translated at 96.7% (680 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-16 08:35:06 +02:00
Sketch6580
d73a7004b1 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-10-16 08:35:04 +02:00
大王叫我来巡山
f71061e835 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-10-16 08:35:04 +02:00
Matthaiks
b2d25cc512 Translated using Weblate (Polish)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-10-16 08:35:03 +02:00
Stephan Paternotte
4d54b56c1d Translated using Weblate (Dutch)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-10-16 08:35:02 +02:00
Liner Seven
c764c6afff Translated using Weblate (Japanese)
Currently translated at 100.0% (703 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-10-16 08:35:02 +02:00
Sylvain Pichon
87b97a3849 Translated using Weblate (French)
Currently translated at 95.0% (668 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-10-16 08:35:01 +02:00
TKF
5e6db44476 Translated using Weblate (French)
Currently translated at 95.0% (668 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-10-16 08:35:01 +02:00
VfBFan
8615fa817f Translated using Weblate (German)
Currently translated at 95.3% (670 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-16 08:35:00 +02:00
Masowick
2f891bacd3 Translated using Weblate (German)
Currently translated at 95.3% (670 of 703 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-16 08:35:00 +02:00
J-Jamet
0d8a426df4 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-16 00:08:01 +02:00
J-Jamet
c952eb4415 Merge branch 'master' into develop 2025-10-15 23:59:04 +02:00
J-Jamet
2fd53b9416 fix: Update README.md 2025-10-15 23:58:49 +02:00
J-Jamet
244ca08890 Merge tag '4.2.0' into develop
4.2.0
2025-10-15 23:30:49 +02:00
J-Jamet
208f1e97d5 Merge branch 'release/4.2.0' 2025-10-15 23:30:40 +02:00
J-Jamet
e4e0628e20 fix: Upgrade CHANGELOG 2025-10-15 23:22:41 +02:00
J-Jamet
f60f31771f fix: Upgrade to 4.2.0 2025-10-15 23:16:45 +02:00
J-Jamet
ff6367bac4 fix: Change screens 2025-10-15 23:08:16 +02:00
J-Jamet
540e72812e fix: Passkey browser signature #2213 2025-10-15 21:15:10 +02:00
J-Jamet
5fe4af8e9d fix: Passkey multiple instance #2215 2025-10-15 20:09:27 +02:00
J-Jamet
ae42ab43b7 fix: Passkey multiple instance #2215 2025-10-15 19:37:56 +02:00
J-Jamet
c463055971 fix: Passkey back #2215 2025-10-15 15:46:26 +02:00
J-Jamet
1849dca81d fix: Form filling auto search #2204 2025-10-15 15:14:30 +02:00
J-Jamet
b3dd3dcfb5 fix: Toast "Usage parameter is null"
#2214
2025-10-14 15:36:08 +02:00
J-Jamet
fef88ff270 feat: Add KPEX_PASSKEY_FLAG_BE and KPEX_PASSKEY_FLAG_BS flags #2212 2025-10-14 14:21:52 +02:00
J-Jamet
f1f7dd1e6c fix: Upgrade to 4.2.0beta04 2025-10-14 14:12:01 +02:00
wheremygit
409f290e33 Translated using Weblate (Marathi)
Currently translated at 1.9% (13 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/mr/
2025-10-14 10:07:25 +02:00
J-Jamet
96c3af097a fix: Callback after registration 2025-10-13 14:57:13 +02:00
J-Jamet
4fe6b2e115 fix: Multiple validation 2025-10-13 11:48:12 +02:00
J-Jamet
cc936b9304 fix: Database Info 2025-10-13 11:08:56 +02:00
J-Jamet
e7f2a22583 fix: Hardware key dialog 2025-10-13 10:52:25 +02:00
J-Jamet
4bf905ecda fix: Hardware key #2196 2025-10-13 10:33:04 +02:00
J-Jamet
f8d80525d9 fix: Hardware key #2196 2025-10-13 10:32:43 +02:00
Linerly
7ce6092270 Translated using Weblate (Indonesian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2025-10-12 07:15:04 +02:00
J-Jamet
65857596a6 fix: Multiple launch instance 2025-10-10 13:52:08 +02:00
J-Jamet
e6253336bd fix: Remove intent data 2025-10-09 20:29:46 +02:00
J-Jamet
e5595a3275 fix: Database workflow 2025-10-09 19:41:36 +02:00
J-Jamet
366e8bf1d7 fix: IconCompat 2025-10-09 18:39:59 +02:00
J-Jamet
fa63265599 fix: null database 2025-10-09 15:13:58 +02:00
J-Jamet
755e0ea9a5 fix: Database State 2025-10-09 14:31:03 +02:00
Random
a819f2f8a8 Translated using Weblate (Italian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-10-09 08:07:29 +00:00
mrkaato0
c92da0a72f Translated using Weblate (Finnish)
Currently translated at 62.5% (415 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2025-10-09 08:07:27 +00:00
Fjuro
524963dbd8 Translated using Weblate (Czech)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-10-09 08:07:25 +00:00
J-Jamet
50b1ac388e fix: Complete refactoring of database action 2025-10-08 15:53:41 +02:00
J-Jamet
51c62034df fix: Refactoring database as flow 2025-10-08 12:11:41 +02:00
J-Jamet
e4d0cd89c6 fix: Small refactoring 2025-10-08 11:40:25 +02:00
Priit Jõerüüt
bfe50fa985 Translated using Weblate (Estonian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-10-07 18:02:24 +02:00
Besnik Bleta
3d798e6585 Translated using Weblate (Albanian)
Currently translated at 97.7% (649 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2025-10-07 18:02:23 +02:00
109247019824
068c59ac98 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-10-07 18:02:22 +02:00
Oğuz Ersen
34ec94a0c3 Translated using Weblate (Turkish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-10-07 18:02:18 +02:00
Telaneo
576a355342 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-10-07 18:02:18 +02:00
jonnysemon
aa19f11699 Translated using Weblate (Arabic)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-10-07 18:02:17 +02:00
大王叫我来巡山
2fb4dff46d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-10-07 18:02:15 +02:00
solokot
e6cf3f12a5 Translated using Weblate (Russian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-10-07 18:02:14 +02:00
Matthaiks
ca94ce86ba Translated using Weblate (Polish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-10-07 18:02:12 +02:00
Stephan Paternotte
dea6b25bb4 Translated using Weblate (Dutch)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-10-07 18:02:11 +02:00
Liner Seven
c48f64d331 Translated using Weblate (Japanese)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-10-07 18:02:09 +02:00
Ghost of Sparta
5e3a504c1f Translated using Weblate (Hungarian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-10-07 18:02:07 +02:00
Masowick
b9b7d7b2db Translated using Weblate (German)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-10-07 18:02:03 +02:00
J-Jamet
e085d5d277 fix: Remove unused code 2025-10-07 13:56:42 +02:00
J-Jamet
05336e93a0 fix: Special Characters #2180 2025-10-07 13:56:27 +02:00
J-Jamet
90b3b56893 feat: Warning when overwriting existing passkey #2124 2025-10-07 13:27:46 +02:00
J-Jamet
02c514272e feat: OTP tag #2122 2025-10-07 11:56:14 +02:00
J-Jamet
989e47ed12 Merge branch 'develop' into release/4.2.0 2025-10-06 17:38:54 +02:00
J-Jamet
1caf132558 Merge tag '4.1.9' into develop
4.1.9
2025-10-06 17:27:35 +02:00
J-Jamet
1b98bd740c Merge branch 'release/4.1.9' 2025-10-06 17:27:26 +02:00
J-Jamet
5adeb5cde0 fix: Tags 2025-10-06 17:20:59 +02:00
J-Jamet
b949d5d861 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-10-06 17:18:20 +02:00
J-Jamet
b4264a30a4 fix: Update description 2025-10-06 17:16:57 +02:00
J-Jamet
cf799c0f68 fix: Update to 4.1.9 2025-10-06 17:14:10 +02:00
J-Jamet
97f0ca519b fix: Killed service #2201 2025-10-06 16:59:42 +02:00
J-Jamet
cf4047b701 Merge branch 'chenxiaolong-landscape-insets' into develop 2025-10-06 13:58:39 +02:00
J-Jamet
40608a3eb5 Merge branch 'landscape-insets' of github.com:chenxiaolong/KeePassDX into chenxiaolong-landscape-insets 2025-10-06 13:58:20 +02:00
J-Jamet
99cb50d031 fix: Bug report title 2025-10-06 12:47:30 +02:00
Oğuz Ersen
b0d0c35241 Translated using Weblate (Turkish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/tr/
2025-10-05 18:02:07 +00:00
Mekyla Credo
6044c93a4a Translated using Weblate (Filipino)
Currently translated at 46.5% (309 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2025-10-05 18:02:05 +00:00
Oğuz Ersen
b544b5d54d Translated using Weblate (Turkish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-10-05 18:02:02 +00:00
Andrew Gunnerson
852378e484 Simplify inset logic, fix landscape mode, fix cutout overlapping
The commit primarily fixes a few overlapping issues caused by the window
inset handling. Previously, there were two main issues:

* Because setTransparentNavigationBar() checked for portrait mode, the
  inset logic never executed in landscape mode. This caused the app to
  overlap the status bar and navigation bar.

* The inset logic did not have handling for displayCutout insets. In
  landscape mode, this would cause the app to overlap the notch or
  camera hole punch area on phones.

In addition to fixing those issues, this commit simplifies the inset
logic a bit:

* applyWindowInsets() now accepts an EnumSet of WindowInsetPosition to
  avoid needing to duplicate logic for the various position
  combinations.

* Insets are now applied to the main container in the layout instead of
  individual elements where possible. This eliminates the need for the
  previous manual IME height handling logic in BOTTOM_IME vs
  TOP_BOTTOM_IME (for avoiding the insets being applied twice).

* Since insets are now applied to the main layout container,
  applyWindowInsets() now takes systemBars, displayCutout, and ime all
  into consideration. This should avoid all possible overlapping.

* Add support for using padding instead of margins for insets. This is
  used for GroupActivity's navigation drawer, where Material design
  intends for the drawer background to be drawn behind system bars.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-10-04 17:24:24 -04:00
Alonso González Chaves
711a344860 Translated using Weblate (Spanish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/es/
2025-10-04 14:02:05 +02:00
Alonso González Chaves
72087c7e5c Translated using Weblate (Spanish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-10-04 14:02:01 +02:00
J-Jamet
a337de3679 fix: Small refactoring 2025-10-01 15:41:49 +02:00
J-Jamet
75b37f5a9f fix: Settings 2025-09-30 15:29:10 +02:00
J-Jamet
075f54b286 feat: passkey selection after close database setting #2187 2025-09-30 15:19:05 +02:00
J-Jamet
e07cbc2e14 fix: Default backup state 2025-09-30 13:19:25 +02:00
J-Jamet
ac29b7bac7 fix: Launch immediately 2025-09-30 13:19:07 +02:00
J-Jamet
b9129cb941 fix: Update CHANGELOG 2025-09-29 19:40:28 +02:00
J-Jamet
6957fcd81a fix: Small bugs 2025-09-29 18:39:14 +02:00
J-Jamet
cfe56fc055 fix: Small error 2025-09-29 00:04:19 +02:00
J-Jamet
6f3e065ad1 fix: Back UI 2025-09-28 23:49:08 +02:00
J-Jamet
abfa7a3f47 fix: Registration and webDomain coroutine 2025-09-28 23:24:37 +02:00
J-Jamet
dd0d85e54e fix: Autofill refactoring 2025-09-26 21:42:22 +02:00
J-Jamet
76c20263f7 fix: Refactoring type mode call 2025-09-25 20:50:14 +02:00
J-Jamet
e447388611 fix: Refactoring activity launcher 2025-09-25 16:32:25 +02:00
J-Jamet
1bfec67c02 fix: Empty save parameter 2025-09-25 14:26:15 +02:00
J-Jamet
45041216d6 fix: Autofill recognition and ask to save disabled by default 2025-09-25 13:54:01 +02:00
J-Jamet
e075e9018c fix: Refactoring result launcher 2025-09-25 12:42:40 +02:00
J-Jamet
eed304ec40 fix: Search settings #2112 #2181 2025-09-24 22:36:50 +02:00
J-Jamet
5bcbbac97f fix: Save mode as registration mode 2025-09-24 19:25:24 +02:00
J-Jamet
ea4750fc11 fix: Change store description 2025-09-24 16:43:44 +02:00
J-Jamet
5037821529 Revert "fix: Backup eligibility default #2150"
This reverts commit 1d4e1687cf.
2025-09-24 14:33:43 +02:00
J-Jamet
3a4c88f19a fix: Empty database name #2159 2025-09-24 14:27:57 +02:00
J-Jamet
e960a8e169 fix: Upgrade to 4.2.0beta03 2025-09-24 14:14:29 +02:00
J-Jamet
1d4e1687cf fix: Backup eligibility default #2150 2025-09-24 14:13:49 +02:00
J-Jamet
033fa95285 Merge branch 'develop' into release/4.2.0 2025-09-24 13:21:22 +02:00
J-Jamet
f17d211fbd fix: Change appearance summary #2171 2025-09-24 13:19:57 +02:00
J-Jamet
cae69e7572 fix: Update CHANGELOG 2025-09-18 21:05:26 +02:00
J-Jamet
01d778650c feat: Setting for auto select #2165 2025-09-18 12:26:56 +02:00
J-Jamet
dd389dbab1 fix: Passkey coroutine 2025-09-18 11:03:07 +02:00
J-Jamet
272ebd0c3f fix: Passkey auto save Signature 2025-09-17 23:23:41 +02:00
J-Jamet
0aecc21f43 fix: Passkey workflow 2025-09-17 20:02:55 +02:00
J-Jamet
1e7e464e65 feat: Add dialog 2025-09-17 13:58:12 +02:00
Xo
ae903ad236 Translated using Weblate (Hebrew)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-09-15 09:02:00 +00:00
J-Jamet
d5c378ac85 fix: Private key format #2164 2025-09-14 23:48:27 +02:00
J-Jamet
672f1ca37d fix: Add toast error #2159 2025-09-14 13:17:49 +02:00
J-Jamet
2f9e1e4bf2 fix: Error message 2025-09-12 22:03:54 +02:00
J-Jamet
25d97e4f2e fix: Passkey Database Username 2025-09-12 21:20:12 +02:00
J-Jamet
f49dcbd654 fix: Unrecognized app that is not a browser #2157 2025-09-12 20:55:53 +02:00
J-Jamet
bf2d56b4fd feat: Add AAGUID Icons 2025-09-12 20:55:24 +02:00
Milo Ivir
7c3a15ce79 Translated using Weblate (Croatian)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2025-09-12 16:01:59 +00:00
J-Jamet
5893541dd2 Merge branch 'develop' into release/4.2.0 2025-09-12 16:14:19 +02:00
J-Jamet
2230fe66ab Merge tag '4.1.8' into develop
4.1.8
2025-09-12 16:04:08 +02:00
J-Jamet
84a62a32ff Merge branch 'release/4.1.8' 2025-09-12 16:03:59 +02:00
J-Jamet
da8ef9340c fix: Loading ViewModel 2025-09-12 15:23:32 +02:00
J-Jamet
af068349e4 fix: Upgrade to 4.1.8 2025-09-12 14:14:06 +02:00
J-Jamet
56cb5953dd fix: Deletable recycle bin #2163 2025-09-12 13:00:56 +02:00
J-Jamet
2fc2a9c7c1 fix: Delete algo during merge #1516 2025-09-11 21:19:40 +02:00
J-Jamet
69e7cdbc47 fix: Search with space #175 2025-09-11 16:43:40 +02:00
J-Jamet
39d9a74a73 fix: Warnings 2025-09-11 16:36:35 +02:00
shinebrillant
b609d4e182 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/zh_Hant/
2025-09-11 13:02:01 +00:00
J-Jamet
7212c73481 fix: Warnings 2025-09-11 14:55:37 +02:00
J-Jamet
3ee4caa153 fix: Warnings 2025-09-11 14:53:41 +02:00
J-Jamet
28e4d929bb fix: Warnings 2025-09-11 14:51:35 +02:00
J-Jamet
803d637510 fix: Backup parameters init 2025-09-11 12:04:39 +02:00
J-Jamet
ccd5da0962 feat: Add backup as setting #2135 2025-09-11 00:00:22 +02:00
Artyom Rybakov
e8ecf28f7c Translated using Weblate (Russian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-09-10 14:21:47 +02:00
shinebrillant
3d5adbfc01 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2025-09-10 14:21:46 +02:00
Matthaiks
72bfc50703 Translated using Weblate (Polish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-09-10 14:21:46 +02:00
J-Jamet
36e3b85400 fix: Warnings 2025-09-09 20:55:30 +02:00
J-Jamet
cd73880e21 fix: Warnings 2025-09-09 14:06:52 +02:00
Matthaiks
a60e2e780d Translated using Weblate (Polish)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-09-09 12:01:58 +00:00
Retrial
9210851765 Translated using Weblate (Greek)
Currently translated at 100.0% (664 of 664 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2025-09-09 12:01:56 +00:00
J-Jamet
8337f98f3a fix: Update to 4.2.0beta01 2025-09-09 13:57:58 +02:00
J-Jamet
47fbb562b7 Merge branch 'develop' into feature/Passkeys 2025-09-09 13:38:15 +02:00
J-Jamet
a46251be7b fix: Better biometric exception implementation 2025-09-09 13:37:50 +02:00
J-Jamet
ef98e8a2db Merge branch 'develop' into feature/Passkeys 2025-09-09 11:51:41 +02:00
J-Jamet
e8ec27dc38 fix: Upgrade to API 35 2025-09-09 11:45:30 +02:00
J-Jamet
30dd7c567c fix: Autofill compatibility package 2025-09-08 15:17:09 +02:00
J-Jamet
e562694606 fix: Min Android version for Autofill 2025-09-08 15:07:19 +02:00
J-Jamet
464bc1442d fix: Min Android version 2025-09-08 15:01:19 +02:00
J-Jamet
c1730353d0 fix: Min Android version 2025-09-08 15:00:27 +02:00
J-Jamet
55e32e4ac5 fix: Web Origin 2025-09-08 14:19:30 +02:00
J-Jamet
96ed9fc7a6 fix: Dummy URL 2025-09-08 14:02:52 +02:00
J-Jamet
5fda628c9c fix: Remove unused dependency 2025-09-08 13:59:51 +02:00
J-Jamet
17742e25a9 fix: Move community json in free build 2025-09-08 13:58:04 +02:00
J-Jamet
8086289e4b fix: Small adjustments 2025-09-08 13:34:23 +02:00
J-Jamet
65157f661f fix: Add Passkey username field 2025-09-08 13:07:07 +02:00
J-Jamet
5df637d01f fix: Add Passkey username field 2025-09-08 13:06:42 +02:00
J-Jamet
8084920b9e Merge branch 'develop' into feature/Passkeys 2025-09-08 12:54:04 +02:00
J-Jamet
b196145578 Merge branch 'milotype-croatian-translation-20250907' into translations
# Conflicts:
#	fastlane/metadata/android/hr/full_description.txt
#	fastlane/metadata/android/hr/short_description.txt
#	fastlane/metadata/android/hr/title.txt
2025-09-08 12:45:09 +02:00
J-Jamet
ac347db2d1 fix: Translations 2025-09-08 12:43:06 +02:00
J-Jamet
013c437cf7 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-09-08 12:38:51 +02:00
Milo Ivir
1f600d60e3 Translated using Weblate (Croatian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/hr/
2025-09-08 12:33:27 +02:00
J-Jamet
a6af9976fc fix: Change wording 2025-09-08 12:18:51 +02:00
J-Jamet
05c480b6d3 feat: Multiple custom list 2025-09-07 20:19:34 +02:00
Milo Ivir
d5ecaeb331 Add Croatian translation 2025-09-07 16:29:42 +02:00
Milo Ivir
db8b0100de Translated using Weblate (Croatian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2025-09-07 15:51:05 +02:00
Ihor Hordiichuk
5f41177a1f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-09-06 12:01:59 +00:00
J-Jamet
fb909dac52 feat: Dialog to ask privileged app 2025-09-05 14:27:04 +02:00
J-Jamet
a8130d67be fix: Wiki link 2025-09-03 19:29:36 +02:00
J-Jamet
754d195e26 fix: Selected privileged app 2025-09-03 17:35:48 +02:00
J-Jamet
074910ea19 feat: Add credential provider setting 2025-09-03 17:02:17 +02:00
J-Jamet
988b18b515 feat: JSON parser to manage Privileged apps 2025-09-03 14:25:53 +02:00
J-Jamet
8924254c25 feat: Custom settings and privileged 2025-09-02 19:57:02 +02:00
Artyom Rybakov
0db2b7023e Translated using Weblate (Russian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-09-02 14:02:02 +00:00
Artyom Rybakov
a2c2a21dde Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-09-02 14:02:00 +00:00
scollovati
d7a3e7fedd Translated using Weblate (Italian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-09-02 14:01:58 +00:00
J-Jamet
2bedbf8a6c feat: Add priviled apps 2025-09-02 14:06:11 +02:00
J-Jamet
437a704bc8 fix: Small refactoring 2025-09-02 13:11:55 +02:00
J-Jamet
a3bd5e1593 fix: README 2025-09-02 12:47:50 +02:00
J-Jamet
3feb177afc fix: R attribute 2025-09-02 12:47:04 +02:00
J-Jamet
821f35fe05 Merge branch 'feature/passkeys_validation' into feature/Passkeys 2025-09-02 12:26:30 +02:00
J-Jamet
d36f675da7 fix: First validation pass 2025-09-02 11:49:40 +02:00
J-Jamet
b7f9690a38 Merge branch 'develop' into feature/Passkeys 2025-09-01 19:12:15 +02:00
J-Jamet
5e4ee167fc Merge branch 'rmacklin-remember-last-read-only-state-of-each-database' into develop 2025-09-01 19:02:59 +02:00
J-Jamet
c911b7c511 fix: Import DatabaseFile 2025-09-01 19:01:35 +02:00
J-Jamet
c79d1f1b81 Merge branch 'Dev-ClayP-master' into develop 2025-09-01 18:19:11 +02:00
J-Jamet
daf717becd fix: Remove JELLY_BEAN_MR1 conditions and unused PRNGFixes 2025-09-01 18:15:00 +02:00
J-Jamet
48d4483484 Merge branch 'master' of github.com:Dev-ClayP/KeePassDX into Dev-ClayP-master 2025-09-01 17:34:46 +02:00
J-Jamet
c861fe790c fix: Change backup eligibility 2025-09-01 16:05:47 +02:00
J-Jamet
1a717bda03 fix: Security exception 2025-09-01 15:53:45 +02:00
J-Jamet
b80acd5a2d fix: Add unit test for app signature and fix multiple fingerprint 2025-09-01 15:42:39 +02:00
J-Jamet
7e41527cfe fix: Refactoring verification 2025-09-01 15:29:19 +02:00
J-Jamet
200881278c fix: Change Android origin 2025-09-01 14:48:36 +02:00
J-Jamet
0d133ffdb0 feat: Change Android origin 2025-09-01 11:49:13 +02:00
leap123
c6b0ee27df Translated using Weblate (Indonesian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/id/
2025-08-31 17:01:54 +00:00
Priit Jõerüüt
0053726d0b Translated using Weblate (Estonian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-08-30 18:16:53 +02:00
leap123
1395af88d1 Translated using Weblate (Indonesian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2025-08-30 18:16:52 +02:00
Fjuro
2e3ade1b4a Translated using Weblate (Czech)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-08-30 18:16:52 +02:00
XiveZ
90c43acfbf Translated using Weblate (Belarusian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/be/
2025-08-30 07:03:02 +02:00
Ghost of Sparta
90b68fd972 Translated using Weblate (Hungarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-08-30 07:03:02 +02:00
J-Jamet
f8787ba03d fix: Add webOrigin, fix title and add verification state 2025-08-29 18:41:40 +02:00
J-Jamet
4f10d13691 fix: Small refactoring and add doc 2025-08-29 12:23:44 +02:00
XiveZ
ef6aeceb20 Translated using Weblate (Belarusian)
Currently translated at 5.2% (35 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/be/
2025-08-29 07:25:41 +02:00
jonnysemon
ef8685f0e7 Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-08-29 07:25:41 +02:00
XiveZ
3021ed158b Added translation using Weblate (Belarusian) 2025-08-29 07:02:51 +02:00
weblator
a57043f496 Translated using Weblate (Azerbaijani)
Currently translated at 96.8% (645 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2025-08-29 07:02:51 +02:00
weblator
fdfd124fee Translated using Weblate (Serbian)
Currently translated at 50.3% (335 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr/
2025-08-29 07:02:50 +02:00
weblator
71739de91a Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 19.9% (133 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/yue_Hant/
2025-08-29 07:02:50 +02:00
weblator
041b1fbf53 Translated using Weblate (Burmese)
Currently translated at 6.4% (43 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/my/
2025-08-29 07:02:50 +02:00
weblator
3a72b32b4a Translated using Weblate (Slovenian)
Currently translated at 9.7% (65 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sl/
2025-08-29 07:02:50 +02:00
weblator
994f174300 Translated using Weblate (Marathi)
Currently translated at 1.2% (8 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/mr/
2025-08-29 07:02:49 +02:00
weblator
c0f32254bb Translated using Weblate (Esperanto)
Currently translated at 21.1% (141 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eo/
2025-08-29 07:02:49 +02:00
109247019824
fd98dbeebe Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-08-29 07:02:49 +02:00
weblator
69ac6e6698 Translated using Weblate (Serbian (Latin script))
Currently translated at 59.0% (393 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr_Latn/
2025-08-29 07:02:48 +02:00
weblator
35e224d227 Translated using Weblate (Portuguese)
Currently translated at 99.6% (664 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2025-08-29 07:02:47 +02:00
weblator
2da8552a53 Translated using Weblate (Javanese)
Currently translated at 3.6% (24 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/jv/
2025-08-29 07:02:47 +02:00
weblator
a9a5047949 Translated using Weblate (Persian)
Currently translated at 43.8% (292 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fa/
2025-08-29 07:02:47 +02:00
weblator
17c98f7fea Translated using Weblate (Indonesian)
Currently translated at 99.6% (664 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2025-08-29 07:02:47 +02:00
weblator
c3bc890665 Translated using Weblate (Malayalam)
Currently translated at 56.4% (376 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2025-08-29 07:02:47 +02:00
weblator
7a295c2541 Translated using Weblate (Punjabi)
Currently translated at 48.4% (323 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pa/
2025-08-29 07:02:46 +02:00
weblator
01b1b74c6a Translated using Weblate (Bengali (Bangladesh))
Currently translated at 13.3% (89 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn_BD/
2025-08-29 07:02:46 +02:00
weblator
fd25d21c72 Translated using Weblate (Hindi)
Currently translated at 21.9% (146 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hi/
2025-08-29 07:02:46 +02:00
Telaneo
6b1d8d24dd Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-08-29 07:02:46 +02:00
大王叫我来巡山
5d002f5128 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-08-29 07:02:45 +02:00
weblator
98314c466f Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.6% (664 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2025-08-29 07:02:44 +02:00
Matthaiks
4f7afd7c97 Translated using Weblate (Polish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-08-29 07:02:44 +02:00
weblator
a9e139ff7e Translated using Weblate (Norwegian Nynorsk)
Currently translated at 42.6% (284 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nn/
2025-08-29 07:02:44 +02:00
Stephan Paternotte
4ff483a8d2 Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-08-29 07:02:44 +02:00
weblator
1916b79df1 Translated using Weblate (Lithuanian)
Currently translated at 60.2% (401 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2025-08-29 07:02:44 +02:00
Liner Seven
98e15a7717 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-08-29 07:02:43 +02:00
Masowick
dfd18e3c7f Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-08-29 07:02:42 +02:00
weblator
8fbbaae05b Translated using Weblate (English)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2025-08-29 07:02:41 +02:00
J-Jamet
98007c962d fix: Browser selection and URL scheme 2025-08-27 23:27:48 +02:00
J-Jamet
5f27f161a5 fix: Allow to check multiple app signatures #1421 2025-08-27 23:10:16 +02:00
J-Jamet
fcf723849b fix: Move application signature function 2025-08-27 19:14:24 +02:00
Richard Macklin
8a60056866 Remove no-longer-needed enable_read_only_* string resources
(We just removed the usages of these strings)
2025-08-26 20:10:46 -07:00
Richard Macklin
e9d20a51a5 Remove global "Write-protected" setting
This addresses J-Jamet's feedback that keeping the global setting would
potentially lead to confusion, so it should be removed now that we are
remembering each database's read-only state.
2025-08-26 20:10:42 -07:00
Richard Macklin
a28d77ba32 feat: Remember the last read-only state of each database
The app has supported a global setting for opening (all) databases in
read-only mode. But that's not particularly flexible for the use case
where you have one database that should be read-only and one that should
be read-write.

Previously, to handle this use case you could open one database in
read-only mode, but the next time you attempted to open the same
database, it would "forget" that, so you would have to toggle it to
read-only mode again manually. This commit changes that behavior so that
if you toggle a database to read-only mode, it'll be remembered the next
time you open the database. (You can still toggle it back to read-write
if you change your mind, and that, too, will be remembered the next time
you open the database.)
2025-08-26 20:08:22 -07:00
J-Jamet
5bd866e104 feat: Add app Signature 2025-08-26 17:08:23 +02:00
J-Jamet
9985c6065d fix: Change Android scheme 2025-08-26 10:50:34 +02:00
J-Jamet
1f2e4a3719 fix: Change parameters 2025-08-25 20:30:28 +02:00
J-Jamet
fa2555a3f7 fix: Remove decodeHexToByteArray 2025-08-25 20:15:46 +02:00
cali-95
b4de7afe77 Fix constant intent request code
only one passkey was used, even if the users selected another one for the same relying party.
2025-08-25 20:10:10 +02:00
cali-95
736cafbcc2 Add support for ed25519 2025-08-25 20:09:42 +02:00
Telaneo
d143605a40 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-08-25 17:28:06 +02:00
J-Jamet
f2f4c1e63d fix: Origin parameters and callingAppInfo 2025-08-25 15:38:34 +02:00
J-Jamet
bc86ee87a0 fix: package name 2025-08-24 23:44:13 +02:00
J-Jamet
5cbd60c024 Merge branch 'develop' into feature/Passkeys 2025-08-23 14:52:51 +02:00
J-Jamet
15972efb4f Merge branch 'develop' 2025-08-23 00:40:23 +02:00
J-Jamet
dae5f65c0d Revert "fix: revert checkUnlock"
This reverts commit 564b5f10ea.
2025-08-23 00:20:48 +02:00
J-Jamet
564b5f10ea fix: revert checkUnlock 2025-08-23 00:03:59 +02:00
J-Jamet
e6e40f9bd4 fix: Update to 4.1.7 2025-08-22 23:54:03 +02:00
J-Jamet
bd15e36b52 fix: Multiple biometric prompt 2025-08-22 22:50:58 +02:00
J-Jamet
43faca3061 fix: Multiple call to check availability 2025-08-22 22:34:11 +02:00
J-Jamet
82af9bada2 fix: CipherDatabase listener and rename advanced unlock in device unlock 2025-08-22 22:05:51 +02:00
J-Jamet
5817273872 fix: Passkey icon color 2025-08-22 11:57:54 +02:00
J-Jamet
32d6a11353 feat: Add passkey icon in entry list #1421 2025-08-22 11:45:59 +02:00
J-Jamet
9477fba704 fix: UUID string format #1421 2025-08-22 11:37:30 +02:00
J-Jamet
80b16bccf1 fix: UUIDUtils and fixed AAGUID #1421 2025-08-22 11:04:53 +02:00
Liner Seven
2befa68c93 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-08-22 11:02:30 +02:00
J-Jamet
6672085d84 fix: Exclude field for form filling #2097 2025-08-21 21:25:26 +02:00
J-Jamet
05a39f6922 fix: Show dedicated Passkey view #2097 2025-08-21 20:47:12 +02:00
J-Jamet
40e8dea485 fix: Capture exception 2025-08-20 21:13:07 +02:00
J-Jamet
7e09532d5d fix: Add check security 2025-08-20 20:56:40 +02:00
Random
4034a2bfc4 Translated using Weblate (Italian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/it/
2025-08-20 13:02:22 +00:00
Nara Huang
0d93e867cf Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2025-08-20 13:02:18 +00:00
J-Jamet
44e8f4f406 fix: AutoSearch 2025-08-20 12:29:24 +02:00
J-Jamet
e3083c7773 fix: search parameters 2025-08-20 10:11:55 +02:00
J-Jamet
d0c0c4a4d6 fix: gitignore .kotlin 2025-08-20 10:11:29 +02:00
J-Jamet
a9e8de26f8 fix: client data hash 2025-08-20 09:13:28 +02:00
J-Jamet
c7a256ebf1 Merge branch 'develop' into feature/Passkeys 2025-08-20 08:39:02 +02:00
Hosted Weblate
8cac4eb51c Merge branch 'origin/develop' into Weblate. 2025-08-19 10:32:22 +02:00
Alex Ionescu
933d34ff1d Translated using Weblate (Romanian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2025-08-19 10:32:21 +02:00
J-Jamet
d34f460b98 fix: Remove title in template issue 2025-08-19 10:12:11 +02:00
J-Jamet
7632face63 fix: Update issue template 2025-08-19 10:09:48 +02:00
J-Jamet
c4cbd2e587 fix: tags 2025-08-19 09:28:59 +02:00
J-Jamet
ad454c2e4a Merge branch 'translations' into develop 2025-08-19 09:20:35 +02:00
J-Jamet
fbb03c3ecf Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-08-19 09:19:55 +02:00
J-Jamet
1a4d963d53 fix: Background event 2025-08-18 20:57:38 +02:00
J-Jamet
0c58992c21 feat: New github templates 2025-08-18 19:47:25 +02:00
J-Jamet
0eaeb3b90b fix: Upgrade CHANGELOG 2025-08-18 18:55:37 +02:00
J-Jamet
fba8443d0a fix: logs 2025-08-18 18:45:11 +02:00
J-Jamet
601874442c fix: Better database loading 2025-08-18 18:29:48 +02:00
J-Jamet
fa34618d67 fix: Connect database 2025-08-18 16:55:39 +02:00
J-Jamet
a60fc83379 fix: old device code 2025-08-18 16:28:45 +02:00
J-Jamet
c2c9ebe4c7 fix: Orientatino change in old device 2025-08-18 16:08:14 +02:00
J-Jamet
fdd86e2b9e fix: cryptoPrompt private variable 2025-08-18 13:16:26 +02:00
J-Jamet
34f0f45862 fix: Add app in background event 2025-08-18 13:12:32 +02:00
J-Jamet
c0345d4dc4 Revert "fix: Smal fix"
This reverts commit 24e859c4ce.
2025-08-18 12:11:17 +02:00
J-Jamet
24e859c4ce fix: Smal fix 2025-08-18 12:09:31 +02:00
J-Jamet
1fff0c526c fix: Authentication error 2025-08-18 10:03:02 +02:00
J-Jamet
1ee1bb8d95 fix: Device credential operation 2025-08-18 09:55:46 +02:00
J-Jamet
b8e996a5af fix: Orientation change 2025-08-17 22:45:00 +02:00
J-Jamet
df999002af fix: DeviceUnlockManager init 2025-08-17 13:36:36 +02:00
J-Jamet
64f5a4152b fix: prompt null 2025-08-17 13:24:17 +02:00
J-Jamet
0af99d1830 fix: Orientation change 2025-08-17 13:16:21 +02:00
J-Jamet
733f337312 fix: Lock orientation change 2025-08-17 11:54:39 +02:00
J-Jamet
1f99b1f884 fix: Error Keystore #2114 2025-08-17 11:24:52 +02:00
J-Jamet
c3b5598a09 fix: Auto open biometric prompt from database list #2113 Update to 4.1.6 2025-08-17 10:09:04 +02:00
J-Jamet
d0ab5267cf fix: Retrieve client data hash 2025-08-17 09:55:23 +02:00
J-Jamet
88b701fd39 fix: Remove Play store dependency 2025-08-16 20:23:03 +02:00
J-Jamet
4a1cee619c fix: Refactiring JSON objects 2025-08-16 19:57:50 +02:00
jonnysemon
26cb36ccd0 Translated using Weblate (Arabic)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-08-16 07:02:04 +02:00
Kaya Zeren
67e0496efc Translated using Weblate (Turkish)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-08-15 00:02:25 +00:00
Ihor Hordiichuk
6c4d040564 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-08-15 00:02:23 +00:00
J-Jamet
c7741115ff Merge branch 'develop' into feature/Passkeys 2025-08-14 17:39:52 +02:00
J-Jamet
cb5a725d50 Merge tag '4.1.5' into develop
4.1.5
2025-08-14 17:18:34 +02:00
J-Jamet
1c30d533ad Merge branch 'release/4.1.5' 2025-08-14 17:18:23 +02:00
J-Jamet
845d1a581b fix: warnings 2025-08-14 17:14:01 +02:00
J-Jamet
23bebf9597 fix: Auto prompt #2111 2025-08-14 17:02:40 +02:00
J-Jamet
536e038306 Merge branch 'release/4.1.4' 2025-08-14 13:04:44 +02:00
J-Jamet
9e1f6d29a5 fix: update Gemfile.lock 2025-08-14 12:28:22 +02:00
J-Jamet
7a9469e59d fix: code improvement #2105 2025-08-13 19:21:42 +02:00
J-Jamet
c12eb3d643 fix: error code 28 #2105 2025-08-13 19:03:15 +02:00
J-Jamet
da0f02e536 fix: better prompt variable management #2105 2025-08-13 18:27:51 +02:00
J-Jamet
04bcc6631c fix: open auto prompt too often #2105 2025-08-13 17:59:56 +02:00
J-Jamet
698e3b7fb1 fix: auto open prompt #2105 2025-08-13 11:49:54 +02:00
J-Jamet
6de02384c1 fix: close prompt #2105 2025-08-13 11:25:36 +02:00
J-Jamet
df3bd7e0a1 fix: cipher call #2105 2025-08-13 11:03:50 +02:00
J-Jamet
c8c232639f fix: better cipher and prompt workflow #2105 2025-08-12 19:49:31 +02:00
J-Jamet
192d6eedd0 fix: autoprompt thread #2105 2025-08-12 15:51:41 +02:00
J-Jamet
9cae3f0794 fix: initDecryptData #2105 2025-08-12 15:33:35 +02:00
J-Jamet
a680db9707 fix: initDecryptData #2105 2025-08-12 15:20:32 +02:00
J-Jamet
fe526089d7 fix: auto prompt #2105 2025-08-12 14:54:52 +02:00
J-Jamet
dfd7ade416 fix: Regression #2105 2025-08-12 13:42:57 +02:00
J-Jamet
3cd65345c5 fix: Small biometric fixes 2025-08-12 13:04:52 +02:00
J-Jamet
2d398908de fix: refresh when activate setting 2025-08-12 11:11:12 +02:00
J-Jamet
756454abc3 fix: update CHANGELOG 2025-08-12 10:52:42 +02:00
J-Jamet
b7619b45b1 fix: Transition deprecation 2025-08-12 10:51:15 +02:00
J-Jamet
1369a3cad9 fix: test dependency and gradle version 2025-08-12 10:26:32 +02:00
J-Jamet
f46c062c4e fix: Auto biometric prompt #2105 2025-08-10 22:34:48 +02:00
J-Jamet
0a0abef4d4 fix: Keystore exception 2025-08-10 21:37:41 +02:00
J-Jamet
3a8245ee74 fix: Exception as Snackbar 2025-08-10 21:32:12 +02:00
J-Jamet
7be554a378 fix: unlock manager #2098 #2101 2025-08-10 12:24:23 +02:00
Masowick
7007efa627 Translated using Weblate (German)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-08-07 08:01:53 +02:00
J-Jamet
6c37f7b12c fix: Update to 4.1.4 2025-08-03 18:17:56 +02:00
Mark Bengtsson
05defff5ef Translated using Weblate (Swedish)
Currently translated at 72.2% (482 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2025-07-31 16:02:37 +02:00
ssantos
e4569662ba Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2025-07-31 16:02:34 +02:00
Xo
4727f7a761 Translated using Weblate (Hebrew)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-07-31 16:02:32 +02:00
scollovati
89b15e715d Translated using Weblate (Italian)
Currently translated at 33.3% (1 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/it/
2025-07-29 10:24:20 +02:00
Liner Seven
ef72df02e3 Translated using Weblate (Japanese)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-07-29 10:24:18 +02:00
J-Jamet
19c987abc3 Merge branch 'develop' into feature/Passkeys 2025-07-26 19:43:55 +02:00
Matthaiks
b6201262f1 Translated using Weblate (Polish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/pl/
2025-07-26 12:05:52 +00:00
Viktor Varvaruk
b99fa9ffcf Translated using Weblate (Ukrainian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-07-26 12:05:50 +00:00
scollovati
1497ab85b2 Translated using Weblate (Italian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-07-26 12:05:49 +00:00
Ghost of Sparta
2ade463974 Translated using Weblate (Hungarian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-07-26 12:05:47 +00:00
J-Jamet
3a67ec09d5 fix: Update CHANGELOG 2025-07-25 16:04:02 +02:00
J-Jamet
dca800b1bb Merge tag '4.1.3' into develop
4.1.3
2025-07-25 15:56:49 +02:00
J-Jamet
70665f110d Merge branch 'release/4.1.3' 2025-07-25 15:56:17 +02:00
J-Jamet
3b39cafb99 fix: Warnings 2025-07-25 14:30:47 +02:00
Priit Jõerüüt
0c1b94468d Translated using Weblate (Estonian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-07-25 12:58:50 +02:00
109247019824
7a841bbf57 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-07-25 12:58:50 +02:00
jonnysemon
0066f1c77a Translated using Weblate (Arabic)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-07-25 12:58:50 +02:00
大王叫我来巡山
14dbff603d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-07-25 12:58:50 +02:00
solokot
42afa93293 Translated using Weblate (Russian)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-07-25 12:58:50 +02:00
Matthaiks
2fa25a51ad Translated using Weblate (Polish)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-07-25 12:58:50 +02:00
Stephan Paternotte
2b7f41477f Translated using Weblate (Dutch)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-07-25 12:58:49 +02:00
Sylvain Pichon
be0d5f80c3 Translated using Weblate (French)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-07-25 12:58:49 +02:00
Retrial
3844188fcc Translated using Weblate (Greek)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2025-07-25 12:58:49 +02:00
VfBFan
ef81ba5a8f Translated using Weblate (German)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-07-25 12:58:49 +02:00
Fjuro
2871668d8f Translated using Weblate (Czech)
Currently translated at 100.0% (667 of 667 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-07-25 12:58:49 +02:00
J-Jamet
d03693341e Merge branch 'develop' into feature/Passkeys 2025-07-24 22:22:24 +02:00
J-Jamet
2b5ecb2f84 fix: Simplify query #2096 2025-07-24 22:19:39 +02:00
J-Jamet
e397b92c36 fix: Search and allow empty query #2096 2025-07-24 21:29:43 +02:00
J-Jamet
e273eb6e03 fix: Replace strong tag 2025-07-24 20:36:01 +02:00
J-Jamet
28b624afa3 fix: Descriptions 2025-07-24 20:30:14 +02:00
J-Jamet
fb1e6cdc3f Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-07-24 20:23:50 +02:00
jonnysemon
8b6499d040 Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-07-24 20:16:45 +02:00
Kunzisoft
054af507ad Translated using Weblate (French)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-07-24 20:16:44 +02:00
J-Jamet
bf496333eb Merge branch 'develop' into feature/Passkeys 2025-07-24 20:12:05 +02:00
J-Jamet
ac9bb9b666 fix: Biometric error #2081 2025-07-24 19:41:28 +02:00
J-Jamet
809e1929e5 fix: Biometric error #2081 2025-07-24 19:21:29 +02:00
J-Jamet
a1b1338d67 fix: Biometric after orientation change #2081 2025-07-24 19:12:25 +02:00
J-Jamet
bd4cacfab1 Merge branch 'IzzySoft-fastlane' into develop 2025-07-24 18:26:32 +02:00
J-Jamet
e0343bdc55 Merge branch 'IzzySoft-iod' into develop 2025-07-24 18:23:40 +02:00
J-Jamet
b743d004e2 fix: Template EMAIL #1986 2025-07-24 18:22:11 +02:00
J-Jamet
4b20e035b2 fix: Upgrade CHANGELOG 2025-07-24 17:12:09 +02:00
J-Jamet
afe5fddc50 Merge branch 'shauser88-fix-save-copy-localtime' into develop 2025-07-24 17:09:48 +02:00
J-Jamet
d68ca1b51f Merge branch 'fix-save-copy-localtime' of github.com:shauser88/KeePassDX into shauser88-fix-save-copy-localtime 2025-07-24 17:08:55 +02:00
Jérémy JAMET
061b087229 Merge pull request #2095 from d20n/fix-en_GB-strings
Fix several incomplete en_GB strings
2025-07-24 16:59:23 +02:00
J-Jamet
bb3a379965 fix: Upgrade CHANGELOG 2025-07-24 16:42:35 +02:00
J-Jamet
593b5c6338 fix: Biometric error prompts #2081 2025-07-24 16:39:42 +02:00
J-Jamet
56f8a1bf9f fix: Upgrade version and CHANGELOG 2025-07-22 18:45:44 +02:00
J-Jamet
962b547b36 fix: Registration #2089 2025-07-22 18:40:36 +02:00
J-Jamet
c6b01947b3 fix: Multiple request 2025-07-22 13:45:44 +02:00
Izzy
6df8ff4310 fastlane: slight formatting adjustments to full descriptions 2025-07-20 00:31:38 +02:00
Martin Milchevski
52f17140b8 Translated using Weblate (Macedonian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/mk/
2025-07-20 00:03:07 +02:00
ginger-co
75c2bb4a87 Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-07-20 00:03:05 +02:00
Martin Milchevski
f36f6c3155 Translated using Weblate (Macedonian)
Currently translated at 4.8% (32 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/mk/
2025-07-18 23:16:13 +02:00
Martin Milchevski
b88b92c5b0 Added translation using Weblate (Macedonian) 2025-07-18 23:00:51 +02:00
pigmentblue15
d2c569c4f0 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-07-16 17:01:50 +02:00
J-Jamet
91781f36ac fix: Registration callback 2025-07-16 16:15:55 +02:00
J-Jamet
3fbdf78ba1 fix: Search Info 2025-07-16 15:30:21 +02:00
J-Jamet
d1f463d497 fix: Refactoring Credential Provider 2025-07-16 14:03:48 +02:00
Izzy
cb1316564e IzzyOnDroid provides both, the Free and the Libre variants 2025-07-15 09:51:50 +02:00
VfBFan
245d3f7df2 Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-07-14 19:02:00 +02:00
Random
3729b3c5a0 Translated using Weblate (Italian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-07-14 19:01:52 +02:00
Random
7ce5eb3c27 Translated using Weblate (Italian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2025-07-14 19:01:50 +02:00
VfBFan
43defea85e Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-07-14 19:01:48 +02:00
d20n
8470c4e39b Fix several incomplete en_GB strings 2025-07-14 17:20:42 +02:00
Dev-ClayP
1f678fc975 Update Loupe.kt
removed old sdk checks
2025-07-09 11:24:30 -04:00
Dev-ClayP
082c839639 Update EntryEditActivity.kt
removed old sdk checks
2025-07-09 11:18:59 -04:00
Dev-ClayP
600d548fce Update BroadcastAction.kt
removed old SDK check
2025-07-09 11:15:46 -04:00
Dev-ClayP
3035f9b686 Update TimeoutHelper.kt
Removed old SDK check
2025-07-09 11:14:18 -04:00
Dev-ClayP
6eae0f02d3 Update FileDatabaseSelectActivity.kt
removed PackageManager.allowCreateDocumentByStorageAccessFramework()
2025-07-09 11:11:53 -04:00
Dev-ClayP
87be2f4b9e Update UriHelper.kt
removed PackageManager.allowCreateDocumentByStorageAccessFramework()  it will always eval to true with sdk update
2025-07-09 11:09:44 -04:00
Dev-ClayP
3b054504a1 Update UriUtil.kt
removed another kitkat check
2025-07-09 11:05:01 -04:00
Dev-ClayP
a88f6b968a Update UriUtil.kt
Removed KitKat sdk check
2025-07-09 11:04:12 -04:00
Dev-ClayP
1fc4f150bf Update item_breadcrumb.xml
Removed jelly_bean target check
2025-07-09 10:45:16 -04:00
Dev-ClayP
1f4e59cbdc Update fragment_set_otp.xml
Removed sdk target checks for jelly_bean
2025-07-09 10:44:21 -04:00
Dev-ClayP
b5dc8d9adf Update build.gradle
changed minsdk to 19
2025-07-09 10:40:38 -04:00
Dev-ClayP
43f7e08548 Update build.gradle
changed minsdk to 19
2025-07-09 10:40:13 -04:00
Dev-ClayP
05fc6f87ec Update build.gradle
changed minsdk to 19
2025-07-09 10:39:34 -04:00
Dev-ClayP
daae535fa1 Update TemplateView.kt
Removed old SDK checks
2025-07-09 10:28:27 -04:00
Dev-ClayP
90c8cb3455 Update ViewUtil.kt
Removed SDK check
2025-07-09 10:26:12 -04:00
Dev-ClayP
daeee10de9 Update TemplateEditView.kt
Removed old SDK check
2025-07-09 10:25:15 -04:00
Dev-ClayP
6c1c401a71 Update NodesAdapter.kt
Removed old SDK check
2025-07-09 10:23:44 -04:00
Dev-ClayP
fd7f0fceb2 Update UriUtil.kt
Removed Old SDK Check
2025-07-09 10:21:55 -04:00
Dev-ClayP
26b8a616be Update AboutActivity.kt
Removed old SDK Check
2025-07-09 10:19:54 -04:00
Dev-ClayP
d88882f439 Removed PRNGFixes App.kt 2025-07-09 10:15:36 -04:00
Dev-ClayP
09dc1d6baa Removed sdk checks on TextFieldView.kt
Removed SDK checks that will always resolve to true now. Since we are updating min sdk to 19, these checks are no longer necessary.
2025-07-09 09:57:23 -04:00
Clay Perry
f4f5e86979 Updated Minimum SDK to 16 2025-07-08 12:52:14 -04:00
J-Jamet
488fd60d5d fix: Better handleCreatePasskeyQuery implementation 2025-07-08 17:23:20 +02:00
J-Jamet
41025f64c0 fix: Add fennec_fdroid according to https://github.com/Kunzisoft/KeePassDX/issues/1421#issuecomment-2872838246 2025-07-08 16:20:17 +02:00
J-Jamet
a2eac2ff76 fix: KeePassDX name 2025-07-08 15:44:57 +02:00
J-Jamet
34f2a2391a Merge branch 'master' into feature/Passkeys 2025-07-08 15:23:27 +02:00
Stephan Paternotte
a41afb7f1e Translated using Weblate (Dutch)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/nl/
2025-07-07 15:02:17 +02:00
Stephan Paternotte
32d9cfbe29 Translated using Weblate (Dutch)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/nl/
2025-07-07 15:02:15 +02:00
Masowick
7210652567 Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-07-07 15:02:13 +02:00
VfBFan
ab15967ad7 Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/de/
2025-07-07 15:02:11 +02:00
Stephan Paternotte
44df4ec181 Added translation using Weblate (Dutch) 2025-07-06 14:29:32 +02:00
Fjuro
7afe356082 Translated using Weblate (Czech)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/cs/
2025-07-06 14:29:32 +02:00
Fjuro
87597553b8 Translated using Weblate (Czech)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/cs/
2025-07-06 14:29:32 +02:00
Fjuro
27e5f58d5e Added translation using Weblate (Czech) 2025-07-05 23:12:59 +02:00
jonnysemon
762c946d35 Translated using Weblate (Arabic)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-07-05 23:12:58 +02:00
jonnysemon
21a927e3e9 Translated using Weblate (Arabic)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-07-05 23:12:58 +02:00
jonnysemon
f93bb7436a Translated using Weblate (Arabic)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ar/
2025-07-05 23:12:58 +02:00
jonnysemon
6294fddbba Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-07-05 23:12:58 +02:00
jonnysemon
c5719dfaf2 Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-07-05 23:12:58 +02:00
Matthaiks
673fd67f15 Translated using Weblate (Polish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-07-05 23:12:58 +02:00
Fjuro
25524c48e9 Translated using Weblate (Czech)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-07-05 23:12:58 +02:00
Fjuro
631b924c33 Translated using Weblate (Czech)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2025-07-05 23:12:58 +02:00
J-Jamet
fba12bc278 Merge tag '4.1.2' into develop
4.1.2
2025-07-05 17:30:44 +02:00
J-Jamet
e809109bb2 Merge branch 'release/4.1.2' 2025-07-05 17:30:06 +02:00
J-Jamet
0e31890624 fix: Compilation with fastlane 2025-07-05 17:11:17 +02:00
J-Jamet
0124021ce5 fix: Upgrade README.md 2025-07-05 14:18:27 +02:00
J-Jamet
74db6bf77f fix: Update CHANGE 2025-07-05 14:09:07 +02:00
J-Jamet
efde33182e fix: Digit count UI #1987 2025-07-05 13:55:06 +02:00
J-Jamet
ec68b22330 fix: Update CHANGELOG 2025-07-05 11:55:24 +02:00
Tomás
bf7e014f8c Translated using Weblate (Spanish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/es/
2025-07-05 11:40:35 +02:00
Tomás
40e1607698 Translated using Weblate (Spanish)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/es/
2025-07-05 11:40:35 +02:00
Matthaiks
4a132f06fe Translated using Weblate (Polish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-07-05 11:40:35 +02:00
Tomás
0396dd975d Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-07-05 02:47:45 +02:00
J-Jamet
80a38c0c54 fix: Update CHANGELOG 2025-07-04 23:05:17 +02:00
J-Jamet
2aa6461094 Merge branch 'Dev-ClayP-master' into develop 2025-07-04 22:56:37 +02:00
Dev-ClayP
258433b3b8 Update SetOTPDialogFragment.kt
Tiny Whitespace fix
2025-07-04 10:53:19 -04:00
Clay Perry
79e723545c Expanded minimum secret length functionality so that the user is alerted if the length is not long enough. This fixed an issue where the user wasnt alerted about invalid secret length but was still able to click ok and submit the string, bypassing the base32 check altogether. 2025-07-04 10:50:25 -04:00
Yurt Page
a6b20455ef Translated using Weblate (Russian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-07-04 16:00:48 +02:00
Dev-ClayP
9659b55bf3 Update SetOTPDialogFragment.kt
Fixed UX bug for button in the way,
Added string length check.
2025-07-04 01:21:47 -04:00
Bora Atıcı
ca148ef546 Translated using Weblate (Turkish)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/tr/
2025-07-02 19:01:48 +02:00
J-Jamet
322b21d645 fix: Update Gradle to 8.11.0 2025-07-01 20:40:42 +02:00
J-Jamet
ed2ba65ecf fix: Update CHANGELOG 2025-07-01 20:26:48 +02:00
J-Jamet
defc8b1c57 fix: Update CHANGELOG 2025-07-01 20:25:34 +02:00
J-Jamet
a90ecc56d8 Merge branch 'develop' into codokie-rtl 2025-07-01 20:16:24 +02:00
J-Jamet
2faa0ac320 fix: Note group modification #2053 2025-07-01 20:07:19 +02:00
J-Jamet
e391fd59fe fix: Autofill registration popup #2054 An CHANGELOG 2025-07-01 19:45:26 +02:00
Priit Jõerüüt
25df86606c Translated using Weblate (Estonian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/et/
2025-07-01 18:31:43 +02:00
Priit Jõerüüt
811f33eb3f Translated using Weblate (Estonian)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/et/
2025-07-01 18:31:43 +02:00
solokot
ca7e2ed89d Translated using Weblate (Russian)
Currently translated at 66.6% (2 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ru/
2025-07-01 18:31:43 +02:00
solokot
6f4cd79e2c Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-07-01 18:31:43 +02:00
solokot
da1caf4b8b Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-07-01 18:31:43 +02:00
Stephan Paternotte
4a0a8e44ca Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-07-01 18:31:43 +02:00
Stephan Paternotte
6bc2c3481b Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-07-01 18:31:43 +02:00
J-Jamet
0aa89ea9ff fix: Same domain unit tests 2025-07-01 17:26:56 +02:00
J-Jamet
f31a30bf47 fix: Search #1946 #2003 2025-07-01 17:22:42 +02:00
Liner Seven
dc75837ac7 Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ja/
2025-07-01 09:22:50 +02:00
Liner Seven
9849b0a1da Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/ja/
2025-07-01 09:22:50 +02:00
Priit Jõerüüt
2c15a1ddd6 Translated using Weblate (Estonian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-07-01 09:22:50 +02:00
Priit Jõerüüt
98eb9976cf Translated using Weblate (Estonian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-07-01 09:22:50 +02:00
Keterion
0d9a5810b1 Translated using Weblate (Filipino)
Currently translated at 46.2% (308 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2025-07-01 09:22:50 +02:00
Besnik Bleta
1adaa137a5 Translated using Weblate (Albanian)
Currently translated at 96.5% (643 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2025-07-01 09:22:50 +02:00
bowornsin
44a428d15a Translated using Weblate (Thai)
Currently translated at 89.3% (595 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2025-07-01 09:22:50 +02:00
Suman Garai
5416a7942a Translated using Weblate (Bengali)
Currently translated at 52.5% (350 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn/
2025-07-01 09:22:49 +02:00
Keterion
9e0024baf5 Translated using Weblate (English (United Kingdom))
Currently translated at 33.9% (226 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2025-07-01 09:22:49 +02:00
Keterion
8d47ce38c2 Translated using Weblate (Vietnamese)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2025-07-01 09:22:49 +02:00
109247019824
80af43c0ca Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-07-01 09:22:49 +02:00
109247019824
225f8243c2 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-07-01 09:22:49 +02:00
தமிழ்நேரம்
68bc118add Translated using Weblate (Tamil)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2025-07-01 09:22:49 +02:00
தமிழ்நேரம்
abbc584402 Translated using Weblate (Tamil)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2025-07-01 09:22:49 +02:00
தமிழ்நேரம்
6635594639 Translated using Weblate (Tamil)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2025-07-01 09:22:49 +02:00
தமிழ்நேரம்
10db77d402 Translated using Weblate (Tamil)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2025-07-01 09:22:49 +02:00
Allan Nordhøy
000fd7e520 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.4% (656 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2025-07-01 09:22:48 +02:00
Keterion
c8ced4ae59 Translated using Weblate (Galician)
Currently translated at 87.0% (580 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2025-07-01 09:22:48 +02:00
hugoalh
9209ca9af7 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2025-07-01 09:22:48 +02:00
大王叫我来巡山
c7bd90c610 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2025-07-01 09:22:47 +02:00
aasami
fceb9c3547 Translated using Weblate (Slovak)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2025-07-01 09:22:47 +02:00
Ash Ed
030c49b571 Translated using Weblate (Russian)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-07-01 09:22:47 +02:00
Stephan Paternotte
f2d6a6a536 Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-07-01 09:22:47 +02:00
Liner Seven
8f61521f05 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-07-01 09:22:47 +02:00
cc5efd7b0
89af7ec5d0 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-07-01 09:22:47 +02:00
Ghost of Sparta
362f1aebed Translated using Weblate (Hungarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-07-01 09:22:46 +02:00
summoner001
5226527cec Translated using Weblate (Hungarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2025-07-01 09:22:46 +02:00
Aitor Elorza
b8464cd0e5 Translated using Weblate (Basque)
Currently translated at 95.6% (637 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2025-07-01 09:22:46 +02:00
Óscar Fernández Díaz
46e7b04d66 Translated using Weblate (Spanish)
Currently translated at 99.6% (664 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-07-01 09:22:46 +02:00
VfBFan
73111b770f Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-07-01 09:22:46 +02:00
Keterion
995d485700 Translated using Weblate (Danish)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2025-07-01 09:22:46 +02:00
pitroig
5ebbbef667 Translated using Weblate (Catalan)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2025-07-01 09:22:45 +02:00
J-Jamet
c79144400f Merge branch 'translations' into develop 2025-06-30 17:17:13 +02:00
J-Jamet
b56556f5a2 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2025-06-30 17:11:30 +02:00
J-Jamet
35d5f01b8e Merge branch 'fuzzpart-Error-strings-mismatch-#1938' into develop 2025-06-30 15:02:44 +02:00
J-Jamet
501c647236 fix: Warnings 2025-06-05 19:47:35 +02:00
J-Jamet
e77c7b84a3 fix: Upgrade gradle 2025-06-05 19:38:32 +02:00
J-Jamet
d9f4e9b6ab fix: Upgrade JDK and Gradle 2025-06-05 19:27:41 +02:00
Kunzisoft
1d7f7d2a5b Translated using Weblate (French)
Currently translated at 100.0% (3 of 3 strings)

Translation: KeePassDX/Metadata
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/metadata/fr/
2025-05-29 10:27:20 +02:00
Yurt Page
df408e862b Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-05-29 10:24:23 +02:00
Yurt Page
66845926d5 Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-05-28 21:30:16 +02:00
J-Jamet
0a06acbf1d Merge branch 'ymcx-master' into develop 2025-05-28 21:13:50 +02:00
J-Jamet
5d3c6798c0 Merge branch 'codokie-dialog' into develop 2025-05-28 21:04:16 +02:00
solokot
b9d2b9ddc9 Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-05-22 07:01:52 +00:00
abdelbasset jabrane
8a6525f45e Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2025-05-19 01:32:49 +02:00
Priit Jõerüüt
41d89b590d Translated using Weblate (Estonian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-05-14 17:03:07 +02:00
Ash Ed
8354d08ff5 Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2025-05-10 18:03:02 +02:00
fuzzpart
16725b21f3 All error messages were ended with a full stop. The respective punctuation marks have been used in translations. 2025-05-08 10:31:39 +02:00
ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝)
93ba17792f Translated using Weblate (Latvian)
Currently translated at 22.3% (149 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lv/
2025-05-08 07:03:01 +02:00
Priit Jõerüüt
940b96cf21 Translated using Weblate (Estonian)
Currently translated at 94.4% (629 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-04-29 09:03:25 +02:00
Ihor Hordiichuk
157253ce24 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-04-21 23:01:42 +02:00
ssantos
46b3810f34 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2025-04-20 00:01:42 +02:00
VfBFan
05c030dbbb Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-04-18 08:26:32 +02:00
aasami
5b0fd99351 Translated using Weblate (Slovak)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2025-04-15 00:44:02 +02:00
Xo
2946a6e231 Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-04-07 09:03:37 +00:00
codokie
47896fcdc9 [RTL] Fix text direction of OTP token 2025-04-06 14:32:03 +03:00
codokie
bc51345f0d [RTL] Workaround for license text direction 2025-04-06 10:54:57 +03:00
codokie
750e1b6c43 [RTL] Force LTR text direction for protected fields 2025-04-06 10:44:36 +03:00
codokie
4a2106837c [RTL] Align text to the start of the view 2025-04-06 10:44:10 +03:00
codokie
39b9fc350a [RTL] Fix direction of arrow icons 2025-04-05 23:23:07 +03:00
codokie
53560dbe29 [RTL] Fix order of duration pickers 2025-04-05 23:22:52 +03:00
codokie
5172dbe114 [RTL] Fix padding of toolbar buttons 2025-04-05 23:22:25 +03:00
codokie
4f0ff67fdf [RTL] Fix DX logo 2025-04-05 23:20:19 +03:00
codokie
bac971ced8 [RTL] Enable support 2025-04-05 23:20:00 +03:00
Priit Jõerüüt
64bf5ba165 Translated using Weblate (Estonian)
Currently translated at 90.2% (601 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-03-27 20:25:08 +01:00
Tino
90c4a5e1b8 Fix OTP code alignment in the main screen 2025-03-19 14:26:00 +02:00
Xo
b08a5d9cda Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-15 18:52:30 +01:00
Alonso González Chaves
d5be79948d Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-03-14 14:06:23 +01:00
Alonso González Chaves
6b64be4925 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-03-14 13:07:43 +01:00
Alonso González Chaves
e8e6eb6ca5 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-03-14 09:49:22 +01:00
codokie
ee9383dd0b [Dialog] Fix EditText context menu background 2025-03-12 14:50:33 +00:00
Xo
aee0b82cff Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-12 13:25:53 +01:00
Xo
ba5913da57 Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-11 14:27:29 +01:00
Xo
3238b9b2ce Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-10 12:07:14 +01:00
Xo
5215181c0f Translated using Weblate (Hebrew)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-10 09:56:08 +01:00
Xo
d3676a1454 Translated using Weblate (Hebrew)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-10 00:21:10 +01:00
Xo
792ce6f86e Translated using Weblate (Hebrew)
Currently translated at 99.0% (660 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-09 19:02:23 +01:00
Xo
ddbd0376fc Translated using Weblate (Hebrew)
Currently translated at 99.2% (661 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-09 15:35:57 +01:00
Xo
496655093c Translated using Weblate (Hebrew)
Currently translated at 99.2% (661 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-03-09 13:24:53 +01:00
109247019824
755293eff7 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2025-03-06 21:14:00 +01:00
Priit Jõerüüt
b6e51a1f32 Translated using Weblate (Estonian)
Currently translated at 89.0% (593 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-03-05 13:20:43 +01:00
Priit Jõerüüt
08f17f3f19 Translated using Weblate (Estonian)
Currently translated at 83.9% (559 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-03-05 11:20:13 +01:00
pitroig
14440725fc Translated using Weblate (Catalan)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2025-03-02 22:01:57 +01:00
pitroig
b4f84c5cd6 Translated using Weblate (Catalan)
Currently translated at 97.4% (649 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2025-03-02 11:57:49 +01:00
Xo
ad6b1cead1 Translated using Weblate (Hebrew)
Currently translated at 93.3% (622 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-02-25 18:07:20 +01:00
Priit Jõerüüt
e06398ff19 Translated using Weblate (Estonian)
Currently translated at 78.6% (524 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-02-25 11:01:38 +01:00
신태진
919ad5cfd4 Translated using Weblate (Korean)
Currently translated at 50.1% (334 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2025-02-24 16:49:09 +01:00
신태진
b3fb721588 Translated using Weblate (Korean)
Currently translated at 47.5% (317 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2025-02-24 15:34:19 +01:00
Xo
590497852d Translated using Weblate (Hebrew)
Currently translated at 92.7% (618 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2025-02-17 23:57:45 +01:00
Bora Atıcı
ebd7a9c7cf Translated using Weblate (Turkish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-02-15 13:56:30 +01:00
SHINJI.K
939fb2fa54 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-02-13 13:02:12 +00:00
cc5efd7b0
2245daffe9 Translated using Weblate (Japanese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-02-13 13:02:11 +00:00
Priit Jõerüüt
784b25ada8 Translated using Weblate (Estonian)
Currently translated at 70.5% (470 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-02-11 11:02:10 +01:00
Milo Ivir
146d631794 Translated using Weblate (Croatian)
Currently translated at 99.5% (663 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2025-02-08 10:54:21 +01:00
Priit Jõerüüt
cc062d7f0e Translated using Weblate (Estonian)
Currently translated at 64.4% (429 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-02-06 23:02:18 +00:00
Francisco Serrador
298dd4af61 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-02-02 23:32:33 +01:00
NicolaeFericitu
739ba3b14d Translated using Weblate (Romanian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2025-01-22 21:44:57 +01:00
Priit Jõerüüt
2864ea9868 Translated using Weblate (Estonian)
Currently translated at 62.7% (418 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2025-01-21 18:12:06 +01:00
efelantti
ed8d3247ca Translated using Weblate (Finnish)
Currently translated at 59.0% (393 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2025-01-20 16:15:45 +01:00
efelantti
ff5de7b327 Translated using Weblate (Finnish)
Currently translated at 53.1% (354 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2025-01-19 13:00:30 +01:00
Francisco Serrador
aa77552ff4 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2025-01-19 13:00:29 +01:00
efelantti
12456c0ea2 Translated using Weblate (Finnish)
Currently translated at 43.3% (289 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2025-01-18 10:47:03 +01:00
Stefan Hauser
a7a93fa2a2 Use local date/time in filename for saving a copy of the database 2025-01-16 09:15:32 +01:00
Matthaiks
ff7cd29b77 Translated using Weblate (Polish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2025-01-14 11:05:42 +01:00
Ihor Hordiichuk
f7065acc40 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2025-01-13 04:30:56 +01:00
Bora Atıcı
e4e2e5c43c Translated using Weblate (Turkish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2025-01-11 22:42:13 +01:00
Stephan Paternotte
48d240b010 Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-01-09 22:43:59 +01:00
Stephan Paternotte
9f2deb56b9 Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2025-01-09 09:01:50 +01:00
Masowick
951257bed8 Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2025-01-09 09:01:50 +01:00
தமிழ்நேரம்
f27ce804fb Translated using Weblate (Tamil)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ta/
2025-01-05 18:01:22 +00:00
FX8350
1d896e83b3 Translated using Weblate (Japanese)
Currently translated at 96.8% (645 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2025-01-05 18:01:19 +00:00
Laurent FAVOLE
a2c2925610 Translated using Weblate (French)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2025-01-01 18:00:34 +01:00
hugoalh
97e5f90603 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-12-27 09:00:25 +01:00
qx100
593b7188dc Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-12-27 09:00:24 +01:00
bowornsin
4c4e61a711 Translated using Weblate (Thai)
Currently translated at 89.9% (599 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-12-24 02:07:49 +01:00
Besnik Bleta
140f09c77e Translated using Weblate (Albanian)
Currently translated at 97.1% (647 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-12-17 12:00:23 +01:00
Masowick
c0fe4faf8a Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-12-17 12:00:22 +01:00
Pixelcode
066fff7aca Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-12-15 15:02:18 +01:00
Masowick
6dcbdffed4 Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-12-15 15:02:16 +01:00
Priit Jõerüüt
3a07dea6d7 Translated using Weblate (Estonian)
Currently translated at 57.9% (386 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-12-14 07:02:47 +01:00
Sandyran
72ed07ef17 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 42.6% (284 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nn/
2024-12-14 07:02:46 +01:00
Sandyran
51512b4588 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 20.5% (137 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nn/
2024-12-10 22:00:22 +00:00
J-Jamet
aed49e19e8 fix: view null exception 2024-12-08 17:07:46 +01:00
J-Jamet
113601d09a fix: Remove unused tests 2024-12-08 17:07:05 +01:00
J-Jamet
b7d0b65715 fix: Add linebreak in description 2024-12-08 15:55:37 +01:00
Priit Jõerüüt
3075591885 Translated using Weblate (Estonian)
Currently translated at 57.9% (386 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-12-06 08:00:33 +01:00
Masowick
e9596f56db Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-12-06 08:00:31 +01:00
Priit Jõerüüt
3559830738 Translated using Weblate (Estonian)
Currently translated at 55.7% (371 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-12-04 16:06:01 +01:00
jonnysemon
e42beccb22 Translated using Weblate (Arabic)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-11-29 15:00:34 +01:00
Priit Jõerüüt
d05b7394e8 Translated using Weblate (Estonian)
Currently translated at 41.8% (279 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-11-27 21:00:44 +01:00
Besnik Bleta
00b11ea659 Translated using Weblate (Albanian)
Currently translated at 96.2% (641 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-11-27 21:00:44 +01:00
Cleverson Cândido
99f0f096d1 Translated using Weblate (Portuguese)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2024-11-27 21:00:43 +01:00
Priit Jõerüüt
416329d50d Translated using Weblate (Estonian)
Currently translated at 35.4% (236 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-11-26 19:05:32 +00:00
Besnik Bleta
cb0d1b05d7 Translated using Weblate (Albanian)
Currently translated at 94.4% (629 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-11-26 19:05:31 +00:00
Oliver Cervera
a2845c33f8 Translated using Weblate (Italian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-11-26 19:05:30 +00:00
Besnik Bleta
6d06265d94 Translated using Weblate (Albanian)
Currently translated at 88.5% (590 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-11-25 17:18:05 +01:00
109247019824
49d03efe56 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-11-25 17:18:04 +01:00
Wellington Terumi Uemura
7f13a3ca76 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2024-11-25 17:18:04 +01:00
Retrial
cec9d168e3 Translated using Weblate (Greek)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2024-11-25 17:18:04 +01:00
VfBFan
ecf253451d Translated using Weblate (German)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-11-25 17:18:04 +01:00
J-Jamet
89a6219659 fix: Remove unused tests 2024-11-25 14:23:35 +01:00
J-Jamet
9dec308caa fix: URL matching fails in presence of a path #1940 2024-11-25 14:21:39 +01:00
J-Jamet
fb1459de9b fix: Upgrade to 4.1.2 2024-11-25 14:18:18 +01:00
109247019824
f3bef64461 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-11-25 10:53:54 +01:00
Linerly
2509caff6b Translated using Weblate (Indonesian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2024-11-25 10:53:54 +01:00
hugoalh
232aafe2c0 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 99.3% (662 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-11-25 10:53:54 +01:00
大王叫我来巡山
ed26cb4891 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-11-25 10:53:54 +01:00
solokot
0b966a6cd1 Translated using Weblate (Russian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-11-25 10:53:54 +01:00
Stephan Paternotte
c84afd2281 Translated using Weblate (Dutch)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-11-25 10:53:54 +01:00
summoner001
1711f09547 Translated using Weblate (Hungarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-11-25 10:53:54 +01:00
Masowick
4460a44e99 Translated using Weblate (German)
Currently translated at 99.6% (664 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-11-25 10:53:54 +01:00
Fjuro
c8f7bcfb52 Translated using Weblate (Czech)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2024-11-25 10:53:53 +01:00
J-Jamet
bd2bd842af Merge tag '4.1.1' into develop
4.1.1
2024-11-24 15:46:07 +01:00
J-Jamet
2b0f4fe46b Merge branch 'release/4.1.1' 2024-11-24 15:46:00 +01:00
J-Jamet
55c2f41c71 fix: tags 2024-11-24 14:59:06 +01:00
109247019824
94985422ca Translated using Weblate (Bulgarian)
Currently translated at 99.0% (660 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-11-24 14:27:14 +01:00
Matthaiks
eaff1aa58f Translated using Weblate (Polish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-11-24 14:27:14 +01:00
Ghost of Sparta
c7cb9d0990 Translated using Weblate (Hungarian)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-11-24 14:27:14 +01:00
gallegonovato
a9fdf30421 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-11-24 14:27:13 +01:00
Matthaiks
d0f45f6dfb Translated using Weblate (Polish)
Currently translated at 99.3% (662 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-11-24 12:07:11 +01:00
Hosted Weblate
8cdb6a3c9f Merge branch 'origin/develop' into Weblate. 2024-11-24 12:06:37 +01:00
Kunzisoft
b369a46431 Translated using Weblate (French)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-11-24 12:06:37 +01:00
gallegonovato
f40ca2d5e0 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-11-24 12:06:37 +01:00
Anonymous
400393c677 Translated using Weblate (Azerbaijani)
Currently translated at 97.1% (647 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-11-24 12:05:41 +01:00
Anonymous
5710e3be55 Translated using Weblate (Serbian)
Currently translated at 50.4% (336 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr/
2024-11-24 12:05:41 +01:00
Anonymous
af1312a92b Translated using Weblate (Estonian)
Currently translated at 32.4% (216 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-11-24 12:05:40 +01:00
Anonymous
d2a63c48b1 Translated using Weblate (Filipino)
Currently translated at 46.2% (308 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2024-11-24 12:05:40 +01:00
Anonymous
8f10ea7ed6 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 20.1% (134 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/yue_Hant/
2024-11-24 12:05:40 +01:00
Anonymous
7bd701368a Translated using Weblate (Thai)
Currently translated at 89.3% (595 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-11-24 12:05:40 +01:00
Anonymous
894e846a62 Translated using Weblate (English (United Kingdom))
Currently translated at 33.9% (226 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2024-11-24 12:05:40 +01:00
Anonymous
504ef5a7ab Translated using Weblate (Vietnamese)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2024-11-24 12:05:40 +01:00
Anonymous
af6436da77 Translated using Weblate (Bulgarian)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-11-24 12:05:40 +01:00
Anonymous
d031420ed3 Translated using Weblate (Portuguese)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2024-11-24 12:05:40 +01:00
Anonymous
4b6b7478de Translated using Weblate (Indonesian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2024-11-24 12:05:40 +01:00
Anonymous
bb1b5eab96 Translated using Weblate (Romanian)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-11-24 12:05:40 +01:00
Anonymous
9486b50342 Translated using Weblate (Croatian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2024-11-24 12:05:39 +01:00
Anonymous
fc9a5b3545 Translated using Weblate (Turkish)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-11-24 12:05:39 +01:00
Anonymous
8b4d0e2541 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2024-11-24 12:05:39 +01:00
Anonymous
0ef07f615c Translated using Weblate (Galician)
Currently translated at 86.7% (578 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2024-11-24 12:05:39 +01:00
Anonymous
836413cff2 Translated using Weblate (Arabic)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-11-24 12:05:39 +01:00
Anonymous
c71e34fee9 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-11-24 12:05:39 +01:00
Anonymous
b389a4db92 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-11-24 12:05:39 +01:00
Anonymous
0a1a54cb33 Translated using Weblate (Ukrainian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2024-11-24 12:05:39 +01:00
Anonymous
630228675c Translated using Weblate (Swedish)
Currently translated at 62.4% (416 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2024-11-24 12:05:39 +01:00
Anonymous
f7955d00fc Translated using Weblate (Slovak)
Currently translated at 92.0% (613 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2024-11-24 12:05:39 +01:00
Anonymous
68b08f9b9a Translated using Weblate (Russian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-11-24 12:05:39 +01:00
Anonymous
612f136ced Translated using Weblate (Portuguese (Portugal))
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2024-11-24 12:05:39 +01:00
Anonymous
8c313c3d48 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2024-11-24 12:05:38 +01:00
Anonymous
517a6c0062 Translated using Weblate (Polish)
Currently translated at 99.2% (661 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-11-24 12:05:38 +01:00
Matthaiks
46c61b10de Translated using Weblate (Polish)
Currently translated at 99.2% (661 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-11-24 12:05:38 +01:00
Anonymous
edc8d27577 Translated using Weblate (Dutch)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-11-24 12:05:38 +01:00
Anonymous
05354777fe Translated using Weblate (Japanese)
Currently translated at 96.8% (645 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2024-11-24 12:05:38 +01:00
Anonymous
98d3d2a39b Translated using Weblate (Italian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-11-24 12:05:38 +01:00
Anonymous
f91f75912e Translated using Weblate (Hungarian)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-11-24 12:05:38 +01:00
Anonymous
9f21f67035 Translated using Weblate (French)
Currently translated at 99.5% (663 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-11-24 12:05:38 +01:00
Kunzisoft
699e4da112 Translated using Weblate (French)
Currently translated at 99.5% (663 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-11-24 12:05:38 +01:00
Anonymous
6458285e75 Translated using Weblate (Basque)
Currently translated at 95.7% (638 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2024-11-24 12:05:38 +01:00
Anonymous
e0ddf3711f Translated using Weblate (Spanish)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-11-24 12:05:38 +01:00
gallegonovato
68d415375d Translated using Weblate (Spanish)
Currently translated at 99.8% (665 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-11-24 12:05:38 +01:00
Anonymous
0e11afdd8b Translated using Weblate (Greek)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2024-11-24 12:05:37 +01:00
Anonymous
ff2c01584f Translated using Weblate (German)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-11-24 12:05:37 +01:00
Anonymous
a59e20b864 Translated using Weblate (Danish)
Currently translated at 98.6% (657 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2024-11-24 12:05:37 +01:00
Anonymous
d7d898896d Translated using Weblate (Czech)
Currently translated at 98.7% (658 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2024-11-24 12:05:37 +01:00
Anonymous
2dde78d5e7 Translated using Weblate (Catalan)
Currently translated at 94.8% (632 of 666 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2024-11-24 12:05:37 +01:00
J-Jamet
2ca39ff399 fix: update string 2024-11-24 12:04:36 +01:00
Hosted Weblate
1f6fdaf9b3 Merge branch 'origin/develop' into Weblate. 2024-11-24 11:58:45 +01:00
J-Jamet
edaf58135f fix: Upgrade to 4.1.1 2024-11-24 11:56:41 +01:00
J-Jamet
724698fc51 fix: Date parser #1933 2024-11-24 11:52:13 +01:00
summoner001
6aabe9e12c Translated using Weblate (Hungarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-11-23 19:39:32 +01:00
bowornsin
2b5a1bb893 Translated using Weblate (Thai)
Currently translated at 90.2% (595 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-11-23 04:00:27 +00:00
summoner001
7403305f3c Translated using Weblate (Hungarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-11-23 04:00:26 +00:00
J-Jamet
e780f8a3f0 fix: Matching against domain goes too far #1820 2024-11-19 21:48:16 +01:00
J-Jamet
67b09014aa Merge branch 'develop' into feature/Passkeys 2024-11-18 16:40:46 +01:00
J-Jamet
7f2fda0327 Merge tag '4.1.0' into develop
4.1.0
2024-11-18 11:54:37 +01:00
J-Jamet
ada8f74e2c Merge branch 'release/4.1.0' 2024-11-18 11:54:27 +01:00
J-Jamet
1ddfeaf950 fix: update Gemfile 2024-11-18 11:07:14 +01:00
J-Jamet
2ed2cc1499 fix: Replace strong tag 2024-11-18 11:00:46 +01:00
J-Jamet
be0f90f12a Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2024-11-18 10:58:28 +01:00
J-Jamet
4b362df23b Merge branch 'master' into develop 2024-11-17 20:32:20 +01:00
J-Jamet
b953a1c2f6 fix: Package authenticity 2024-11-17 20:29:45 +01:00
J-Jamet
4c30fa43d3 fix: Screenshot protection #1870 2024-11-17 19:32:23 +01:00
J-Jamet
6866b1a3bb fix: change expires icon 2024-11-17 14:59:07 +01:00
J-Jamet
328030f152 fix: timer precision #1467 2024-11-17 14:46:16 +01:00
J-Jamet
fd195bd926 fix: reset timer only if in DAO 2024-11-17 13:52:58 +01:00
J-Jamet
87e07366cd Resets the advanced unlock expiration #1600 2024-11-17 13:31:47 +01:00
J-Jamet
8133977e09 feat: Add shield icon as password strength indicator #1355 2024-11-16 12:56:46 +01:00
J-Jamet
11199b996c fix: Password color and entropy view 2024-11-15 19:25:00 +01:00
J-Jamet
eee61db189 Add entropy 2024-11-15 17:45:09 +01:00
J-Jamet
c7c5130030 fix: passkeyview to passwordview 2024-11-14 19:57:35 +01:00
J-Jamet
6de14a3840 fix: TimeZone 2024-11-13 21:13:50 +01:00
J-Jamet
55206b3dde fix: All past dates and times are crossed out #1710 2024-11-13 20:54:31 +01:00
Priit Jõerüüt
e1a44477af Translated using Weblate (Estonian)
Currently translated at 32.9% (217 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-11-12 18:00:27 +01:00
Aitor Elorza
23afee453e Translated using Weblate (Basque)
Currently translated at 96.8% (638 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2024-11-12 18:00:26 +01:00
J-Jamet
8e05309021 feat: Support otpauth://steam/Steam link #1289 2024-11-11 16:06:48 +01:00
J-Jamet
26fdf87070 fix: distinct domain names #1820 2024-11-11 15:34:48 +01:00
J-Jamet
1c95a0edc4 fix: distinct domain names #1105 2024-11-08 21:11:26 +01:00
J-Jamet
4723fb39e9 fix: Sort 2024-11-08 13:27:26 +01:00
Jamil Farajov
13d667d81c Translated using Weblate (Azerbaijani)
Currently translated at 98.6% (650 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-11-07 12:00:40 +00:00
Jamil Farajov
dce255dc58 Translated using Weblate (Azerbaijani)
Currently translated at 91.9% (606 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-11-06 12:00:23 +01:00
Random
f72c9704d9 Translated using Weblate (Italian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-11-06 12:00:21 +01:00
Sylvain Pichon
e623010e91 Translated using Weblate (French)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-11-05 07:00:22 +00:00
J-Jamet
587bfdc162 fix: Month save 2024-11-04 20:02:19 +01:00
J-Jamet
7c1c299282 fix: Off-by-one when selecting date fields #1695 2024-11-04 19:43:30 +01:00
Priit Jõerüüt
f2a5c0b04b Translated using Weblate (Estonian)
Currently translated at 32.6% (215 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-11-03 23:00:16 +01:00
J-Jamet
4ba77b76ec feat: Instant in copy #1889 2024-11-02 13:37:33 +01:00
J-Jamet
0bfce44317 fix: unlocking procedure #1760 2024-11-02 11:04:15 +01:00
solokot
13b6d6384c Translated using Weblate (Russian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-10-29 00:12:52 +01:00
ginger-co
80838bbef0 Translated using Weblate (Hebrew)
Currently translated at 92.5% (610 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-10-27 15:00:19 +01:00
Avi Parshan
8a557ff2fb Translated using Weblate (Hebrew)
Currently translated at 81.0% (534 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-10-26 15:45:55 +02:00
ginger-co
16e394087d Translated using Weblate (Hebrew)
Currently translated at 81.0% (534 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-10-26 15:45:55 +02:00
Avi Parshan
ee1b67b36e Translated using Weblate (Hebrew)
Currently translated at 70.4% (464 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-10-26 15:04:50 +02:00
ginger-co
36b9fa2387 Translated using Weblate (Hebrew)
Currently translated at 70.4% (464 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-10-26 15:04:50 +02:00
Jérémy JAMET
378169e939 Update README.md, remove the keytool section 2024-10-24 11:50:33 +02:00
Jamil Farajov
70a01e559d Translated using Weblate (Azerbaijani)
Currently translated at 36.8% (243 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-22 17:16:33 +02:00
shinebrillant
c09e8196f3 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-10-22 17:16:32 +02:00
J-Jamet
0382c05152 fix: Filters 2024-10-21 19:54:47 +02:00
Celeste Liu
75cf6e2a56 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-10-21 00:20:33 +02:00
J-Jamet
9fb4754430 feat: Number of children #421 2024-10-20 21:15:21 +02:00
J-Jamet
0312b504a9 fix: upgrade CHANGELOG 2024-10-20 18:51:28 +02:00
J-Jamet
1d6a9651bf fix: upgrade to API34 #1894 2024-10-20 18:44:06 +02:00
Jamil Farajov
e36b18e85e Translated using Weblate (Azerbaijani)
Currently translated at 26.5% (175 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-18 00:23:21 +02:00
Roko Magdalenić
4ae4951e0d Translated using Weblate (Serbian)
Currently translated at 51.1% (337 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr/
2024-10-16 23:15:48 +02:00
Roko Magdalenić
a65f52ffba Translated using Weblate (Serbian (Latin script))
Currently translated at 59.7% (394 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr_Latn/
2024-10-16 23:15:47 +02:00
Linerly
6de25ffa65 Translated using Weblate (Indonesian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2024-10-15 04:15:44 +02:00
Francisco Serrador
d8429bdd99 Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-10-13 22:16:18 +00:00
cali
c907750446 implement creation and update of passkeys 2024-10-13 20:37:46 +02:00
Jamil Farajov
26976ae6cf Translated using Weblate (Azerbaijani)
Currently translated at 22.7% (150 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-09 20:12:00 +02:00
Jamil Farajov
53beaca563 Translated using Weblate (Azerbaijani)
Currently translated at 18.6% (123 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-06 23:53:53 +02:00
Jamil Farajov
77628e2fb9 Translated using Weblate (Azerbaijani)
Currently translated at 18.2% (120 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-06 16:35:40 +02:00
Jamil Farajov
40d2f2de96 Translated using Weblate (Azerbaijani)
Currently translated at 18.0% (119 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-06 05:40:45 +02:00
Jamil Farajov
84cdb2483f Translated using Weblate (Azerbaijani)
Currently translated at 15.9% (105 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-06 05:04:26 +02:00
Jamil Farajov
c7866bfbbf Translated using Weblate (Azerbaijani)
Currently translated at 12.8% (85 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-05 23:50:54 +02:00
Jamil Farajov
5de1d6b343 Translated using Weblate (Azerbaijani)
Currently translated at 10.7% (71 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/az/
2024-10-05 20:04:36 +02:00
Jamil Farajov
7bde363704 Added translation using Weblate (Azerbaijani) 2024-10-05 17:03:03 +02:00
Priit Jõerüüt
1f9b2ce7b9 Translated using Weblate (Estonian)
Currently translated at 25.3% (167 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-10-04 00:41:37 +02:00
Francisco Serrador
ac2e47776a Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-09-25 01:15:42 +02:00
Ghost of Sparta
e7de5ca263 Translated using Weblate (Hungarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-09-24 00:12:32 +02:00
Francisco Serrador
f7f079e653 Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-09-24 00:12:32 +02:00
Ghost of Sparta
f7cccb33de Translated using Weblate (Hungarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-09-22 19:40:47 +02:00
cali
69114c3cc0 first version credential provider 2024-09-08 18:49:27 +02:00
J-Jamet
8177c9c34b fix: textColorHighlight #1711 2024-09-06 18:13:12 +02:00
J-Jamet
850c46f881 feat: Generate keyfile #1290 2024-09-06 17:13:24 +02:00
J-Jamet
ffcfe966d2 fix: Add warning for KeyFile length #1780 2024-09-06 15:55:36 +02:00
J-Jamet
800badd2a4 Merge branch 'shuvashish76-patch-1' into develop 2024-09-06 11:08:16 +02:00
J-Jamet
0a7ffbcc8f fix: Make the apk verification even clearer #1831 2024-09-06 10:31:41 +02:00
J-Jamet
019ec4de9a fix: Avoid DEPENDENCY_INFO_BLOCK 2024-09-06 09:54:35 +02:00
Priit Jõerüüt
78d1e4a12a Translated using Weblate (Estonian)
Currently translated at 24.8% (164 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-09-02 17:09:18 +02:00
SC
6c99fefad0 Translated using Weblate (Portuguese)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2024-08-23 19:09:13 +00:00
SC
24bc1424b9 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2024-08-23 19:09:12 +00:00
Random
94a0e17cfc Translated using Weblate (Italian)
Currently translated at 99.8% (658 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-08-23 19:09:12 +00:00
獅童乱(しどらん)
2f012d8cf2 Translated using Weblate (Japanese)
Currently translated at 98.0% (646 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2024-08-22 11:09:12 +00:00
gallegonovato
75b800054f Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-08-16 15:09:18 +02:00
Renko
d7c7733315 Translated using Weblate (Romanian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-08-15 13:09:11 +02:00
Ihor Hordiichuk
99600ad8d8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2024-08-15 13:09:11 +02:00
Milo Ivir
4a4c7b8b6b Translated using Weblate (Croatian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2024-08-14 12:09:21 +02:00
jonnysemon
8c3267b345 Translated using Weblate (Arabic)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-08-14 12:09:21 +02:00
ΣΤΑΥΡΟΣ ΔΑΛΙΑΚΟΠΟΥΛΟΣ
c0240b047b Translated using Weblate (Greek)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2024-08-14 12:09:20 +02:00
Fjuro
c52957ccfe Translated using Weblate (Czech)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2024-08-14 12:09:18 +02:00
Besnik Bleta
fb3f057adf Translated using Weblate (Albanian)
Currently translated at 54.9% (362 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-08-12 17:09:28 +02:00
109247019824
e333bd08a4 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-08-12 17:09:28 +02:00
Oğuz Ersen
1844a269cb Translated using Weblate (Turkish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-08-12 17:09:27 +02:00
大王叫我来巡山
859882d24f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-08-12 17:09:26 +02:00
solokot
c95543b8b0 Translated using Weblate (Russian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-08-12 17:09:26 +02:00
Wellington Terumi Uemura
d874125dc1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2024-08-12 17:09:25 +02:00
Matthaiks
795cd099f4 Translated using Weblate (Polish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-08-12 17:09:24 +02:00
Alex Bruinsma
66ef6fd9d8 Translated using Weblate (Dutch)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-08-12 17:09:23 +02:00
gallegonovato
3b0655354d Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-08-12 17:09:23 +02:00
Keterion
76acec93fd Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-08-12 17:09:22 +02:00
VfBFan
3b60068369 Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-08-12 17:09:21 +02:00
Keterion
a0fd0a71a2 Translated using Weblate (Catalan)
Currently translated at 96.0% (633 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2024-08-11 16:57:29 +02:00
Keterion
907accbcc9 Translated using Weblate (English)
Currently translated at 99.8% (658 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2024-08-11 16:57:29 +02:00
Alex Bruinsma
ee284abf8d Translated using Weblate (Dutch)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-08-07 16:09:12 +00:00
Hierax Swiftwing
8239275770 Translated using Weblate (Serbian)
Currently translated at 9.1% (60 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr/
2024-08-01 03:09:13 +02:00
Hierax Swiftwing
029485bace Translated using Weblate (Serbian)
Currently translated at 8.6% (57 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr/
2024-07-31 02:09:18 +02:00
Thom
cef9f6ae3b Translated using Weblate (Danish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2024-07-31 02:09:17 +02:00
Hierax Swiftwing
8d88c94956 Added translation using Weblate (Serbian) 2024-07-30 02:03:40 +02:00
Patricio Carrau
692e155117 Translated using Weblate (French)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-07-30 02:03:40 +02:00
The One
9a9410de2b Translated using Weblate (Swedish)
Currently translated at 63.2% (417 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2024-07-29 23:09:12 +02:00
ssantos
a27f1181ea Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2024-07-15 14:09:14 +02:00
searinminecraft
ac65cadb1b Translated using Weblate (Filipino)
Currently translated at 47.0% (310 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2024-07-13 06:09:13 +00:00
Renko
4345e75b20 Translated using Weblate (Romanian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-07-10 16:09:29 +02:00
j
f64d085e7b Translated using Weblate (Lithuanian)
Currently translated at 61.0% (402 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2024-07-10 16:09:28 +02:00
Kemal Karagöz
325b878f0a Translated using Weblate (Turkish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-07-09 10:09:19 +02:00
Salih ARSLAN
330f375a30 Translated using Weblate (Turkish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-07-06 11:49:51 +02:00
Fqwe1
7d6c211de1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2024-07-04 16:09:10 +02:00
Ben Towali
af5b36752c Translated using Weblate (Estonian)
Currently translated at 19.8% (131 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-07-01 22:09:24 +02:00
WebTrans
e0f563befb Translated using Weblate (Hebrew)
Currently translated at 44.3% (292 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2024-07-01 22:09:23 +02:00
Ben Towali
bf1b84dfea Translated using Weblate (Estonian)
Currently translated at 9.2% (61 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-06-30 21:20:00 +02:00
searinminecraft
b3f8ce9c16 Translated using Weblate (Filipino)
Currently translated at 46.4% (306 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2024-06-27 07:14:57 +02:00
Mr-Update
499152d066 Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-06-25 11:09:13 +02:00
VfBFan
c47e7edc9e Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-06-25 11:09:12 +02:00
shuvashish76
afc034b495 Improved IzzyOnDroid shield 2024-06-24 19:00:43 +05:30
J-Jamet
e1d19741af Merge branch 'feature/API34' into develop 2024-06-24 13:26:36 +02:00
J-Jamet
8d2de40df6 fix: Upgrade all modules to API 34 #1730 2024-06-24 13:14:52 +02:00
J-Jamet
f4b7db667f fix: Broadcast Receiver #1730 2024-06-24 12:59:33 +02:00
J-Jamet
4032e52317 fix: Upgrade Foreground service and version to 4.1.0 2024-06-24 12:32:05 +02:00
J-Jamet
f9db4325d8 Merge branch 'master' into develop 2024-06-24 10:37:39 +02:00
Jérémy JAMET
815824f76d Merge pull request #1862 from shuvashish76/patch-1
Add download sources table
2024-06-24 10:23:01 +02:00
Masowick
8534672c33 Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-06-24 10:20:54 +02:00
shuvashish76
b9bb9a166a Update README.md 2024-06-24 13:39:11 +05:30
shuvashish76
185c886472 Add download sources table 2024-06-24 04:52:01 +05:30
Mr-Update
acc565d021 Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-06-23 15:57:04 +02:00
solokot
64db137c6c Translated using Weblate (Russian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-06-21 20:09:59 +02:00
Kirill Isakov
4f2bdeb2c9 Translated using Weblate (Russian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-06-20 19:09:58 +02:00
ngocanhtve
527994084b Translated using Weblate (Vietnamese)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2024-06-19 02:09:25 +00:00
J-Jamet
ffaf5c9475 Merge tag '4.0.8' into develop
4.0.8
2024-06-18 20:19:52 +02:00
J-Jamet
d1e24bfcd8 Merge branch 'release/4.0.8' 2024-06-18 20:19:46 +02:00
J-Jamet
aab8612c63 fix: update fastlane 2024-06-18 20:19:40 +02:00
J-Jamet
79c1e2d21c fix: #1848 #1850 2024-06-18 20:08:54 +02:00
J-Jamet
7ff44f4839 Merge tag '4.0.7' into develop
4.0.7
2024-06-17 23:20:57 +02:00
J-Jamet
2de0272e3e Merge branch 'release/4.0.7' 2024-06-17 23:20:48 +02:00
J-Jamet
1d22fe72d0 fix: ru fastlane 2024-06-17 23:20:30 +02:00
J-Jamet
7e9821551c fix: tags 2024-06-17 22:53:21 +02:00
J-Jamet
8fb3cd71b9 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2024-06-17 22:50:28 +02:00
J-Jamet
b0f6f1f2ba fix: Update CHANGELOG 2024-06-17 19:07:43 +02:00
Jérémy JAMET
df56dc4a4a Merge pull request #1840 from lgjint/fix-inline-suggestion
fix: inline suggestions in keyboard only show ''Select entry''
2024-06-17 19:05:29 +02:00
J-Jamet
b572ec4901 fix: Update CHANGELOG 2024-06-17 12:46:43 +02:00
J-Jamet
75759171c1 fix: Upgrade to 4.0.7 2024-06-17 12:39:47 +02:00
J-Jamet
322fe185dd fix: Description 2024-06-17 12:38:39 +02:00
J-Jamet
effe514ecb fix: Show broken link by default 2024-06-17 11:45:42 +02:00
jonnysemon
4ff9f69548 Translated using Weblate (Arabic)
Currently translated at 99.5% (656 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-06-11 21:02:14 +02:00
Priit Jõerüüt
e1c7cb17da Translated using Weblate (Estonian)
Currently translated at 2.7% (18 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/et/
2024-06-10 20:09:43 +02:00
jonnysemon
e6defd7770 Translated using Weblate (Arabic)
Currently translated at 99.2% (654 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-06-10 20:09:42 +02:00
J-Jamet
bba9f45075 fix: Group height resizable #1770 2024-06-10 18:45:22 +02:00
Priit Jõerüüt
de1a2d1557 Added translation using Weblate (Estonian) 2024-06-10 12:10:53 +02:00
ngocanhtve
e995d06a26 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2024-06-08 06:09:18 +02:00
NicolaeFericitu
63f54a1318 Translated using Weblate (Romanian)
Currently translated at 98.4% (649 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-06-05 19:09:11 +00:00
Milo Ivir
0828c9f901 Translated using Weblate (Croatian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2024-06-05 19:09:10 +00:00
lgjint
51cd38450a fix: inline suggestions in keyboard only show ''Select entry'' Kunzisoft/KeePassDX#1708 2024-06-03 22:42:53 +08:00
Knut-Helge Eikrem
0a4a720fea Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2024-06-02 19:30:56 +02:00
Knut-Helge Eikrem
7f7ce171e0 Translated using Weblate (English)
Currently translated at 99.6% (657 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2024-06-02 19:30:56 +02:00
ΣΤΑΥΡΟΣ ΔΑΛΙΑΚΟΠΟΥΛΟΣ
87622cc6ec Translated using Weblate (Greek)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2024-06-02 00:09:19 +02:00
solokot
aaf2e64fdb Translated using Weblate (Russian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-05-29 20:09:24 +02:00
Patricio Carrau
c6f22e7ce5 Translated using Weblate (French)
Currently translated at 99.8% (658 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2024-05-28 00:09:11 +02:00
Random
749a2d19aa Translated using Weblate (Italian)
Currently translated at 99.8% (658 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-05-25 10:09:13 +02:00
gallegonovato
ed3491fbba Translated using Weblate (Spanish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-05-21 18:13:40 +02:00
109247019824
f5e4b7cd6a Translated using Weblate (Bulgarian)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-05-21 08:01:49 +00:00
大王叫我来巡山
0b614e81ee Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-05-21 08:01:48 +00:00
Wellington Terumi Uemura
0a93b660cf Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2024-05-21 08:01:47 +00:00
Oğuz Ersen
71e104e4bd Translated using Weblate (Turkish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-05-20 09:01:58 +02:00
Matthaiks
189c5de7ea Translated using Weblate (Polish)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-05-20 09:01:58 +02:00
Stephan Paternotte
a5e141c361 Translated using Weblate (Dutch)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-05-20 09:01:57 +02:00
Masowick
e76d8d4df7 Translated using Weblate (German)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-05-20 09:01:56 +02:00
Fjuro
d54c093985 Translated using Weblate (Czech)
Currently translated at 100.0% (659 of 659 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2024-05-20 09:01:55 +02:00
Hosted Weblate
086709e40f Merge branch 'origin/develop' into Weblate. 2024-05-19 07:55:21 +02:00
SC
9477e32ec8 Translated using Weblate (Portuguese)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2024-05-18 10:01:56 +02:00
SC
a66a0628ad Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2024-05-18 10:01:55 +02:00
Random
ea018bd4c1 Translated using Weblate (Italian)
Currently translated at 99.8% (657 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2024-05-18 10:01:54 +02:00
searinminecraft
8e81ee3b75 Translated using Weblate (Filipino)
Currently translated at 42.2% (278 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2024-05-16 08:02:04 +00:00
NicolaeFericitu
9c326f9be9 Translated using Weblate (Romanian)
Currently translated at 98.6% (649 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-05-16 08:02:02 +00:00
J-Jamet
7d2a0aa793 fix: Bad link for device unlocking #1826 2024-05-15 22:25:04 +02:00
J-Jamet
22d1442033 fix: Screenshot banner #1770 2024-05-15 22:05:31 +02:00
J-Jamet
ffa32a5501 fix: Entry validation button not accessible with the keyboard open #1770 2024-05-15 21:24:01 +02:00
J-Jamet
f50a6a8416 fix: Remove the "merge, reload" wording in read only mode #1709 2024-05-15 19:19:18 +02:00
bowornsin
7a82b75ee2 Translated using Weblate (Thai)
Currently translated at 90.1% (593 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-05-15 07:01:51 +00:00
NicolaeFericitu
350c0585b8 Translated using Weblate (Romanian)
Currently translated at 98.1% (646 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-05-15 07:01:51 +00:00
J-Jamet
5c4454d3ed fix: Nav header margin 2024-05-14 16:32:13 +02:00
J-Jamet
dfd1ae7a50 fix:
Removing a file from recents list doesn't remove star #1755
2024-05-14 15:16:53 +02:00
J-Jamet
d16fdd062f fix: F-Droid logotype #1814 2024-05-14 14:22:50 +02:00
J-Jamet
d6432698fc Merge branch 'yurtpage-i18n' into develop 2024-05-13 13:17:38 +02:00
J-Jamet
1a9c1e8bc1 Merge branch 'i18n' of github.com:yurtpage/KeePassDX into yurtpage-i18n 2024-05-13 13:17:20 +02:00
J-Jamet
2d05c6cb13 fix:
Coordinator layout to show the Snackbar error
2024-05-13 12:40:02 +02:00
J-Jamet
05f689913f fix:
Database is 0 Byte if Yubikey save is canceled #1680
2024-05-13 12:16:42 +02:00
J-Jamet
9e714c4192 fix:
Database is 0 Byte if Yubikey save is canceled #1680
2024-05-13 11:38:16 +02:00
J-Jamet
e1ad1e31f8 Gemfile 2024-05-13 09:50:02 +02:00
109247019824
b473bf1da8 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-05-09 13:33:28 +02:00
Milo Ivir
dfdc6eecb6 Translated using Weblate (Croatian)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2024-05-09 13:33:28 +02:00
ngocanhtve
e9145df77a Translated using Weblate (Vietnamese)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2024-05-07 14:07:14 +02:00
Pavel
4e824990d1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2024-05-07 14:07:13 +02:00
Wellington Terumi Uemura
5a053378e1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2024-05-07 14:07:12 +02:00
J-Jamet
e09306b6f3 Merge remote-tracking branch 'origin/develop' into develop 2024-05-06 16:15:44 +02:00
J-Jamet
21e577c1d3 fix: Change version to 4.0.6 2024-05-06 16:15:30 +02:00
searinminecraft
9d85d0979c Translated using Weblate (Filipino)
Currently translated at 14.4% (95 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fil/
2024-05-05 16:07:16 +02:00
Linerly
e4fec7ea1f Translated using Weblate (Indonesian)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2024-05-05 16:07:12 +02:00
Fjuro
c998e513fc Translated using Weblate (Czech)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2024-05-05 16:07:11 +02:00
searinminecraft
855b06c4b2 Added translation using Weblate (Filipino) 2024-05-05 05:34:40 +02:00
Oğuz Ersen
776012f02f Translated using Weblate (Turkish)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2024-05-04 15:07:23 +02:00
何意挽秋風
9bff531618 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-05-04 15:07:22 +02:00
DVXG
4d64818da1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-05-04 15:07:21 +02:00
qx100
837c8773a8 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-05-04 15:07:20 +02:00
solokot
97a199b504 Translated using Weblate (Russian)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-05-04 15:07:19 +02:00
Matthaiks
9cd37be5b1 Translated using Weblate (Polish)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2024-05-04 15:07:18 +02:00
Stephan Paternotte
90895fed52 Translated using Weblate (Dutch)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2024-05-04 15:07:17 +02:00
gallegonovato
df4b73abbb Translated using Weblate (Spanish)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2024-05-04 15:07:15 +02:00
ΣΤΑΥΡΟΣ ΔΑΛΙΑΚΟΠΟΥΛΟΣ
d2bed08ae0 Translated using Weblate (Greek)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2024-05-04 15:07:14 +02:00
Masowick
fd5e1472e1 Translated using Weblate (German)
Currently translated at 100.0% (658 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-05-04 15:07:13 +02:00
Anonymous
6a22126006 Translated using Weblate (Romanian)
Currently translated at 97.8% (644 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-05-03 14:38:37 +02:00
Anonymous
ff5e9049a0 Translated using Weblate (German)
Currently translated at 99.5% (655 of 658 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-05-03 14:38:36 +02:00
J-Jamet
7570aa9f49 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2024-05-03 14:32:56 +02:00
J-Jamet
4fcdd3da23 Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2024-05-03 14:07:35 +02:00
qx100
f8a65b4d13 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2024-05-02 09:45:46 +02:00
Yurt Page
5bb70a5043 fastlane i18n ru
Signed-off-by: Yurt Page <yurtpage@gmail.com>
2024-04-23 19:18:09 +03:00
J-Jamet
c4788159e4 Merge branch 'thekyber-patch-1' into develop 2024-04-22 17:13:16 +02:00
J-Jamet
403b0aedca Merge branch 'ngocanhtve-master' into develop 2024-04-22 17:06:56 +02:00
TheKyber
cae12b84df better way to verify github apk files
Verifying the signing certificate hash is way better than verifying the hash of the apk files, because it does not change at all, and it will be very noticeable if it was changed from the README.md file than just GitHub release notes.

And you will not have to calculate the hash and publish it in the future, so less work for you.
2024-04-19 13:52:26 +01:00
J-Jamet
40dbbbf0a0 fix: Recognize Brave forms 2024-04-19 13:41:58 +02:00
Champ0999
9e0da27484 Translated using Weblate (English (United Kingdom))
Currently translated at 35.2% (228 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2024-04-18 18:03:25 +02:00
Gia Huy Lữ
7e6c2463d6 Translated using Weblate (Vietnamese)
Currently translated at 43.2% (280 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2024-04-18 18:03:24 +02:00
Thomas
69a76db166 Translated using Weblate (Chinese (Traditional))
Currently translated at 93.1% (603 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-04-18 18:03:24 +02:00
devchung
efd282e326 Translated using Weblate (Yue (Traditional))
Currently translated at 20.7% (134 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/yue_Hant/
2024-04-17 14:33:37 +02:00
devchung
6471fef28c Translated using Weblate (Chinese (Traditional))
Currently translated at 92.5% (599 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2024-04-17 14:33:36 +02:00
devchung
5a0691b70c Added translation using Weblate (Yue (Traditional)) 2024-04-17 08:21:39 +02:00
J-Jamet
27ea357348 fix: Fix username recognition with firefox #1665 2024-04-14 20:44:51 +02:00
bowornsin
e12de29cce Translated using Weblate (Thai)
Currently translated at 91.8% (594 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-04-12 06:01:55 +02:00
Rasmus
a236be4e38 Translated using Weblate (Danish)
Currently translated at 99.0% (641 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2024-04-05 14:01:53 +02:00
SpaceFrrog
5300643f62 Translated using Weblate (English (United Kingdom))
Currently translated at 25.9% (168 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2024-04-04 09:43:58 +02:00
SpaceFrrog
469b837daf Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-04-04 09:43:57 +02:00
Ghost of Sparta
84ca3e8982 Translated using Weblate (Hungarian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2024-03-28 12:02:02 +01:00
Tim Trek
bfb8be159a Translated using Weblate (German)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2024-03-19 07:09:23 +01:00
bowornsin
13d7af9c76 Translated using Weblate (Thai)
Currently translated at 91.6% (593 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2024-03-17 08:43:10 +01:00
David C
66d78497b9 Translated using Weblate (Slovenian)
Currently translated at 10.2% (66 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sl/
2024-03-08 14:01:54 +01:00
109247019824
32743f7e19 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-03-06 22:01:44 +01:00
109247019824
c2cc4451ff Translated using Weblate (Bulgarian)
Currently translated at 95.0% (615 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-03-05 15:02:38 +01:00
109247019824
67370bbc3d Translated using Weblate (Bulgarian)
Currently translated at 84.8% (549 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-03-04 08:01:48 +01:00
Lenni
1201ae1956 Translated using Weblate (English (United Kingdom))
Currently translated at 25.1% (163 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2024-03-03 07:01:51 +01:00
109247019824
0eb2786ce5 Translated using Weblate (Bulgarian)
Currently translated at 82.3% (533 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-03-03 07:01:50 +01:00
109247019824
bbdeb66f8e Translated using Weblate (Bulgarian)
Currently translated at 78.3% (507 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-03-02 06:02:00 +01:00
J-Jamet
641698a652 Merge branch 'MDP43140-master' into develop 2024-03-01 17:24:16 +01:00
J-Jamet
15880e995a Merge branch 'master' of github.com:MDP43140/KeePassDX into MDP43140-master 2024-03-01 17:23:54 +01:00
J-Jamet
1e849b106b Merge branch 'Rilele-master' into develop 2024-03-01 16:36:42 +01:00
109247019824
31bc405e86 Translated using Weblate (Bulgarian)
Currently translated at 73.1% (473 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-29 23:02:09 +01:00
109247019824
6e69eee2a3 Translated using Weblate (Bulgarian)
Currently translated at 69.2% (448 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-27 20:49:46 +01:00
109247019824
279b095f30 Translated using Weblate (Bulgarian)
Currently translated at 62.1% (402 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-27 12:02:02 +01:00
109247019824
9821499cdb Translated using Weblate (Bulgarian)
Currently translated at 36.9% (239 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-26 08:02:03 +01:00
--//--
b76c39862a Translated using Weblate (Burmese)
Currently translated at 6.8% (44 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/my/
2024-02-23 16:01:59 +01:00
adfc
7948d234f5 Translated using Weblate (English (United Kingdom))
Currently translated at 13.9% (90 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2024-02-23 16:01:58 +01:00
109247019824
50fc3aa9fc Translated using Weblate (Bulgarian)
Currently translated at 20.2% (131 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-23 16:01:57 +01:00
adfc
79a7adb05b Translated using Weblate (Hindi)
Currently translated at 22.7% (147 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hi/
2024-02-23 16:01:56 +01:00
Besnik Bleta
c02770ec2a Translated using Weblate (Albanian)
Currently translated at 55.9% (362 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sq/
2024-02-22 13:02:04 +01:00
Elvis
bf24033513 Translated using Weblate (Uzbek)
Currently translated at 7.5% (49 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uz/
2024-02-18 20:02:04 +01:00
solokot
d97c87e90c Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-02-18 20:02:02 +01:00
Elvis
6d29fdc448 Translated using Weblate (Uzbek)
Currently translated at 4.3% (28 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uz/
2024-02-17 19:39:44 +01:00
Elvis
d16f0dd3f6 Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-02-17 19:39:43 +01:00
Elvis
d023c97049 Added translation using Weblate (Uzbek) 2024-02-17 19:08:49 +01:00
jonnysemon
73c8d63f0e Translated using Weblate (Arabic)
Currently translated at 99.3% (643 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2024-02-07 11:52:29 +01:00
Salif Mehmed
30709de7e2 Translated using Weblate (Bulgarian)
Currently translated at 20.0% (130 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-06 23:01:53 +01:00
Salif Mehmed
c3e560766e Translated using Weblate (Bulgarian)
Currently translated at 17.9% (116 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2024-02-05 13:01:51 +01:00
Aleksandar Sazdanović
7c7889e87e Translated using Weblate (Serbian (latin))
Currently translated at 37.0% (240 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sr_Latn/
2024-02-04 12:01:59 +01:00
Pere O
6547ccecdd Translated using Weblate (Catalan)
Currently translated at 97.9% (634 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2024-02-03 09:35:48 +01:00
Pere O
d467a591b0 Translated using Weblate (Catalan)
Currently translated at 65.3% (423 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2024-02-01 23:01:52 +01:00
solokot
42613ff480 Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2024-01-21 10:01:50 +01:00
NicolaeFericitu
3a3d4b9e54 Translated using Weblate (Romanian)
Currently translated at 99.6% (645 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-01-15 17:06:12 +01:00
NicolaeFericitu
0b5c0bcf74 Translated using Weblate (Romanian)
Currently translated at 88.4% (572 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-01-09 23:06:34 +00:00
Deleted User
5937f009e1 Translated using Weblate (Danish)
Currently translated at 98.4% (637 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2024-01-09 23:06:13 +00:00
SC
c7f3d2a64e Translated using Weblate (Portuguese)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2024-01-06 17:06:11 +01:00
NicolaeFericitu
41263307d7 Translated using Weblate (Romanian)
Currently translated at 78.0% (505 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2024-01-06 17:06:11 +01:00
SC
f2ff52477e Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2024-01-06 17:06:11 +01:00
Mike
6baa5a0730 Translated using Weblate (Slovak)
Currently translated at 95.0% (615 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2023-12-31 18:36:23 +01:00
ERYpTION
5a5cb8c866 Translated using Weblate (Danish)
Currently translated at 96.9% (627 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2023-12-08 20:03:49 +00:00
Kenny
a3a78366f7 Translated using Weblate (Romanian)
Currently translated at 76.3% (494 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2023-12-06 15:11:11 +01:00
ngocanhtve
d936292ed9 Updated vi\strings.xml
Complete the Vietnamese translation.
2023-12-01 21:35:47 +07:00
ngocanhtve
a3ded18f59 Translated using Weblate (Vietnamese)
Currently translated at 38.3% (248 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2023-12-01 14:14:44 +01:00
Avi Parshan
0969135f7f Translated using Weblate (Hebrew)
Currently translated at 44.9% (291 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2023-11-27 18:02:20 +00:00
Rilele
7e1c1a6324 Fix German translation 2023-11-25 13:02:36 +01:00
J-Jamet
aa43dd2a49 fix: Upgrade to 4.1.0 2023-11-10 23:00:34 +01:00
J-Jamet
81fe9b6fc2 fix: phone field recognition 2023-11-10 22:56:23 +01:00
J-Jamet
6807fdc1cc Merge tag '4.0.5' into develop
4.0.5
2023-11-09 08:16:36 +01:00
J-Jamet
ac1590810f Merge branch 'release/4.0.5' 2023-11-09 08:16:29 +01:00
J-Jamet
9dd4d77535 fix: upgrade version 2023-11-09 08:06:30 +01:00
J-Jamet
c7aa6a9d96 fix: revert #1490 2023-11-09 08:02:16 +01:00
J-Jamet
fc56db2f83 fix: Autofill recognition 2023-11-08 20:43:50 +01:00
MDP43140
c58e56257d Fix Indonesia language
language code for Indonesia is apparently `in` instead of `id` in Java, thus makes the app displays default English language for users with Indonesian language. a simple folder rename should hopefully fix this
2023-11-07 23:27:49 +07:00
solokot
b27eb280a8 Translated using Weblate (Russian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2023-11-07 10:11:49 +01:00
J-Jamet
78c7f4078c Merge tag '4.0.4' into develop
4.0.4
2023-11-07 00:33:28 +01:00
J-Jamet
28ccaffcf5 Merge branch 'release/4.0.4' 2023-11-07 00:33:15 +01:00
J-Jamet
78739558d6 fix: password color #1490 2023-11-07 00:26:24 +01:00
J-Jamet
cd195d30de fix: update CHANGELOG 2023-11-06 23:49:58 +01:00
J-Jamet
03bf752284 fix: update CHANGELOG 2023-11-06 23:48:39 +01:00
J-Jamet
238fab3e1d fix: device unlock #1682 2023-11-06 23:47:31 +01:00
J-Jamet
fcd9af8f84 Revert "Revert "fix: Remove Lock in Autofill""
This reverts commit b44491ebbe.
2023-11-06 23:37:28 +01:00
J-Jamet
b44491ebbe Revert "fix: Remove Lock in Autofill"
This reverts commit 544f7003f6.
2023-11-06 23:35:14 +01:00
J-Jamet
1f8018fd5b fix: autofill 2023-11-06 23:23:42 +01:00
J-Jamet
1d67656fa0 fix: autofill 2023-11-06 22:40:48 +01:00
J-Jamet
64b8023d1a fix: upgrade version and CHANGELOG 2023-11-06 21:40:01 +01:00
J-Jamet
cc1697e7ec fix: last form field recognition #1572 2023-11-06 21:37:27 +01:00
J-Jamet
28943e77e8 Merge tag '4.0.3' into develop
4.0.3
2023-11-06 12:29:02 +01:00
J-Jamet
575109da9f Merge branch 'release/4.0.3' 2023-11-06 12:28:53 +01:00
J-Jamet
a99667d471 feat: New fastfile to build Libre in github 2023-11-06 12:28:28 +01:00
J-Jamet
6a7420bd3a fix: replace tags 2023-11-06 10:45:20 +01:00
J-Jamet
e8dbe05615 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2023-11-06 10:35:53 +01:00
J-Jamet
6b6566cd29 fix: small change #1674 2023-11-06 08:58:36 +01:00
J-Jamet
0001d31c2c fix: small change #1674 2023-11-06 08:57:48 +01:00
J-Jamet
974686e698 fix: small changes 2023-11-04 18:56:36 +01:00
J-Jamet
7b7063b9be fix: check biometric unlock availability before build the fragment #1400 2023-11-04 18:14:24 +01:00
J-Jamet
55061a9469 fix: runtime exception #1649 2023-11-04 18:01:23 +01:00
J-Jamet
c433fb643c fix: change password color dynamically #1490 2023-11-04 17:33:40 +01:00
J-Jamet
02306385b6 fix: #1641 #1656 2023-11-04 16:09:10 +01:00
J-Jamet
432ac1bcec Merge branch 'JohnVeness-biometric' into develop 2023-11-04 16:07:41 +01:00
J-Jamet
d9480e0c9a Merge branch 'MkQtS-f-droid-link' into develop 2023-11-04 16:05:24 +01:00
J-Jamet
815fb911d6 fix: regex OTP recognition #1596 2023-11-04 16:03:20 +01:00
J-Jamet
68cbdae8e0 fix: update CHANGELOG 2023-11-04 15:07:15 +01:00
J-Jamet
2d8f8aeef3 fix: Compatibility mode to retrieve username #1508 2023-11-04 13:02:57 +01:00
J-Jamet
479bc7be71 Upgrade to 4.0.3 2023-11-04 11:36:44 +01:00
/dev/urandom
cbc6df2e62 Translated using Weblate (Esperanto)
Currently translated at 21.9% (142 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eo/
2023-11-03 06:10:15 +01:00
Urystem
d16b4cfadb Added translation using Weblate (Kazakh) 2023-10-31 12:48:01 +01:00
ngocanhtve
a97bad1f86 Translated using Weblate (Vietnamese)
Currently translated at 34.4% (223 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2023-10-30 07:09:19 +01:00
J-Jamet
7198ffff43 fix: Save as 2023-10-29 20:45:35 +01:00
ngocanhtve
e173159d13 Translated using Weblate (Vietnamese)
Currently translated at 33.3% (216 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2023-10-28 17:33:10 +02:00
Jean Mareilles
1b54b79e88 Translated using Weblate (French)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2023-10-27 11:26:36 +02:00
P.O
bb3436615e Translated using Weblate (Swedish)
Currently translated at 60.1% (389 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2023-10-16 04:19:17 +00:00
Stasky745
809db61c35 Translated using Weblate (Catalan)
Currently translated at 62.4% (404 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2023-10-16 04:19:16 +00:00
elgratea
88e53fcba8 Translated using Weblate (Bulgarian)
Currently translated at 13.1% (85 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2023-10-13 21:02:05 +02:00
Balázs Meskó
fe68b7e294 Translated using Weblate (Hungarian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2023-10-08 16:01:48 +02:00
Åzze
b7b76d6da7 Translated using Weblate (Finnish)
Currently translated at 39.7% (257 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fi/
2023-10-08 16:01:48 +02:00
Fjuro
e2eae43fc9 Translated using Weblate (Czech)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2023-10-08 16:01:47 +02:00
bowornsin
f41ecec09c Translated using Weblate (Thai)
Currently translated at 91.4% (592 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/th/
2023-10-02 02:01:14 +02:00
John Veness
15ad4d11ef Fix paragraph break in Biometric warning text
Added an extra new line between the last two paragraphs, for consistency with the paragraphs above.
2023-10-01 21:07:54 +01:00
P.O
0cf9d98f14 Translated using Weblate (Swedish)
Currently translated at 59.3% (384 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2023-09-29 18:35:22 +02:00
elgratea
612e642523 Translated using Weblate (Bulgarian)
Currently translated at 9.8% (64 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2023-09-19 22:59:13 +02:00
Milo Ivir
0ff77eb157 Translated using Weblate (Croatian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2023-09-18 17:01:16 +00:00
Alexthegib
2b8935a5d7 Translated using Weblate (Portuguese)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2023-09-16 02:53:56 +02:00
Mesut Akcan
afdc5c8460 Translated using Weblate (Turkish)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2023-09-13 15:48:51 +02:00
alejandracios
91ba2dff2d Translated using Weblate (Spanish)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2023-09-13 15:48:50 +02:00
MkQtS
2d26079c49 readme: don't specify language in F-droid links
then F-droid will follow the browser locale
2023-09-13 19:16:43 +08:00
J-Jamet
f13d99e0d1 Merge tag '4.0.2' into develop
4.0.2
2023-09-11 21:55:56 +02:00
Linerly
d3182b8d2a Translated using Weblate (Indonesian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2023-09-11 19:51:53 +02:00
jonnysemon
f52d139acc Translated using Weblate (Arabic)
Currently translated at 97.5% (631 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2023-09-11 19:51:53 +02:00
Eric
87e9a38548 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2023-09-11 19:51:52 +02:00
Ihor Hordiichuk
faa70c57b3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2023-09-11 19:51:52 +02:00
Alexthegib
5172c07c18 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2023-09-11 19:51:51 +02:00
Wellington Terumi Uemura
a80fa03db4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2023-09-11 19:51:51 +02:00
Matthaiks
d73e02948e Translated using Weblate (Polish)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2023-09-11 19:51:51 +02:00
Stephan Paternotte
283657e1b7 Translated using Weblate (Dutch)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2023-09-11 19:51:50 +02:00
Random
d3c4a3a17e Translated using Weblate (Italian)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2023-09-11 19:51:50 +02:00
Kunzisoft
9184bc40e5 Translated using Weblate (French)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2023-09-11 19:51:50 +02:00
gallegonovato
84bd98ebf4 Translated using Weblate (Spanish)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2023-09-11 19:51:49 +02:00
Retrial
4ef2cbcaeb Translated using Weblate (Greek)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2023-09-11 19:51:49 +02:00
Fjuro
35f8b45bf4 Translated using Weblate (Czech)
Currently translated at 100.0% (647 of 647 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2023-09-11 19:51:48 +02:00
J-Jamet
0474e5b1fe Merge branch 'Sandelinos-themed-icons' into develop 2022-09-29 16:44:43 +02:00
593 changed files with 29869 additions and 11407 deletions

View File

@@ -1,49 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**KeePass Database**
- Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2]
- Location: [e.g. Remote file retrieved with GDrive app]
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
- Size: [e.g. 150Mo]
- Contains attachment: [e.g. Yes]
**KeePassDX:**
- Version: [e.g. 2.5.0.0beta23]
- Build: [e.g. Free]
- Language: [e.g. French]
**Android:**
- Device: [e.g. GalaxyS8]
- Version: [e.g. 8.1]
**Additional context**
Add any other context about the problem here.
- Browser for Autofill: [e.g. Chrome version X]

62
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Bug report
description: Report a bug.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Please check out the [Wiki](https://github.com/Kunzisoft/KeePassDX/wiki) and [existing issues](https://github.com/Kunzisoft/KeePassDX/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) to see if your problem has already been reported.
- type: checkboxes
id: checks
attributes:
label: Checks
options:
- label: I have read the Wiki, searched the open issues, and still think this is a new bug.
required: true
- type: textarea
id: bug
attributes:
label: "Explain the problem clearly and succinctly:"
validations:
required: true
- type: textarea
id: expected
attributes:
label: "Describe what you expected to happen:"
- type: input
id: app-version
attributes:
label: "KeePassDX version:"
validations:
required: true
- type: dropdown
id: app-build
attributes:
label: "Build:"
multiple: true
options:
- Free
- Libre
validations:
required: true
- type: input
id: database-version
attributes:
label: "Database version:"
- type: input
id: file-provider
attributes:
label: "File provider (`content://` URI)"
- type: input
id: android-version
attributes:
label: "Android version:"
- type: input
id: android-device
attributes:
label: "Android device:"
- type: textarea
id: additional-context
attributes:
label: "Additional context:"

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,33 @@
name: Feature request
description: Suggest an idea.
labels: ["feature"]
body:
- type: markdown
attributes:
value: |
Please check out the [Wiki](https://github.com/Kunzisoft/KeePassDX/wiki) and [existing issues](https://github.com/Kunzisoft/KeePassDX/issues?q=is%3Aissue%20state%3Aopen%20label%3Afeature) to see if your feature has already been reported.
- type: checkboxes
id: checks
attributes:
label: Checks
options:
- label: I have read the Wiki, searched the open issues, and still think this is a new feature.
required: true
- type: textarea
id: problem
attributes:
label: "Explain the problem clearly and succinctly:"
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like:"
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "Describe alternatives you've considered:"
- type: textarea
id: additional-context
attributes:
label: "Additional context:"

3
.gitignore vendored
View File

@@ -19,6 +19,9 @@ bin/
gen/
out/
# Kotlin folder
.kotlin/
# Gradle files
.gradle/
build/

128
CHANGELOG
View File

@@ -1,3 +1,131 @@
KeePassDX(4.3.0)
* Manual change of app language #1884 #1990
* Fix autofill username detection #2276
KeePassDX(4.2.4)
* Fix remembering database location #2262
KeePassDX(4.2.3)
* Fix multiple Passkey selection #2253
* Fix database dialog subtitle #2254
* Fix save search info if URL present #2255
* Small fixes
KeePassDX(4.2.2)
* Fix database merge algorithm #2223
* Fix save search info #2243
* Fix Play Service as privileged app for Passkey Cross Device Authentication #2244
* Small fixes
KeePassDX(4.2.1)
* Fix Magikeyboard autosearch #2233
* Fix database merge #2223
* Fix dialog database action #2234
* Fix autofill selection #2238 #2235
* Small fixes
KeePassDX(4.2.0)
* Passkeys management #1421 #2097 (@cali-95)
* Confirm usage of passkey #2165 #2124
* Dialog to manage missing signature #2152 #2155 #2161 #2160
* Capture error #2159 #2215
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
* Search settings #2112 #2181 #2187 #2204
* Autofill refactoring #765 #2196
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
KeePassDX(4.1.9)
* Fix landscape UI #2198 #2200 (@chenxiaolong)
* Fix start loop and flash screen #2201
* Small fixes
KeePassDX(4.1.8)
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
* Fix merge deletion #1516
* Fix space in search #175
* Fix deletable recycle bin #2163
* Small fixes
KeePassDX(4.1.7)
* Fix CipherDatabase for biometric states #2119
KeePassDX(4.1.6)
* Auto open biometric prompt from database list #2113
* Fix Keystore errors #2114 #2115
* Complete biometric refactoring for better compatibility
KeePassDX(4.1.5)
* Fix auto prompt #2111
KeePassDX(4.1.4)
* Fix UnlockManager #2098 #2101
* Auto device unlock prompt #2105
* Small fixes ##2066
KeePassDX(4.1.3)
* Fix Autofill Registration #2089
* Fix Biometric errors #2081
* Fixed timestamp in copy file #1981 #1983
* Fix Template Email #1986
* Fix Search #2096
KeePassDX(4.1.2)
* Fix URL search #1940 #1946 #2003 #2040 #2044
* Fix Autofill popup #2054
* Fix Group notes #2053
* Fix Dialog background #2005 #2004 (Thx @codokie)
* Fix OTP configuration #2042 #2065 (Thx @Dev-ClayP)
* Fix small UI elements #1987 #2007 (Thx @ymcx)
* RTL layout support #2021 (Thx @codokie)
* App Metadata to translation #1823
KeePassDX(4.1.1)
* Fix date parser #1933
* Fix domain search #1820 #1936
KeePassDX(4.1.0)
* Generate keyfile #1290
* Hide template group #1894
* Group count sum recursively #421
* Fix date fields #1695 #1710
* Fix distinct domain names #1105 #1820
* Resets the advanced unlock expiration #1600
* Password entropy #1490 #1355
* Upgrade to API 34 (Android 14) #1730
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889 #1289 #1600 #1467 #1870
KeePassDX(4.0.8)
* Fix graphical bug that prevented databases from being opened on some versions of Android #1848 #1850
KeePassDX(4.0.7)
* Prevent 0 Byte file with cache during a save exception #1620 #1594 #1680
* Fix inline suggestions in keyboard #1840
* Fix broken links by default #1755
* Fix UX by allowing validation in entry edition #1770
* Fix small bugs #1709
KeePassDX(4.0.6)
* Fix form filled recognition #1508 #1735 #1508 #1790 #1783 #1797 #1801 #1802 #1804 #1665
* Fix translations #1707 #1683 #1712
* Update APK verifier #1810
KeePassDX(4.0.5)
* Fix form filled recognition #1572 #1508
* Rollback password color #1686 #1490
KeePassDX(4.0.4)
* Fix form filled recognition #1572 #1677
* Fix device unlock #1682
* Fix password color #1490
KeePassDX(4.0.3)
* Fix "Save as" in Read Only mode #1666
* Fix username autofill #1665 #530 #1572 #1426 #1523 #1556 #1653 #1658 #1508 #1667
* Fix regex OTP recognition #1596
* Change password color dynamically #1490
* Small fixes #1641 #1656 #1649 #1400 #1674
KeePassDX(4.0.2)
* Fix Autofill with API 33

View File

@@ -1,43 +1,49 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.6)
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.794.0)
aws-sdk-core (3.180.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
aws-eventstream (1.4.0)
aws-partitions (1.1146.0)
aws-sdk-core (3.229.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.132.0)
aws-sdk-core (~> 3, >= 3.179.0)
logger
aws-sdk-kms (1.110.0)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.196.1)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6)
aws-sigv4 (1.6.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
bigdecimal (3.2.2)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.5)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.100.0)
faraday (1.10.3)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -53,27 +59,27 @@ GEM
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.2.7)
fastlane (2.214.0)
fastimage (2.4.0)
fastlane (2.228.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
@@ -82,34 +88,39 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-versioning_android (0.1.1)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.46.0)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.1)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -117,64 +128,66 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.7.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.8.3)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.6.3)
jwt (2.7.1)
memoist (0.16.2)
mini_magick (4.12.0)
mini_mime (1.1.2)
multi_json (1.15.0)
multipart-post (2.3.0)
nanaimo (0.3.0)
naturally (2.2.1)
optparse (0.1.1)
json (2.13.2)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.17.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.0)
public_suffix (5.0.3)
rake (13.0.6)
plist (3.7.2)
public_suffix (6.0.2)
rake (13.3.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.6)
rouge (2.0.7)
rexml (3.4.1)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.17.0)
rubyzip (2.4.1)
security (0.1.5)
signet (0.20.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@@ -182,30 +195,27 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.8.1)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.22.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
@@ -217,4 +227,4 @@ DEPENDENCIES
fastlane-plugin-versioning_android
BUNDLED WITH
2.1.4
2.6.9

View File

@@ -1,24 +1,26 @@
# Android KeePassDX
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
<img alt="KeePassDX Icon" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password safe and manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
<img alt="KeePassDX Screenshot" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
### Features
- Create database files / entries and groups.
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
- **Passkeys** for authentication and **local storage of private keys**.
- **Biometric recognition** for fast unlocking (fingerprint / face unlock / …).
- **One-Time Password** management (HOTP / TOTP) for two-factor authentication (2FA).
- **Autofill** for easy form filling with passwords.
- **Magikeyboard** to efficiently fill in any field.
- Create **encrypted database files**.
- Organisation of credentials by **entry** and in **group** trees.
- Allows opening and **copying URI / URL fields quickly**.
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
- Material design with **themes**.
- **Auto-Fill** and Integration.
- Field filling **keyboard**.
- Dynamic **templates**
- Dynamic **templates** for each type of entry.
- **History** of each entry.
- Precise management of **settings**.
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
- Material design with **themes**.
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
- Code written in **native languages** (Kotlin / Java / JNI / C).
KeePassDX is **open source** and **ad-free**.
@@ -48,17 +50,39 @@ Optional visual styles are accessible after a contribution (and a congratulatory
## Download
*[F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies that all the libraries and app code is libre software.*
*[F-Droid](https://f-droid.org/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies all the libraries and app code is libre software.*
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
[<img src="https://raw.githubusercontent.com/Kunzisoft/Github-badge/main/get-it-on-github.png"
alt="Get it on Github"
height="80">](https://github.com/Kunzisoft/KeePassDX/releases)
| Source | Status | [Version](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ#why-a-libre-and-free-version) |
|--------|--------|---------|
| [Google Play](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free) | ![Google Play Release](https://img.shields.io/endpoint?color=blue&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dcom.kunzisoft.keepass.free%26gl%3DUS%26hl%3Den%26l%3DGoogle%2520Play%26m%3D%24version) | Free + [Pro](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro) |
| [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) | ![F-Droid Version](https://img.shields.io/f-droid/v/com.kunzisoft.keepass.libre?logo=F-Droid&label=F-Droid) | Libre |
| [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.free) | ![IzzyOnDroid Version](https://img.shields.io/endpoint?&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAADAFBMVEUA0////wAA0v8A0v8A0////wD//wAFz/QA0/8A0/8A0/8A0/8A0v///wAA0/8A0/8A0/8A0/8A0//8/gEA0/8A0/8B0/4A0/8A0/8A0/+j5QGAwwIA0//C9yEA0/8A0/8A0/8A0/8A0/8A0/+n4SAA0/8A0/8A0/+o6gCw3lKt7QCv5SC+422b3wC19AC36zAA0/+d1yMA0/8A0/+W2gEA0/+w8ACz8gCKzgG7+QC+9CFLfwkA0/8A0////wAA0/8A0/8A0/8A0/+f2xym3iuHxCGq5BoA1P+m2joI0vONyiCz3mLO7oYA0/8M1Piq3Ei78CbB8EPe8LLj9Ly751G77zWQ1AC96UYC0fi37CL//wAA0/8A0////wD//wCp3jcA0/+j3SGj2i/I72Sx4zHE8FLB8zak1kYeycDI6nRl3qEA0/7V7psA0v6WzTa95mGi2RvB5XkPy9zH5YJ3uwGV1yxVihRLiwdxtQ1ZkAf//wD//wD//wD//wD//wCn5gf//wD//wD//wD//wD//wAA0/+h4A3R6p8A0/+X1w565OD6/ARg237n9csz2vPz+gNt37V/vifO8HW68B/L6ZOCwxXY8KRQsWRzhExAtG/E612a1Rd/pTBpmR9qjysduKVhmxF9mTY51aUozK+CsDSA52T//wD//wAA0////wD//wBJ1JRRxFWjzlxDyXRc0pGT1wCG0CWB3VGUzSTh8h6c0TSr5CCJ5FFxvl6s4H3m8xML0/DA5CvK51EX1N+Y2gSt4Dag3ChE3fax2ki68yO57NF10FRZnUPl88eJxhuCxgCz5EOLwEGf1DFutmahzGW98x0W1PGk3R154MHE6bOn69qv3gy92oG90o+Hn07B7rhCmiyMwECv1nO+0pQfwrCo57xF2daXsVhKrEdenQAduaee1Bsjr42z5D9RoCXy+QNovXpy2Z5MtWDO/TiSukaF3UtE1K6j3B4YwLc5wXlzpyIK0u5zy3uJqg4pu5RTpkZmpVKyAP8A0wBHcExHcEyBUSeEAAABAHRSTlP///9F9wjAAxD7FCEGzBjd08QyEL39abMd6///8P/ZWAnipIv/cC6B//7////////L/1Dz/0D///////86/vYnquY3/v///5T//v///17///////////////84S3QNB/8L/////////////7r/////NP////9l/////wPD4yis/x7Ym2lWSP+em////0n////////v///////////////////7//7pdGN3Urr6/+v/6aT////+//H/o2P/1v+7r7jp4PM/3p4g////g///K///481LxO///v////9w////8v/////9/p3J///a+P9v/5KR/+n///+p/xf//8P//wAAe7FyaAAABCZJREFUSMdj+E8iYKBUgwIHnwQ3N7cEHxcH+///VayoAE0Dh41qR7aBnCIQ8MsJKHH9/99czYYMWlA0cIkJGjMgAKfq//9RNYzIgLcBWYOTiCgDMhDn+B9bh6LebiWyH6L5UZQzONoAHWSHoqEpDkkDsyKqelv1//9rG1HUN9YihZK9AKp6BkG+/6xNqA5ajhSsCkrIipmYGGRa//9vQXVQXSySBnkWJOUMfn5Myuz/G3hR1NdEIUUchwiy+bkTsg4dbW/fu6W/e1c3XMMy5JiOZkFxUFZo74mgKTqaKXu0+2HqVwkja3BH9kFu361JwcHTfPJD4mdfe8ULAdVRyGlJAcVFfg+CQOozZ4XrJ85+JgwBsVXIGriQw5Tp4ZScezd8JiWnBupru30qwJZa+ZAjmWlC8fUZM4qB6kPnLNSPLMWqQQ5ZQ5aOzs1HmamBaQHzFs6y+qAmJCTE8f9/QgKSBg4DJPWc6zVDQkIC09JkZSPD38kukpExFpT4z67uYI/QwCOOCCK/izvu5CWl6AcEWMnKWml7LWbKZfH9/99UkknQHhGsynDz+65eWXv3/JmJrq5eXienVlRUfH/z8VvCf45soKQIH1yDEQsszrp6gwq9C73T87xcXadKl5TkFev4A/2tygmSBqYXqAYJmK+ZuoJydDR1vP09DA0NOy2kpdML81+U/heCpH1JU3jig7lJ5nKOT4i/t6ZHkqGzs4lJmIVHfrj+JR4HqLQSD0yDkCNEpGNn5ix9D03/eJdElTZdKV2TpNOhkwt8YUlNUgimgV0dLMBvf1gz1MolPd5FRcVNSkpDQ8owJeBCDyIhrIDnOD5QcuIU+3/2QKSs9laQ+noNLS0zLWdtqyP7mBAFAw88TwsJgMuJYweBGjYngtWbmeuZOW+bvNQToUFOAlFqOBk4Ov3/L7Z60/aN0p1tUhpa5nqWlub7C3p2I9QzyAghlUvczOz/1fhzPT3XSIfpSmmYAdVbmm1gV0dSz8DSilpUQsqCddIWIA3meuZaJqdMJZEzl6gRqgZIWZAxUdoizERXN8yi5MltcZTChzMaRQM3JNUWHS8rL/+yaPGvMmvr5ywoGoxtkDWwQ+Pb89ycBeWfGSJeL/la+RS1eOPnRtbQKgMRjZg+t8x6PkP273nWQAoFOPAgaeAThKXAmXMrK39Kmr5fsuBlBqoXfJGLe3VbmHjG9Mczi9T//3h7vygXtcDlQtJg44iQiIjIBRbGPO7gghPJy0ZIxT2HOLIUgwxQzsgYrUR350HSIMaJLidhgKY+mw+pflBDrX8E7OGBjPCAPc76gQFSTqAIiYrb/8dRP4CyosJ/rmwU5XIxHMilt4QBJwsSkBMClxOQULBlkRRwEONmR2kJcDGjADX2/+xO8r5iqjExqmLyrWpcPFRta1BfAwCtyN3XpuJ4RgAAAABJRU5ErkJggg==&url=https://apt.izzysoft.de/fdroid/api/v1/shield/com.kunzisoft.keepass.free&label=IzzyOnDroid) | Free & [Libre](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.libre) |
| [GitHub](https://github.com/Kunzisoft/KeePassDX/releases) / [Obtainium](https://github.com/ImranR98/Obtainium) | ![GitHub Release](https://img.shields.io/github/v/release/Kunzisoft/KeePassDX?include_prereleases&logo=GitHub&label=GitHub) | Free & Libre |
## Package authenticity from GitHub
- Download the app from [GitHub releases](https://github.com/Kunzisoft/KeePassDX/releases/latest)
- Install [`apksigner`](https://developer.android.com/tools/apksigner) from [Android Studio](https://developer.android.com/studio)
- Open the directory where you saved the downloaded file in the Terminal
- Make sure that you have `apksigner` installed by running:
```shell
apksigner --version
```
- Depending on the APK file you downloaded, run:
```shell
apksigner verify --verbose --print-certs -min-sdk-version 24 KeePassDX-*.apk
```
You should get this output :
```shell
Verified using v2 scheme (APK Signature Scheme v2): true
...
Number of signers: 1
Signer #1 certificate SHA-256 digest: 7d55b8af210381aabf960f07e17cf7857b6d2a642ca2da6bf0bdf1b200362f04
...
Signer #1 public key SHA-256 digest: 5d261d3176db1e077b80112824d9390167f3be0561827e42112ed6b71192db81
```
If it's the case, this means that the APK was well built by the author of KeePassDX.
## Frequently Asked Questions
@@ -74,7 +98,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
## License
Copyright © 2023 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
Copyright © 2025 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePassDX.

View File

@@ -5,15 +5,14 @@ apply plugin: 'kotlin-kapt'
android {
namespace 'com.kunzisoft.keepass'
compileSdkVersion 33
buildToolsVersion "33.0.2"
compileSdkVersion 36
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 33
versionCode = 125
versionName = "4.0.2"
minSdkVersion 19
targetSdkVersion 35
versionCode = 150
versionName = "4.3.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -36,6 +35,17 @@ android {
}
}
buildFeatures {
buildConfig true
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
flavorDimensions "version"
productFlavors {
libre {
@@ -85,12 +95,24 @@ android {
}
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
}
buildFeatures {
buildConfig true
}
packaging {
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
}
androidResources {
generateLocaleConfig true
}
}
@@ -109,6 +131,7 @@ dependencies {
implementation 'androidx.media:media:1.6.0'
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:$android_core_version"
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
implementation 'androidx.fragment:fragment-ktx:1.6.0'
implementation "com.google.android.material:material:$android_material_version"
// Token auto complete
@@ -120,7 +143,7 @@ dependencies {
// Autofill
implementation "androidx.autofill:autofill:1.1.0"
// Time
implementation 'joda-time:joda-time:2.10.13'
implementation 'joda-time:joda-time:2.13.0'
// Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
// Education
@@ -131,11 +154,13 @@ dependencies {
// Password generator
implementation 'me.gosimple:nbvcxz:1.5.0'
// Credentials Provider
implementation "androidx.credentials:credentials:1.2.2"
// Modules import
implementation project(path: ':database')
implementation project(path: ':icon-pack')
// Tests
androidTestImplementation "androidx.test:runner:$android_test_version"
androidTestImplementation "androidx.test:rules:$android_test_version"
}

View File

@@ -0,0 +1,96 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "a20aec7cf09664b1102ec659fa51160a",
"entities": [
{
"tableName": "file_database_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `read_only` INTEGER, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "databaseAlias",
"columnName": "database_alias",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "keyFileUri",
"columnName": "keyfile_uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "hardwareKey",
"columnName": "hardware_key",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "readOnly",
"columnName": "read_only",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "updated",
"columnName": "updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"database_uri"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "cipher_database",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "encryptedValue",
"columnName": "encrypted_value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "specParameters",
"columnName": "specs_parameters",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"database_uri"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a20aec7cf09664b1102ec659fa51160a')"
]
}
}

View File

@@ -0,0 +1,76 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "io.github.forkmaintainers.iceraven",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.chromium.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A8:56:48:50:79:BC:B3:57:BF:BE:69:BA:19:A9:BA:43:CD:0A:D9:AB:22:67:52:C7:80:B6:88:8A:FD:48:21:6B"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.cromite.cromite",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "63:3F:A4:1D:82:11:D6:D0:91:6A:81:9B:89:66:8C:6D:E9:2E:64:23:2D:A6:7F:9D:16:FD:81:C3:B7:E9:23:FF"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.ironfoxoss.ironfox",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.ironfoxoss.ironfox.nightly",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fennec_fdroid",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "06:66:53:58:EF:D8:BA:05:BE:23:6A:47:A1:2C:B0:95:8D:7D:75:DD:93:9D:77:C2:B3:1F:53:98:53:7E:BD:C5"
}
]
}
}
]
}

View File

@@ -0,0 +1,820 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "com.android.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.chrome.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C"
},
{
"build": "release",
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.chrome.dev",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
},
{
"build": "release",
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.chrome.canary",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.chromium.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.google.android.apps.chrome",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fennec_webauthndebug",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.firefox",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.firefox_beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.focus",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fennec_aurora",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.rocket",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fenix",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "50:04:77:90:88:E7:F9:88:D5:BC:5C:C5:F8:79:8F:EB:F4:F8:CD:08:4A:1B:2A:46:EF:D4:C8:EE:4A:EA:F2:11"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fenix.debug",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.focus.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.focus.nightly",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.klar",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.reference.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "B0:09:90:E3:0F:9D:81:5D:2E:BC:7B:9B:B2:21:CE:47:E5:C9:D5:17:AA:C7:0E:7F:D5:95:B1:E5:3E:9A:4B:14"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx.canary",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx.dev",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx.rolling",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx.local",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.brave.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.brave.browser_beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.brave.browser_nightly",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "app.vanadium.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.vivaldi.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.vivaldi.browser.snapshot",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.vivaldi.browser.sopranos",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.citrix.Receiver",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "3D:D1:12:67:10:69:AB:36:4E:F9:BE:73:9A:B7:B5:EE:15:E1:CD:E9:D8:75:7B:1B:F0:64:F5:0C:55:68:9A:49"
},
{
"build": "release",
"cert_fingerprint_sha256": "CE:B2:23:D7:77:09:F2:B6:BC:0B:3A:78:36:F5:A5:AF:4C:E1:D3:55:F4:A7:28:86:F7:9D:F8:0D:C9:D6:12:2E"
},
{
"build": "release",
"cert_fingerprint_sha256": "AA:D0:D4:57:E6:33:C3:78:25:77:30:5B:C1:B2:D9:E3:81:41:C7:21:DF:0D:AA:6E:29:07:2F:C4:1D:34:F0:AB"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.android.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C9:00:9D:01:EB:F9:F5:D0:30:2B:C7:1B:2F:E9:AA:9A:47:A4:32:BB:A1:73:08:A3:11:1B:75:D7:B2:14:90:25"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.sec.android.app.sbrowser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
},
{
"build": "release",
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.sec.android.app.sbrowser.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
},
{
"build": "release",
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.google.android.gms",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "7C:E8:3C:1B:71:F3:D5:72:FE:D0:4C:8D:40:C5:CB:10:FF:75:E6:D8:7D:9D:F6:FB:D5:3F:04:68:C2:90:50:53"
},
{
"build": "release",
"cert_fingerprint_sha256": "D2:2C:C5:00:29:9F:B2:28:73:A0:1A:01:0D:E1:C8:2F:BE:4D:06:11:19:B9:48:14:DD:30:1D:AB:50:CB:76:78"
},
{
"build": "release",
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
},
{
"build": "release",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser.alpha",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser.corp",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser.canary",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser.broteam",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.talonsec.talon",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A3:66:03:44:A6:F6:AF:CA:81:8C:BF:43:96:A2:3C:CF:D5:ED:7A:78:1B:B4:A3:D1:85:03:01:E2:F4:6D:23:83"
},
{
"build": "release",
"cert_fingerprint_sha256": "E2:A5:64:74:EA:23:7B:06:67:B6:F5:2C:DC:E9:04:5E:24:88:3B:AE:D0:82:59:9A:A2:DF:0B:60:3A:CF:6A:3B"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.talonsec.talon_beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "F5:86:62:7A:32:C8:9F:E6:7E:00:6D:B1:8C:34:31:9E:01:7F:B3:B2:BE:D6:9D:01:01:B7:F9:43:E7:7C:48:AE"
},
{
"build": "release",
"cert_fingerprint_sha256": "9A:A1:25:D5:E5:5E:3F:B0:DE:96:72:D9:A9:5D:04:65:3F:49:4A:1E:C3:EE:76:1E:94:C4:4E:5D:2F:65:8E:2F"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.duckduckgo.mobile.android.debug",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C4:F0:9E:2B:D7:25:AD:F5:AD:92:0B:A2:80:27:66:AC:16:4A:C1:53:B3:EA:9E:08:48:B0:57:98:37:F7:6A:29"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.duckduckgo.mobile.android",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BB:7B:B3:1C:57:3C:46:A1:DA:7F:C5:C5:28:A6:AC:F4:32:10:84:56:FE:EC:50:81:0C:7F:33:69:4E:B3:D2:D4"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.naver.whale",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "0B:8B:85:23:BB:4A:EF:FA:34:6E:4B:DD:4F:BF:7D:19:34:50:56:9A:A1:4A:AA:D4:AD:FD:94:A3:F7:B2:27:BB"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.fido.fido2client",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "FC:98:DA:E6:3A:D3:96:26:C8:C6:7F:BE:83:F2:F0:6F:74:93:2A:9C:D1:46:B9:2C:EC:FC:6A:04:7A:90:43:86"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.heytap.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AF:F8:A7:49:CF:0E:7D:75:44:65:D0:FB:FA:7B:8D:0C:64:5E:22:5C:10:C6:E2:32:AD:A0:D9:74:88:36:B8:E5"
},
{
"build": "release",
"cert_fingerprint_sha256": "A8:FE:A4:CA:FB:93:32:DA:26:B8:E6:81:08:17:C1:DA:90:A5:03:0E:35:A6:0A:79:E0:6C:90:97:AA:C6:A4:42"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.Island",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "D9:C3:39:AC:9C:3A:EE:E1:75:1D:85:8C:35:D9:BA:C5:CC:87:B3:CE:76:30:93:F0:F5:10:64:F5:A2:F6:9B:04"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.IslandCanary",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "90:17:13:23:45:6E:6F:39:CB:FD:CF:B2:56:BE:1D:CF:F3:BC:1C:59:8A:15:93:30:E4:97:73:D0:4C:B9:C9:05"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.IslandBeta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "35:31:83:1A:9E:2B:21:1D:E6:AA:C3:69:4B:45:83:6E:56:09:B9:D7:D0:04:C3:1B:21:87:40:FB:77:17:38:D1"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.IslandDev",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.island.intune",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C2:38:24:15:41:20:A0:8F:C3:95:42:AC:D8:2A:E9:24:94:78:80:1E:47:FD:6C:66:2B:18:1C:28:CA:7E:59:4E"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.island.canary.intune",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "1E:16:74:BB:79:EA:09:FB:37:CF:9F:1B:07:1B:1D:51:8D:46:03:0E:D3:EE:F2:C1:4E:AD:93:9E:C6:EE:3A:4C"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.island.beta.intune",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "D2:5E:AD:F6:1C:E6:36:6C:A4:23:A4:7F:C4:DB:9B:8C:9C:8A:35:B4:B0:19:E8:D9:82:FB:D0:8A:D9:DB:49:5A"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.island.dev.intune",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "net.quetta.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "F1:38:00:4F:38:04:51:D4:8A:05:2B:B3:A3:EF:17:24:23:D4:B0:D0:C8:A3:AA:DD:FB:DB:66:30:31:48:EC:A4"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "cz.seznam.sbrowser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "DB:95:40:66:10:78:83:6E:4E:B1:66:F6:9E:F4:07:30:9E:8D:AE:33:34:68:5E:C8:F6:FA:2F:13:81:B9:AC:F6"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.opera.mini.native",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.opera.mini.native.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
}
]
}
}
]
}

View File

@@ -9,6 +9,10 @@
android:anyDensity="true" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission
android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
@@ -39,8 +43,10 @@
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
android:largeHeap="true"
android:resizeableActivity="true"
android:supportsRtl="true"
android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="s">
tools:targetApi="s"
tools:ignore="CredentialDependency">
<meta-data
android:name="com.google.android.backup.api_key"
android:value="${googleAndroidBackupAPIKey}" />
@@ -118,7 +124,7 @@
android:name="com.kunzisoft.keepass.activities.GroupActivity"
android:exported="false"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustPan">
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="android.app.default_searchable"
android:value="com.kunzisoft.keepass.search.SearchResults"
@@ -145,7 +151,7 @@
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
android:windowSoftInputMode="adjustPan" />
android:windowSoftInputMode="adjustResize" />
<!-- About and Settings -->
<activity
android:name="com.kunzisoft.keepass.activities.AboutActivity"
@@ -154,24 +160,40 @@
<activity
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:excludeFromRecents="true"/>
android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity" />
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
android:label="@string/keyboard_setting_label"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<activity
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity"
tools:targetApi="26" />
<activity
android:name="com.kunzisoft.keepass.settings.PasskeysSettingsActivity"
tools:targetApi="34" />
<activity
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
android:theme="@style/Theme.Transparent" />
android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
android:theme="@style/Theme.Transparent"
android:exported="false"
android:excludeFromRecents="true" />
<activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:exported="false"
android:excludeFromRecents="true" />
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent"
android:launchMode="singleInstance"
android:exported="true">
android:exported="true"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
@@ -187,34 +209,41 @@
</intent-filter>
</activity>
<activity
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
android:label="@string/keyboard_setting_label"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:exported="false"
android:excludeFromRecents="true"
tools:targetApi="upside_down_cake" />
<service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
android:foregroundServiceType="dataSync"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
android:foregroundServiceType="dataSync"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.DeviceUnlockNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<!-- Receiver for Autofill -->
<service
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
android:label="@string/autofill_service_name"
android:name="com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService"
android:label="@string/app_name"
android:exported="true"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<meta-data
@@ -225,7 +254,7 @@
</intent-filter>
</service>
<service
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
android:name="com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService"
android:label="@string/keyboard_label"
android:exported="true"
android:permission="android.permission.BIND_INPUT_METHOD" >
@@ -236,9 +265,21 @@
</intent-filter>
</service>
<service
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:name="com.kunzisoft.keepass.credentialprovider.passkey.PasskeyProviderService"
android:enabled="true"
android:exported="false" />
android:exported="true"
android:label="@string/passkey_service_name"
android:icon="@mipmap/ic_launcher"
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
tools:targetApi="upside_down_cake">
<intent-filter>
<action android:name="android.service.credentials.CredentialProviderService" />
</intent-filter>
<meta-data
android:name="android.credentials.provider"
android:resource="@xml/provider" />
</service>
<receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true">

View File

@@ -38,7 +38,11 @@ import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.util.TypedValue
import android.view.*
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
@@ -202,7 +206,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
override fun onDown(e: MotionEvent): Boolean = true
override fun onScroll(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
@@ -220,7 +224,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
}
override fun onFling(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
@@ -355,8 +359,6 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
isViewTranslateAnimationRunning = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
imageView.run {
val translationY = if (velY > 0) {
originalViewBounds.top + height - top
@@ -392,41 +394,6 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
}
})
}
} else {
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) {
originalViewBounds.top + imageView.height - imageView.top
} else {
originalViewBounds.top - imageView.height - imageView.top
}.toFloat()).apply {
duration = dismissAnimationDuration
interpolator = dismissAnimationInterpolator
addUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(imageView, amount)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
// no op
}
override fun onAnimationEnd(p0: Animator) {
isViewTranslateAnimationRunning = false
onViewTranslateListener?.onDismiss(imageView)
cleanup()
}
override fun onAnimationCancel(p0: Animator) {
isViewTranslateAnimationRunning = false
}
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
start()
}
}
}
private fun processFlingBitmap(velocityX: Float, velocityY: Float) {
@@ -653,8 +620,6 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
private fun restoreViewTransform() {
val imageView = imageViewRef.get() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
imageView.run {
animate()
.setDuration(restoreAnimationDuration)
@@ -683,41 +648,12 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
}
})
}
} else {
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, (originalViewBounds.top - imageView.top).toFloat()).apply {
duration = restoreAnimationDuration
interpolator = restoreAnimationInterpolator
addUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(imageView, amount)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
// no op
}
override fun onAnimationEnd(p0: Animator) {
onViewTranslateListener?.onRestore(imageView)
}
override fun onAnimationCancel(p0: Animator) {
// no op
}
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
start()
}
}
}
private fun startDragToDismissAnimation() {
val imageView = imageViewRef.get() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
imageView.run {
val translationY = if (y - initialY > 0) {
originalViewBounds.top + height - top
@@ -753,37 +689,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
}
})
}
} else {
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY).apply {
duration = dismissAnimationDuration
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(imageView, amount)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
isViewTranslateAnimationRunning = true
}
override fun onAnimationEnd(p0: Animator) {
isViewTranslateAnimationRunning = false
onViewTranslateListener?.onDismiss(imageView)
cleanup()
}
override fun onAnimationCancel(p0: Animator) {
isViewTranslateAnimationRunning = false
}
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
start()
}
}
}
private fun processFlingToDismiss(velocityY: Float) {

View File

@@ -24,13 +24,14 @@ import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.core.text.HtmlCompat
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.getPackageInfoCompat
import org.joda.time.DateTime
@@ -56,7 +57,7 @@ class AboutActivity : StylishActivity() {
var version: String
var build: String
try {
version = packageManager.getPackageInfoCompat(packageName).versionName
version = packageManager.getPackageInfoCompat(packageName).versionName ?: ""
build = BuildConfig.BUILD_VERSION
} catch (e: NameNotFoundException) {
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
@@ -76,6 +77,8 @@ class AboutActivity : StylishActivity() {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
HtmlCompat.FROM_HTML_MODE_LEGACY)
textDirection = View.TEXT_DIRECTION_ANY_RTL
}
findViewById<TextView>(R.id.activity_about_privacy_text).apply {

View File

@@ -1,269 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.WebDomain
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() {
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this, true)
else null
override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
// Retrieve selection mode
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) {
SpecialMode.SELECTION -> {
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
// To pass extra inline request
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
}
// Build search param
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
WebDomain.getConcreteWebDomain(
this,
searchInfo.webDomain
) { concreteWebDomain ->
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper
.retrieveAutofillComponent(intent)
?.assistStructure
val newAutofillComponent = if (assistStructure != null) {
AutofillComponent(
assistStructure,
compatInlineSuggestionsRequest
)
} else {
null
}
searchInfo.webDomain = concreteWebDomain
launchSelection(database, newAutofillComponent, searchInfo)
}
}
}
// Remove bundle
intent.removeExtra(KEY_SELECTION_BUNDLE)
}
SpecialMode.REGISTRATION -> {
// To register info
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
val searchInfo = SearchInfo(registerInfo?.searchInfo)
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchRegistration(database, searchInfo, registerInfo)
}
}
else -> {
// Not an autofill call
setResult(Activity.RESULT_CANCELED)
finish()
}
}
}
}
private fun launchSelection(database: ContextualDatabase?,
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo) {
if (autofillComponent == null) {
setResult(Activity.RESULT_CANCELED)
finish()
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
PreferencesUtil.webDomainBlocklist(this))) {
showBlockRestartMessage()
setResult(Activity.RESULT_CANCELED)
finish()
} else {
// If database is open
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
// Items found
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
finish()
},
{ openedDatabase ->
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
openedDatabase,
mAutofillActivityResultLauncher,
autofillComponent,
searchInfo,
false)
},
{
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this,
mAutofillActivityResultLauncher,
autofillComponent,
searchInfo)
}
)
}
}
private fun launchRegistration(database: ContextualDatabase?,
searchInfo: SearchInfo,
registerInfo: RegisterInfo?) {
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
PreferencesUtil.webDomainBlocklist(this))) {
showBlockRestartMessage()
setResult(Activity.RESULT_CANCELED)
} else {
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, _ ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
openedDatabase,
registerInfo)
} else {
showReadOnlySaveMessage()
}
},
{ openedDatabase ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(this,
openedDatabase,
registerInfo)
} else {
showReadOnlySaveMessage()
}
},
{
// If database not open
FileDatabaseSelectActivity.launchForRegistration(this,
registerInfo)
}
)
}
finish()
}
private fun showBlockRestartMessage() {
// If item not allowed, show a toast
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
}
private fun showReadOnlySaveMessage() {
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
}
companion object {
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
return PendingIntent.getActivity(context, 0,
// Doesn't work with direct extra Parcelable (don't know why?)
// Wrap into a bundle to bypass the problem
Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
}
})
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
}
fun getPendingIntentForRegistration(context: Context,
registerInfo: RegisterInfo): PendingIntent {
return PendingIntent.getActivity(context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
}
fun launchForRegistration(context: Context,
registerInfo: RegisterInfo) {
val intent = Intent(context, AutofillLauncherActivity::class.java)
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
}

View File

@@ -23,7 +23,6 @@ import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -38,13 +37,10 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
@@ -54,15 +50,15 @@ import com.google.android.material.tabs.TabLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.EntryFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TagsAdapter
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
@@ -73,7 +69,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.WindowInsetPosition
import com.kunzisoft.keepass.view.applyWindowInsets
@@ -83,11 +79,13 @@ import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.EnumSet
import java.util.UUID
class EntryActivity : DatabaseLockActivity() {
private var footer: ViewGroup? = null
private var container: View? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null
@@ -127,6 +125,8 @@ class EntryActivity : DatabaseLockActivity() {
private var mBackgroundColor: Int? = null
private var mForegroundColor: Int? = null
override fun manageDatabaseInfo(): Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -139,6 +139,7 @@ class EntryActivity : DatabaseLockActivity() {
// Get views
footer = findViewById(R.id.activity_entry_footer)
container = findViewById(R.id.activity_entry_container)
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar)
@@ -154,8 +155,12 @@ class EntryActivity : DatabaseLockActivity() {
setTransparentNavigationBar {
// To fix margin with API 27
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
container?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}
// Empty title
@@ -261,7 +266,7 @@ class EntryActivity : DatabaseLockActivity() {
mIcon = entryInfo.icon
// Assign title text
val entryTitle =
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
entryInfo.title.ifEmpty { entryInfo.id.asHexString() }
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
// Assign tags
@@ -309,11 +314,11 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.historySelected.observe(this) { historySelected ->
mDatabase?.let { database ->
launch(
this,
database,
historySelected.nodeId,
historySelected.historyPosition,
mEntryActivityResultLauncher
activity = this,
database = database,
entryId = historySelected.nodeId,
historyPosition = historySelected.historyPosition,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
@@ -327,9 +332,8 @@ class EntryActivity : DatabaseLockActivity() {
return coordinatorLayout
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database)
}
@@ -475,11 +479,12 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_edit -> {
mDatabase?.let { database ->
mMainEntryId?.let { entryId ->
EntryEditActivity.launchToUpdate(
this,
database,
entryId,
mEntryActivityResultLauncher
EntryEditActivity.launch(
activity = this,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
@@ -517,7 +522,7 @@ class EntryActivity : DatabaseLockActivity() {
// Transit data in previous Activity after an update
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(Activity.RESULT_OK, this)
setResult(RESULT_OK, this)
}
super.finish()
}
@@ -531,34 +536,22 @@ class EntryActivity : DatabaseLockActivity() {
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
/**
* Open standard Entry activity
* Open standard or history Entry activity
*/
fun launch(activity: Activity,
fun launch(
activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
activityResultLauncher.launch(intent)
}
}
}
/**
* Open history Entry activity
*/
fun launch(activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
historyPosition: Int,
activityResultLauncher: ActivityResultLauncher<Intent>) {
historyPosition: Int? = null,
activityResultLauncher: ActivityResultLauncher<Intent>
) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
historyPosition?.let {
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
}
activityResultLauncher.launch(intent)
}
}

View File

@@ -30,21 +30,20 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ProgressBar
import android.widget.Spinner
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker
@@ -56,37 +55,40 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.TimeUtil.datePickerToDataDate
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.ToolbarAction
@@ -96,9 +98,11 @@ import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import kotlinx.coroutines.launch
import java.util.EnumSet
import java.util.UUID
class EntryEditActivity : DatabaseLockActivity(),
@@ -108,8 +112,8 @@ class EntryEditActivity : DatabaseLockActivity(),
ReplaceFileDialogFragment.ActionChooseListener {
// Views
private var footer: ViewGroup? = null
private var container: ViewGroup? = null
private var footer: View? = null
private var container: View? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: NestedScrollView? = null
private var templateSelectorSpinner: Spinner? = null
@@ -153,8 +157,7 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
// To ask data lost only one time
private var backPressedAlreadyApproved = false
override fun manageDatabaseInfo(): Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -179,8 +182,12 @@ class EntryEditActivity : DatabaseLockActivity(),
// To apply fit window with transparency
setTransparentNavigationBar(applyToStatusBar = true) {
container?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
container?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
@@ -204,8 +211,8 @@ class EntryEditActivity : DatabaseLockActivity(),
mDatabase,
entryId,
parentId,
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
intent.retrieveRegisterInfo()
?: intent.retrieveSearchInfo()?.toRegisterInfo()
)
// To retrieve attachment
@@ -301,7 +308,7 @@ class EntryEditActivity : DatabaseLockActivity(),
// Launch the time picker
MaterialTimePicker.Builder().build().apply {
addOnPositiveButtonClickListener {
mEntryEditViewModel.selectTime(this.hour, this.minute)
mEntryEditViewModel.selectTime(DataTime(this.hour, this.minute))
}
show(supportFragmentManager, "TimePickerFragment")
}
@@ -309,7 +316,7 @@ class EntryEditActivity : DatabaseLockActivity(),
// Launch the date picker
MaterialDatePicker.Builder.datePicker().build().apply {
addOnPositiveButtonClickListener {
mEntryEditViewModel.selectDate(it)
mEntryEditViewModel.selectDate(datePickerToDataDate(it))
}
show(supportFragmentManager, "DatePickerFragment")
}
@@ -372,23 +379,30 @@ class EntryEditActivity : DatabaseLockActivity(),
} ?: run {
updateEntry(entrySave.oldEntry, entrySave.newEntry)
}
// Don't wait for saving if it's to provide autofill
mDatabase?.let { database ->
EntrySelectionHelper.doSpecialAction(intent,
{},
{},
{},
{
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
},
{ _, _ ->
entryValidatedForAutofillSelection(database, entrySave.newEntry)
},
{
entryValidatedForAutofillRegistration(entrySave.newEntry)
}
)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mEntryEditViewModel.uiState.collect { uiState ->
when (uiState) {
EntryEditViewModel.UIState.Loading -> {}
EntryEditViewModel.UIState.ShowOverwriteMessage -> {
if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
AlertDialog.Builder(this@EntryEditActivity)
.setTitle(R.string.warning_overwrite_data_title)
.setMessage(R.string.warning_overwrite_data_description)
.setNegativeButton(android.R.string.cancel) { _, _ ->
mEntryEditViewModel.backPressedAlreadyApproved = true
onCancelSpecialMode()
}
.setPositiveButton(android.R.string.ok) { _, _ ->
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
}
.create().show()
}
}
}
}
}
}
}
@@ -401,13 +415,13 @@ class EntryEditActivity : DatabaseLockActivity(),
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mAllowCustomFields = database?.allowEntryCustomFields() == true
mAllowOTP = database?.allowOTP == true
mEntryEditViewModel.loadDatabase(database)
mAllowCustomFields = database.allowEntryCustomFields() == true
mAllowOTP = database.allowOTP == true
mEntryEditViewModel.loadTemplateEntry(database)
mTemplatesSelectorAdapter?.apply {
iconDrawableFactory = mDatabase?.iconDrawableFactory
iconDrawableFactory = database.iconDrawableFactory
notifyDataSetChanged()
}
}
@@ -418,41 +432,47 @@ class EntryEditActivity : DatabaseLockActivity(),
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
mEntryEditViewModel.unlockAction()
when (actionTask) {
ACTION_DATABASE_CREATE_ENTRY_TASK,
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
try {
if (result.isSuccess) {
var newNodes: List<Node> = ArrayList()
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
}
if (newNodes.size == 1) {
(newNodes[0] as? Entry?)?.let { entry ->
EntrySelectionHelper.doSpecialAction(intent,
{
result.data?.getNewEntry(database)?.let { entry ->
EntrySelectionHelper.doSpecialAction(
intent = intent,
defaultAction = {
// Finish naturally
finishForEntryResult(entry)
},
{
searchAction = {
// Nothing when search retrieved
},
{
entryValidatedForSave(entry)
},
{
selectionAction = { intentSender, typeMode, searchInfo ->
when(typeMode) {
TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD ->
entryValidatedForKeyboardSelection(database, entry)
TypeMode.PASSKEY ->
entryValidatedForPasskey(database, entry)
TypeMode.AUTOFILL ->
entryValidatedForAutofill(database, entry)
}
},
{ _, _ ->
entryValidatedForAutofillSelection(database, entry)
},
{
entryValidatedForAutofillRegistration(entry)
registrationAction = { _, typeMode, _ ->
when(typeMode) {
TypeMode.DEFAULT ->
entryValidatedForSave(entry)
TypeMode.MAGIKEYBOARD -> {}
TypeMode.PASSKEY ->
entryValidatedForPasskey(database, entry)
TypeMode.AUTOFILL ->
entryValidatedForAutofill(database, entry)
}
}
)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry after database action", e)
}
@@ -467,29 +487,33 @@ class EntryEditActivity : DatabaseLockActivity(),
}
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
// Populate Magikeyboard with entry
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
this,
entry.getEntryInfo(database)
// Build Magikeyboard response with the entry selected
this.buildSpecialModeResponseAndSetResult(
entryInfo = entry.getEntryInfo(database),
extras = buildEntryResult(entry)
)
onValidateSpecialMode()
// Don't keep activity history for entry edition
finishForEntryResult(entry)
}
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
private fun entryValidatedForAutofill(database: ContextualDatabase, entry: Entry) {
// Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
database,
entry.getEntryInfo(database))
this.buildSpecialModeResponseAndSetResult(
entryInfo = entry.getEntryInfo(database),
extras = buildEntryResult(entry)
)
}
onValidateSpecialMode()
}
private fun entryValidatedForAutofillRegistration(entry: Entry) {
private fun entryValidatedForPasskey(database: ContextualDatabase, entry: Entry) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
this.buildPasskeyResponseAndSetResult(
entryInfo = entry.getEntryInfo(database),
extras = buildEntryResult(entry) // To update the previous screen
)
}
onValidateSpecialMode()
finishForEntryResult(entry)
}
override fun onResume() {
@@ -502,7 +526,7 @@ class EntryEditActivity : DatabaseLockActivity(),
}
// Padding if lock button visible
entryEditAddToolBar?.updateLockPaddingLeft()
entryEditAddToolBar?.updateLockPaddingStart()
mAttachmentFileBinderManager?.apply {
registerProgressTask()
@@ -603,16 +627,12 @@ class EntryEditActivity : DatabaseLockActivity(),
isVisible = isEnabled
}
menu?.findItem(R.id.menu_add_attachment)?.apply {
// Attachment not compatible below KitKat
isEnabled = !mIsTemplate
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
isVisible = isEnabled
}
menu?.findItem(R.id.menu_add_otp)?.apply {
// OTP not compatible below KitKat
isEnabled = mAllowOTP
&& !mIsTemplate
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
isVisible = isEnabled
}
return super.onPrepareOptionsMenu(menu)
@@ -727,13 +747,13 @@ class EntryEditActivity : DatabaseLockActivity(),
}
private fun onApprovedBackPressed(approved: () -> Unit) {
if (!backPressedAlreadyApproved) {
if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ ->
mAttachmentFileBinderManager?.stopUploadAllAttachments()
backPressedAlreadyApproved = true
mEntryEditViewModel.backPressedAlreadyApproved = true
approved.invoke()
}.create().show()
} else {
@@ -741,14 +761,19 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
private fun buildEntryResult(entry: Entry): Bundle {
return Bundle().apply {
putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
}
}
private fun finishForEntryResult(entry: Entry) {
// Assign entry callback as a result
try {
val bundle = Bundle()
val bundle = buildEntryResult(entry)
val intentEntry = Intent()
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
intentEntry.putExtras(bundle)
setResult(Activity.RESULT_OK, intentEntry)
setResult(RESULT_OK, intentEntry)
super.finish()
} catch (e: Exception) {
// Exception when parcelable can't be done
@@ -756,6 +781,10 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
enum class RegistrationType {
UPDATE, CREATE
}
companion object {
private val TAG = EntryEditActivity::class.java.name
@@ -765,23 +794,12 @@ class EntryEditActivity : DatabaseLockActivity(),
const val KEY_PARENT = "parent"
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
fun registerForEntryResult(fragment: Fragment,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
}
}
}
fun registerForEntryResult(activity: FragmentActivity,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
fun registerForEntryResult(
activity: FragmentActivity,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit
): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
if (result.resultCode == RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
)
@@ -792,151 +810,78 @@ class EntryEditActivity : DatabaseLockActivity(),
}
/**
* Launch EntryEditActivity to update an existing entry by his [entryId]
* Launch EntryEditActivity to update an existing entry or to add a new entry in an existing group
*/
fun launchToUpdate(activity: Activity,
fun launch(
activity: Activity,
database: ContextualDatabase,
entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
registrationType: RegistrationType,
nodeId: NodeId<*>,
activityResultLauncher: ActivityResultLauncher<Intent>
) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
when (registrationType) {
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
}
activityResultLauncher.launch(intent)
}
}
}
/**
* Launch EntryEditActivity to add a new entry in an existent group
* Launch EntryEditActivity to add a new entry in special selection
*/
fun launchToCreate(activity: Activity,
fun launchForSelection(
context: Context,
database: ContextualDatabase,
typeMode: TypeMode,
groupId: NodeId<*>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
activityResultLauncher.launch(intent)
}
}
}
fun launchToUpdateForSave(context: Context,
database: ContextualDatabase,
entryId: NodeId<UUID>,
searchInfo: SearchInfo) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
EntrySelectionHelper.startActivityForSaveModeResult(
context,
intent,
searchInfo
)
}
}
}
fun launchToCreateForSave(context: Context,
database: ContextualDatabase,
groupId: NodeId<*>,
searchInfo: SearchInfo) {
searchInfo: SearchInfo? = null,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForSaveModeResult(
context,
intent,
searchInfo
EntrySelectionHelper.startActivityForSelectionModeResult(
context = context,
intent = intent,
typeMode = typeMode,
searchInfo = searchInfo,
activityResultLauncher = activityResultLauncher
)
}
}
}
/**
* Launch EntryEditActivity to add a new entry in keyboard selection
* Launch EntryEditActivity to update an updated entry or register a new entry (from autofill)
*/
fun launchForKeyboardSelectionResult(context: Context,
fun launchForRegistration(
context: Context,
database: ContextualDatabase,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null) {
nodeId: NodeId<*>,
registerInfo: RegisterInfo? = null,
typeMode: TypeMode,
registrationType: RegistrationType,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
when (registrationType) {
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
}
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
intent,
searchInfo
)
}
}
}
/**
* Launch EntryEditActivity to add a new entry in autofill selection
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: AppCompatActivity,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
activityResultLauncher,
autofillComponent,
searchInfo
)
}
}
}
/**
* Launch EntryEditActivity to register an updated entry (from autofill)
*/
fun launchToUpdateForRegistration(context: Context,
database: ContextualDatabase,
entryId: NodeId<UUID>,
registerInfo: RegisterInfo? = null) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
intent,
registerInfo
)
}
}
}
/**
* Launch EntryEditActivity to register a new entry (from autofill)
*/
fun launchToCreateForRegistration(context: Context,
database: ContextualDatabase,
groupId: NodeId<*>,
registerInfo: RegisterInfo? = null) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
intent,
registerInfo
registerInfo,
typeMode
)
}
}

View File

@@ -1,226 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.WebDomain
/**
* Activity to search or select entry in database,
* Commonly used with Magikeyboard
*/
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return false
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
if (keySelectionBundle != null) {
// To manage package name
var searchInfo = SearchInfo()
keySelectionBundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
searchInfo = mSearchInfo
}
launch(database, searchInfo)
} else {
// To manage share
var sharedWebDomain: String? = null
var otpString: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if ("text/plain" == intent.type) {
// Retrieve web domain or OTP
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
sharedWebDomain = Uri.parse(extra).host
}
}
launchSelection(database, sharedWebDomain, otpString)
}
Intent.ACTION_VIEW -> {
// Retrieve OTP
intent.dataString?.let { extra ->
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
}
launchSelection(database, sharedWebDomain, otpString)
}
else -> {
if (database != null) {
GroupActivity.launch(this, database)
} else {
FileDatabaseSelectActivity.launch(this)
}
}
}
}
finish()
}
private fun launchSelection(database: ContextualDatabase?,
sharedWebDomain: String?,
otpString: String?) {
// Build domain search param
val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain
this.otpString = otpString
}
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
}
}
private fun launch(database: ContextualDatabase?,
searchInfo: SearchInfo) {
// Setting to integrate Magikeyboard
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
// If database is open
val readOnly = database?.isReadOnly != false
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
// Items found
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (searchShareForMagikeyboard) {
MagikeyboardService.performSelection(
items,
{ entryInfo ->
// Automatically populate keyboard
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
this,
entryInfo
)
},
{ autoSearch ->
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
autoSearch)
}
)
} else {
GroupActivity.launchForSearchResult(this,
openedDatabase,
searchInfo,
true)
}
},
{ openedDatabase ->
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(this,
openedDatabase,
searchInfo,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
}
} else if (searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(this,
openedDatabase,
searchInfo,
false)
} else {
GroupActivity.launchForSearchResult(this,
openedDatabase,
searchInfo,
false)
}
},
{
// If database not open
if (searchInfo.otpString != null) {
FileDatabaseSelectActivity.launchForSaveResult(this,
searchInfo)
} else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
searchInfo)
} else {
FileDatabaseSelectActivity.launchForSearchResult(this,
searchInfo)
}
}
)
}
companion object {
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
fun launch(context: Context,
searchInfo: SearchInfo? = null) {
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
})
}
// New task needed because don't launch from an Activity context
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
}
}

View File

@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -33,8 +32,6 @@ import android.view.MenuItem
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
@@ -44,35 +41,30 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.parseUri
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.utils.allowCreateDocumentByStorageAccessFramework
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
@@ -100,10 +92,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun manageDatabaseInfo(): Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -134,7 +123,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let {
launchPasswordActivityWithPath(uri)
launchMainCredentialActivityWithPath(uri)
}
}
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
@@ -163,7 +152,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity(
launchMainCredentialActivity(
databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey
@@ -180,17 +169,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
// Load default database if not an orientation change
if (!(savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_STAY)
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
databasePath?.parseUri()?.let { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
} ?: run {
Log.i(TAG, "No default database to prepare")
}
// Load default database the first time
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
launchMainCredentialActivityWithPath(databaseFileUri)
}
// Retrieve the database URI provided by file manager after an orientation change
@@ -233,54 +214,39 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Retrieve settings for default database
mAdapterDatabaseHistory?.setDefaultDatabase(it)
}
// Remove all the remember locations if needed
if (PreferencesUtil.rememberDatabaseLocations(applicationContext).not()) {
FileDatabaseHistoryAction.getInstance(applicationContext)
.deleteAll()
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
if (database != null) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
launchGroupActivityIfLoaded(database)
}
}
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
if (result.isSuccess) {
// Update list
when (actionTask) {
ACTION_DATABASE_CREATE_TASK,
ACTION_DATABASE_LOAD_TASK -> {
result.data?.getParcelableCompat<Uri>(DATABASE_URI_KEY)?.let { databaseUri ->
val mainCredential =
result.data?.getParcelableCompat(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
?: MainCredential()
databaseFilesViewModel.addDatabaseFile(
databaseUri,
mainCredential.keyFileUri,
mainCredential.hardwareKey
)
}
}
}
// Launch activity
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
GroupActivity.launch(
this@FileDatabaseSelectActivity,
database,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
false
)
coordinatorLayout.showActionErrorIfNeeded(result)
}
ACTION_DATABASE_LOAD_TASK -> {
launchGroupActivityIfLoaded(database)
}
}
}
coordinatorLayout.showActionErrorIfNeeded(result)
}
/**
@@ -298,17 +264,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
}
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
MainCredentialActivity.launch(this,
databaseUri,
keyFile,
hardwareKey,
{ exception ->
fileNoFoundAction(exception)
private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
try {
EntrySelectionHelper.doSpecialAction(
intent = this.intent,
defaultAction = {
MainCredentialActivity.launch(
activity = this,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey
)
},
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher)
searchAction = { searchInfo ->
MainCredentialActivity.launchForSearchResult(
activity = this,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
selectionAction = { intentSenderMode, typeMode, searchInfo ->
MainCredentialActivity.launchForSelection(
activity = this,
activityResultLauncher = if (intentSenderMode)
mCredentialActivityResultLauncher else null,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = typeMode,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
registrationAction = { intentSenderMode, typeMode, registerInfo ->
MainCredentialActivity.launchForRegistration(
activity = this,
activityResultLauncher = if (intentSenderMode)
mCredentialActivityResultLauncher else null,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = typeMode,
registerInfo = registerInfo
)
onLaunchActivitySpecialMode()
}
)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
}
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
@@ -318,13 +325,15 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher)
mCredentialActivityResultLauncher
)
}
}
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null, null)
private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
launchMainCredentialActivity(databaseUri, null, null)
// Delete flickering for kitkat <=
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
overridePendingTransition(0, 0)
}
@@ -338,13 +347,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Show open and create button or special mode
when (mSpecialMode) {
SpecialMode.DEFAULT -> {
if (packageManager.allowCreateDocumentByStorageAccessFramework()) {
// There is an activity which can handle this intent.
createDatabaseButtonView?.visibility = View.VISIBLE
} else{
// No Activity found that can handle this intent.
createDatabaseButtonView?.visibility = View.GONE
}
}
else -> {
// Disable create button if in selection mode or request for autofill
@@ -352,10 +355,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
}
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
// Show recent files if allowed
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
databaseFilesViewModel.loadListOfDatabases()
@@ -366,8 +365,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// only to keep the current activity
outState.putBoolean(EXTRA_STAY, true)
// to retrieve the URI of a created database after an orientation change
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
}
@@ -376,7 +373,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
try {
mDatabaseFileUri?.let { databaseUri ->
// Create the new database
createDatabase(databaseUri, mainCredential)
mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
}
} catch (e: Exception) {
val error = getString(R.string.error_create_database_file)
@@ -442,7 +439,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
companion object {
private const val TAG = "FileDbSelectActivity"
private const val EXTRA_STAY = "EXTRA_STAY"
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
/*
@@ -461,55 +457,36 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* -------------------------
*/
fun launchForSearchResult(context: Context,
searchInfo: SearchInfo) {
EntrySelectionHelper.startActivityForSearchModeResult(context,
Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo)
fun launchForSearch(
context: Context,
searchInfo: SearchInfo
) {
EntrySelectionHelper.startActivityForSearchModeResult(
context = context,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo = searchInfo
)
}
/*
* -------------------------
* Save Launch
* Selection Launch
* -------------------------
*/
fun launchForSaveResult(context: Context,
searchInfo: SearchInfo) {
EntrySelectionHelper.startActivityForSaveModeResult(context,
Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo)
}
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
fun launchForKeyboardSelectionResult(activity: Activity,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
searchInfo)
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: AppCompatActivity,
fun launchForSelection(
context: Context,
typeMode: TypeMode,
searchInfo: SearchInfo? = null,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) {
AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
autofillComponent,
searchInfo)
) {
EntrySelectionHelper.startActivityForSelectionModeResult(
context = context,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
searchInfo = searchInfo,
typeMode = typeMode,
activityResultLauncher = activityResultLauncher
)
}
/*
@@ -517,11 +494,19 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Registration Launch
* -------------------------
*/
fun launchForRegistration(context: Context,
registerInfo: RegisterInfo? = null) {
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
Intent(context, FileDatabaseSelectActivity::class.java),
registerInfo)
fun launchForRegistration(
context: Context,
typeMode: TypeMode,
registerInfo: RegisterInfo? = null,
activityResultLauncher: ActivityResultLauncher<Intent>?,
) {
EntrySelectionHelper.startActivityForRegistrationModeResult(
context = context,
intent = Intent(context, FileDatabaseSelectActivity::class.java),
registerInfo = registerInfo,
typeMode = typeMode,
activityResultLauncher = activityResultLauncher
)
}
}
}

View File

@@ -51,7 +51,7 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
@@ -78,6 +78,8 @@ class IconPickerActivity : DatabaseLockActivity() {
private var mExternalFileHelper: ExternalFileHelper? = null
override fun manageDatabaseInfo(): Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -174,10 +176,10 @@ class IconPickerActivity : DatabaseLockActivity() {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
if (database?.allowCustomIcons == true) {
if (database.allowCustomIcons) {
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else {
uploadButton.visibility = View.GONE
@@ -212,7 +214,7 @@ class IconPickerActivity : DatabaseLockActivity() {
}
// Padding if lock button visible
toolbar.updateLockPaddingLeft()
toolbar.updateLockPaddingStart()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

@@ -45,6 +45,8 @@ class ImageViewerActivity : DatabaseLockActivity() {
private lateinit var imageView: ImageView
private lateinit var progressView: View
override fun manageDatabaseInfo(): Boolean = false
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -101,7 +103,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
try {
@@ -119,7 +121,6 @@ class ImageViewerActivity : DatabaseLockActivity() {
resources.displayMetrics.heightPixels * 2
)
database?.let { database ->
BinaryDatabaseManager.loadBitmap(
database,
attachment.binaryData,
@@ -132,7 +133,6 @@ class ImageViewerActivity : DatabaseLockActivity() {
imageView.setImageBitmap(bitmapLoaded)
}
}
}
} ?: finish()
} catch (e: Exception) {
Log.e(TAG, "Unable to view the binary", e)

View File

@@ -18,7 +18,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class KeyGeneratorActivity : DatabaseLockActivity() {
@@ -28,6 +28,8 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
private lateinit var validationButton: View
private var lockView: View? = null
override fun manageDatabaseInfo(): Boolean = true
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -84,7 +86,7 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
}
// Padding if lock button visible
toolbar.updateLockPaddingLeft()
toolbar.updateLockPaddingStart()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

@@ -32,43 +32,49 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CompoundButton
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
import com.kunzisoft.keepass.biometric.deviceUnlockError
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
import com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
@@ -79,26 +85,31 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.MainCredentialView
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
import kotlinx.coroutines.launch
import java.io.FileNotFoundException
class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
class MainCredentialActivity : DatabaseModeActivity() {
// Views
private var toolbar: Toolbar? = null
private var filenameView: TextView? = null
private var logotypeButton: View? = null
private var advancedUnlockButton: View? = null
private var deviceUnlockButton: View? = null
private var mainCredentialView: MainCredentialView? = null
private var confirmButtonView: Button? = null
private var infoContainerView: ViewGroup? = null
private lateinit var coordinatorLayout: CoordinatorLayout
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private var deviceUnlockFragment: DeviceUnlockFragment? = null
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
private val mDeviceUnlockViewModel: DeviceUnlockViewModel? by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewModelProvider(this)[DeviceUnlockViewModel::class.java]
} else null
}
private val mPasswordActivityEducation = PasswordActivityEducation(this)
@@ -113,10 +124,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun manageDatabaseInfo(): Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -131,7 +139,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
filenameView = findViewById(R.id.filename)
logotypeButton = findViewById(R.id.activity_password_logotype)
advancedUnlockButton = findViewById(R.id.fragment_advanced_unlock_container_view)
deviceUnlockButton = findViewById(R.id.fragment_device_unlock_container_view)
mainCredentialView = findViewById(R.id.activity_password_credentials)
confirmButtonView = findViewById(R.id.activity_password_open_button)
infoContainerView = findViewById(R.id.activity_password_info_container)
@@ -140,7 +148,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
savedInstanceState.getBoolean(KEY_READ_ONLY)
} else {
PreferencesUtil.enableReadOnlyDatabase(this)
false
}
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
@@ -165,32 +173,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
startActivity(Intent(this, AppearanceSettingsActivity::class.java))
}
// Init Biometric elements
advancedUnlockFragment = supportFragmentManager
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
if (advancedUnlockFragment == null) {
advancedUnlockFragment = AdvancedUnlockFragment()
supportFragmentManager.commit {
replace(R.id.fragment_advanced_unlock_container_view,
advancedUnlockFragment!!,
UNLOCK_FRAGMENT_TAG)
}
}
// Listen password checkbox to init advanced unlock and confirmation button
mainCredentialView?.onPasswordChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel?.checkConditionToStoreCredential(
condition = verified
)
}
mainCredentialView?.onKeyFileChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
mainCredentialView?.onHardwareKeyChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
// TODO Async by ViewModel
enableConfirmationButton()
}
@@ -214,6 +204,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
mForceReadOnly = databaseFileNotExists
// Restore read-only state from database file if not forced
if (!mForceReadOnly) {
databaseFile?.readOnly?.let { savedReadOnlyState ->
mReadOnly = savedReadOnlyState
}
}
invalidateOptionsMenu()
// Post init uri with KeyFile only if needed
@@ -240,11 +237,63 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel?.let { deviceUnlockViewModel ->
deviceUnlockViewModel.uiState.collect { uiState ->
// New value received
uiState.credentialRequiredCipher?.let { cipher ->
deviceUnlockViewModel.encryptCredential(
credential = getCredentialForEncryption(),
cipher = cipher
)
}
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
onCredentialEncrypted(cipherEncryptDatabase)
deviceUnlockViewModel.consumeCredentialEncrypted()
}
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
onCredentialDecrypted(cipherDecryptDatabase)
deviceUnlockViewModel.consumeCredentialDecrypted()
}
uiState.exception?.let { error ->
Snackbar.make(
coordinatorLayout,
deviceUnlockError(error, this@MainCredentialActivity),
Snackbar.LENGTH_LONG
).asError().show()
deviceUnlockViewModel.exceptionShown()
}
}
}
}
}
}
}
override fun onResume() {
super.onResume()
// Init Biometric elements only if allowed
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& PreferencesUtil.isDeviceUnlockEnable(this)) {
deviceUnlockFragment = supportFragmentManager
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment?
if (deviceUnlockFragment == null) {
deviceUnlockFragment = DeviceUnlockFragment().also {
supportFragmentManager.commit {
replace(
R.id.fragment_device_unlock_container_view,
it,
UNLOCK_FRAGMENT_TAG
)
}
}
}
}
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
@@ -253,23 +302,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
}
// Don't allow auto open prompt if lock become when UI visible
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
}
mDatabaseFileUri?.let { databaseFileUri ->
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
if (database != null) {
// Trying to load another database
if (mDatabaseFileUri != null
&& database.fileUri != null
@@ -281,7 +320,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
launchGroupActivityIfLoaded(database)
}
}
override fun onDatabaseActionFinished(
database: ContextualDatabase,
@@ -291,9 +329,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
if (result.isSuccess) {
launchGroupActivityIfLoaded(database)
} else {
@@ -390,28 +425,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher
mCredentialActivityResultLauncher
)
}
}
override fun retrieveCredentialForEncryption(): ByteArray {
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
?: byteArrayOf()
}
override fun conditionToStoreCredential(): Boolean {
return mainCredentialView?.conditionToStoreCredential() == true
}
override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
// Load the database if password is registered with biometric
loadDatabase(mDatabaseFileUri,
mainCredentialView?.getMainCredential(),
cipherEncryptDatabase
)
}
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
override fun passwordToStore(password: String?): ByteArray? {
return password?.toByteArray()
@@ -428,7 +446,20 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
}
override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
private fun getCredentialForEncryption(): ByteArray {
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
?: byteArrayOf()
}
private fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
// Load the database if password is registered with biometric
loadDatabase(mDatabaseFileUri,
mainCredentialView?.getMainCredential(),
cipherEncryptDatabase
)
}
private fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
// Load the database if password is retrieve from biometric
// Retrieve from biometric
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
@@ -472,15 +503,18 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
val password = intent.getStringExtra(KEY_PASSWORD)
// Consume the intent extra password
intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) {
mainCredentialView?.populatePasswordTextView(password)
}
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
if (launchImmediately) {
loadDatabase()
} else {
// Init Biometric elements
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel?.connect(databaseFileUri)
}
}
enableConfirmationButton()
@@ -508,13 +542,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
}
override fun onPause() {
// Reinit locking activity UI variable
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
super.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
super.onSaveInstanceState(outState)
@@ -535,13 +562,10 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
clearCredentialsViews()
}
if (mReadOnly && (
mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
) {
Log.e(TAG, getString(R.string.autofill_read_only_save))
if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
Log.e(TAG, getString(R.string.error_save_read_only))
Snackbar.make(coordinatorLayout,
R.string.autofill_read_only_save,
R.string.error_save_read_only,
Snackbar.LENGTH_LONG).asError().show()
} else {
databaseFileUri?.let { databaseUri ->
@@ -562,7 +586,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUUID: Boolean) {
loadDatabase(
mDatabaseViewModel.loadDatabase(
databaseUri,
mainCredential,
readOnly,
@@ -630,7 +654,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
try {
menu.findItem(R.id.menu_open_file_read_mode_key)
} catch (e: Exception) {
Log.e(TAG, "Unable to find read mode menu")
Log.e(TAG, "Unable to find read mode menu", e)
}
performedNextEducation(menu)
},
@@ -640,17 +664,17 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !readOnlyEducationPerformed) {
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this)
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& advancedUnlockButton != null) {
&& deviceUnlockButton != null) {
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
advancedUnlockButton!!,
deviceUnlockButton!!,
{
startActivity(
Intent(
this,
AdvancedUnlockSettingsActivity::class.java
DeviceUnlockSettingsActivity::class.java
)
)
},
@@ -659,7 +683,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
})
}
}
} catch (ignored: Exception) {}
} catch (_: Exception) {}
}
}
@@ -680,6 +704,12 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
R.id.menu_open_file_read_mode_key -> {
mReadOnly = !mReadOnly
changeOpenFileReadIcon(item)
// Save the read-only state to database
mDatabaseFileUri?.let { databaseUri ->
FileDatabaseHistoryAction.getInstance(applicationContext).addOrUpdateDatabaseFile(
DatabaseFile(databaseUri = databaseUri, readOnly = mReadOnly)
)
}
}
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
}
@@ -687,6 +717,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
return super.onOptionsItemSelected(item)
}
override fun onDestroy() {
super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel?.disconnect()
}
}
companion object {
private val TAG = MainCredentialActivity::class.java.name
@@ -702,11 +739,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity,
private fun buildAndLaunchIntent(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit) {
intentBuildLauncher: (Intent) -> Unit
) {
val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null)
@@ -723,10 +762,12 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
*/
@Throws(FileNotFoundException::class)
fun launch(activity: Activity,
fun launch(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?) {
hardwareKey: HardwareKey?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent)
}
@@ -739,81 +780,46 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
*/
@Throws(FileNotFoundException::class)
fun launchForSearchResult(activity: Activity,
fun launchForSearchResult(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
searchInfo: SearchInfo
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult(
activity,
intent,
searchInfo)
context = activity,
intent = intent,
searchInfo = searchInfo
)
}
}
/*
* -------------------------
* Save Launch
* Selection Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForSaveResult(activity: Activity,
fun launchForSelection(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
typeMode: TypeMode,
searchInfo: SearchInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSaveModeResult(
activity,
intent,
searchInfo)
}
}
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForKeyboardResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
activity,
intent,
searchInfo)
}
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class)
fun launchForAutofillResult(activity: AppCompatActivity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
activityResultLauncher,
autofillComponent,
searchInfo)
EntrySelectionHelper.startActivityForSelectionModeResult(
context = activity,
intent = intent,
typeMode = typeMode,
searchInfo = searchInfo,
activityResultLauncher = activityResultLauncher
)
}
}
@@ -822,102 +828,25 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
* Registration Launch
* -------------------------
*/
fun launchForRegistration(activity: Activity,
@Throws(FileNotFoundException::class)
fun launchForRegistration(
activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
registerInfo: RegisterInfo?) {
typeMode: TypeMode,
registerInfo: RegisterInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
activity,
intent,
registerInfo)
}
}
/*
* -------------------------
* Global Launch
* -------------------------
*/
fun launch(activity: AppCompatActivity,
databaseUri: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
try {
EntrySelectionHelper.doSpecialAction(activity.intent,
{
launch(
activity,
databaseUri,
keyFile,
hardwareKey
context = activity,
intent = intent,
typeMode = typeMode,
registerInfo = registerInfo,
activityResultLauncher = activityResultLauncher,
)
},
{ searchInfo -> // Search Action
launchForSearchResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Save Action
launchForSaveResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Keyboard Selection Action
launchForKeyboardResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo, autofillComponent -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
launchForAutofillResult(
activity,
databaseUri,
keyFile,
hardwareKey,
autofillActivityResultLauncher,
autofillComponent,
searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
{ registerInfo -> // Registration Action
launchForRegistration(
activity,
databaseUri,
keyFile,
hardwareKey,
registerInfo
)
onLaunchActivitySpecialMode()
}
)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
}
}

View File

@@ -43,6 +43,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(OLD_FILE_DATABASE_INFO)
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(NEW_FILE_DATABASE_INFO)
val readOnlyDatabase: Boolean = arguments?.getBoolean(READ_ONLY_DATABASE) ?: true
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
// Use the Builder class for convenient dialog construction
@@ -54,13 +55,19 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
+ "\n\n" +
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
stringBuilder.append(getString(
if (readOnlyDatabase) {
R.string.warning_database_info_changed_options_read_only
} else {
R.string.warning_database_info_changed_options
}
))
} else {
stringBuilder.append(getString(R.string.warning_database_revoked))
}
builder.setMessage(stringBuilder)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
actionDatabaseListener?.validateDatabaseChanged()
actionDatabaseListener?.onDatabaseChangeValidated()
}
return builder.create()
}
@@ -69,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
}
interface ActionDatabaseChangedListener {
fun validateDatabaseChanged()
fun onDatabaseChangeValidated()
}
companion object {
@@ -77,15 +84,19 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
newSnapFileDatabaseInfo: SnapFileDatabaseInfo
fun getInstance(
oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
readOnly: Boolean
)
: DatabaseChangedDialogFragment {
val fragment = DatabaseChangedDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
putBoolean(READ_ONLY_DATABASE, readOnly)
}
return fragment
}

View File

@@ -1,42 +1,78 @@
package com.kunzisoft.keepass.activities.dialogs
import android.os.Bundle
import android.view.View
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: ContextualDatabase? = null
private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseViewModel.database.observe(this) { database ->
this.mDatabase = database
resetAppTimeoutOnTouchOrFocus()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
}
}
}
mDatabaseViewModel.actionFinished.observe(this) { result ->
onDatabaseActionFinished(result.database, result.actionTask, result.result)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Screenshot mode or hide views
context?.let {
if (PreferencesUtil.isScreenshotModeEnabled(it)) {
dialog?.window?.clearFlags(FLAG_SECURE)
} else {
dialog?.window?.setFlags(FLAG_SECURE, FLAG_SECURE)
}
}
}
@Suppress("DEPRECATION")
@Deprecated(message = "")
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
resetAppTimeoutOnTouchOrFocus()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Can be overridden by a subclass
}

View File

@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.DateTimeFieldView
@@ -62,14 +62,14 @@ class GroupDialogFragment : DatabaseDialogFragment() {
private lateinit var uuidContainerView: ViewGroup
private lateinit var uuidReferenceView: TextView
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
if (database?.allowCustomSearchableGroup() == true) {
if (database.allowCustomSearchableGroup()) {
searchableLabelView.visibility = View.VISIBLE
searchableView.visibility = View.VISIBLE
} else {
@@ -155,7 +155,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
mGroupInfo.defaultAutoTypeSequence)
val uuid = UuidUtil.toHexString(mGroupInfo.id)
val uuid = mGroupInfo.id?.asHexString()
if (uuid == null || uuid.isEmpty()) {
uuidContainerView.visibility = View.GONE
} else {

View File

@@ -45,7 +45,6 @@ import com.kunzisoft.keepass.view.InheritedCompletionView
import com.kunzisoft.keepass.view.TagsCompletionView
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.tokenautocomplete.FilteredArrayAdapter
import org.joda.time.DateTime
class GroupEditDialogFragment : DatabaseDialogFragment() {
@@ -90,27 +89,21 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
}
mGroupEditViewModel.onDateSelected.observe(this) { dateMilliseconds ->
mGroupEditViewModel.onDateSelected.observe(this) { date ->
// Save the date
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withMillis(dateMilliseconds)
.toDate())
mGroupInfo.expiryTime.setDate(date.year, date.month, date.day)
expirationView.dateTime = mGroupInfo.expiryTime
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
// Trick to recall selection with time
mGroupEditViewModel.requestDateTimeSelection(instantTime)
mGroupEditViewModel.requestDateTimeSelection(
DateInstant(mGroupInfo.expiryTime.instant, DateInstant.Type.TIME)
)
}
}
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
// Save the time
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withHourOfDay(viewModelTime.hours)
.withMinuteOfHour(viewModelTime.minutes)
.toDate(), mGroupInfo.expiryTime.type)
mGroupInfo.expiryTime.setTime(viewModelTime.hour, viewModelTime.minute)
expirationView.dateTime = mGroupInfo.expiryTime
}
@@ -119,32 +112,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
View.VISIBLE
} else {
View.GONE
}
if (database?.allowAutoType() == true) {
if (database.allowAutoType()) {
autoTypeContainerView.visibility = View.VISIBLE
} else {
autoTypeContainerView.visibility = View.GONE
}
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply {
threshold = 1
setAdapter(tagsAdapter)
}
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -255,11 +248,7 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
private fun retrieveGroupInfoFromViews() {
mGroupInfo.title = nameTextView.text.toString()
// Only if there
val newNotes = notesTextView.text.toString()
if (newNotes.isNotEmpty()) {
mGroupInfo.notes = newNotes
}
mGroupInfo.notes = notesTextView.text?.toString()
mGroupInfo.expires = expirationView.activation
mGroupInfo.expiryTime = expirationView.dateTime
mGroupInfo.searchable = searchableView.getValue()

View File

@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
}
mCustomIcon?.let { customIcon ->
populateViewsWithCustomIcon(customIcon)

View File

@@ -35,16 +35,21 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.view.HardwareKeySelectionView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.PasswordEditView
import com.kunzisoft.keepass.view.applyFontVisibility
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.security.SecureRandom
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
@@ -55,11 +60,12 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private lateinit var rootView: View
private lateinit var passwordCheckBox: CompoundButton
private lateinit var passwordView: PassKeyView
private lateinit var passwordEditView: PasswordEditView
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
private lateinit var passwordRepeatView: TextView
private lateinit var keyFileCheckBox: CompoundButton
private lateinit var keyFileGenerateButton: View
private lateinit var keyFileSelectionView: KeyFileSelectionView
private lateinit var hardwareKeyCheckBox: CompoundButton
@@ -141,29 +147,39 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
passwordView = rootView.findViewById(R.id.password_view)
passwordEditView = rootView.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
passwordRepeatView.applyFontVisibility()
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
keyFileGenerateButton = rootView.findViewById(R.id.keyfile_generate)
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
createdFileUri?.let { uri ->
createKeyFile(uri)
keyFileSelectionView.error = null
keyFileCheckBox.isChecked = true
keyFileSelectionView.uri = uri
}
}
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { pathUri ->
pathUri.getDocumentFile(requireContext())?.length()?.let { lengthFile ->
keyFileSelectionView.error = null
keyFileCheckBox.isChecked = true
keyFileSelectionView.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
showLengthKeyFileConfirmationDialog(lengthFile)
}
}
}
keyFileGenerateButton.setOnClickListener {
mExternalFileHelper?.createDocument(DEFAULT_KEYFILE_NAME)
}
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
@@ -202,6 +218,16 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
return super.onCreateDialog(savedInstanceState)
}
private fun createKeyFile(uri: Uri) {
CoroutineScope(Dispatchers.IO).launch {
activity?.contentResolver?.openOutputStream(uri)?.use { outputStream ->
val randomBytes = ByteArray(DEFAULT_KEYFILE_SIZE)
SecureRandom().nextBytes(randomBytes)
outputStream.write(randomBytes)
}
}
}
private fun approveMainCredential() {
val errorPassword = verifyPassword()
val errorKeyFile = verifyKeyFile()
@@ -232,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
showEmptyPasswordConfirmationDialog()
} else if (!error
&& hardwareKey != null
&& !HardwareKeyActivity.isHardwareKeyAvailable(
requireActivity(), hardwareKey, false)
&& !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
) {
// show hardware driver dialog if required
error = true
@@ -250,7 +275,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
var error = false
passwordRepeatTextInputLayout.error = null
if (passwordCheckBox.isChecked) {
mMasterPassword = passwordView.passwordString
mMasterPassword = passwordEditView.passwordString
val confPassword = passwordRepeatView.text.toString()
// Verify that passwords match
@@ -302,13 +327,13 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
super.onResume()
// To check checkboxes if a text is present
passwordView.addTextChangedListener(passwordTextWatcher)
passwordEditView.addTextChangedListener(passwordTextWatcher)
}
override fun onPause() {
super.onPause()
passwordView.removeTextChangedListener(passwordTextWatcher)
passwordEditView.removeTextChangedListener(passwordTextWatcher)
}
private fun showEmptyPasswordConfirmationDialog() {
@@ -339,15 +364,25 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
}
private fun showEmptyKeyFileConfirmationDialog() {
private fun showLengthKeyFileConfirmationDialog(length: Long) {
activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(SpannableStringBuilder().apply {
append(getString(R.string.warning_empty_keyfile))
append("\n\n")
append(getString(R.string.warning_empty_keyfile_explanation))
var warning = false
if (length <= 0L) {
warning = true
append("\n\n")
append(getString(R.string.warning_empty_keyfile))
} else if (length > 10485760L) {
warning = true
append("\n\n")
append(getString(R.string.warning_large_keyfile))
}
if (warning) {
append("\n\n")
append(getString(R.string.warning_sure_add_file))
}
})
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ ->
@@ -362,6 +397,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
companion object {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
private const val DEFAULT_KEYFILE_NAME = "keyfile.bin"
private const val DEFAULT_KEYFILE_SIZE = 128
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
val fragment = SetMainCredentialDialogFragment()

View File

@@ -29,7 +29,11 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
@@ -40,14 +44,15 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpTokenType
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.otp.TokenCalculator
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.UriUtil.openUrl
import com.kunzisoft.keepass.utils.getParcelableCompat
import java.util.*
import java.util.Locale
class SetOTPDialogFragment : DatabaseDialogFragment() {
@@ -224,6 +229,9 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
}
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
// Ensure that the UX does not prevent user from hiding/unhiding text
otpSecretContainer?.errorIconDrawable = null
// Set the default value of OTP element
upgradeType()
upgradeTokenType()
@@ -310,12 +318,17 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {
s?.toString()?.let { userString ->
if (userString.length >= MIN_OTP_SECRET) {
try {
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
otpSecretContainer?.error = null
} catch (exception: Exception) {
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
}
} else {
otpSecretContainer?.error = getString(R.string.error_otp_secret_length,
MIN_OTP_SECRET)
}
mSecretWellFormed = otpSecretContainer?.error == null
}
}

View File

@@ -176,21 +176,14 @@ class SortDialogFragment : DatabaseDialogFragment() {
return bundle
}
fun getInstance(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean): SortDialogFragment {
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
val fragment = SortDialogFragment()
fragment.arguments = bundle
return fragment
}
fun getInstance(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean): SortDialogFragment {
recycleBinBottom: Boolean?): SortDialogFragment {
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
recycleBinBottom?.let {
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
}
val fragment = SortDialogFragment()
fragment.arguments = bundle
return fragment

View File

@@ -4,36 +4,59 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected var mDatabase: ContextualDatabase? = null
protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
if (mDatabase == null || mDatabase != database) {
this.mDatabase = database
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
when (uiState) {
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
}
else -> {}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
database?.let {
onDatabaseRetrieved(database)
}
}
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
onDatabaseActionFinished(result.database, result.actionTask, result.result)
}
}
}
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
context?.let {
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
view?.resetAppTimeoutWhenViewTouchedOrFocused(
context = it,
databaseLoaded = mDatabase?.loaded
)
}
}
@@ -44,8 +67,4 @@ abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
) {
// Can be overridden by a subclass
}
protected fun buildNewBinaryAttachment(): BinaryData? {
return mDatabase?.buildNewBinaryAttachment()
}
}

View File

@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
val attachmentToUploadUri = it.attachmentToUploadUri
val fileName = it.fileName
buildNewBinaryAttachment()?.let { binaryAttachment ->
mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment
if ((!mAllowMultipleAttachments
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
templateView.populateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
}
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
mAllowMultipleAttachments = database.allowMultipleAttachments == true
attachmentsAdapter?.database = database
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
}
}
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
tagsCompletionView.apply {
threshold = 1
setAdapter(tagsAdapter)
}
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
}
private fun assignEntryInfo(entryInfo: EntryInfo?) {

View File

@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
import com.kunzisoft.keepass.view.TemplateView
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showByFading
@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
context?.let { context ->
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
attachmentsAdapter?.database = database
@@ -184,7 +184,7 @@ class EntryFragment: DatabaseFragment() {
// customDataView.text = entryInfo?.customData?.toString()
// Assign special data
uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
uuidReferenceView.text = entryInfo?.id?.asHexString()
}
private fun showClipboardDialog() {

View File

@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.adapters.NodesAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
@@ -76,9 +76,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var specialMode: SpecialMode = SpecialMode.DEFAULT
private var mRecycleBinEnable: Boolean = false
private var mRecycleBin: Group? = null
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
@@ -102,21 +99,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
R.id.menu_sort -> {
context?.let { context ->
val sortDialogFragment: SortDialogFragment =
if (mRecycleBinEnable) {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
if (mDatabase?.isRecycleBinEnabled == true) {
PreferencesUtil.getRecycleBinBottomSort(context)
} else null
)
} else {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context)
)
}
sortDialogFragment.show(childFragmentManager, "sortDialog")
}
true
@@ -164,12 +154,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onDetach()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mRecycleBinEnable = database?.isRecycleBinEnabled == true
mRecycleBin = database?.recycleBin
override fun onDatabaseRetrieved(database: ContextualDatabase) {
context?.let { context ->
database?.let { database ->
mAdapter = NodesAdapter(context, database).apply {
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
override fun onNodeClick(database: ContextualDatabase, node: Node) {
@@ -208,7 +194,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mNodesRecyclerView?.adapter = mAdapter
}
}
}
override fun onDatabaseActionFinished(
database: ContextualDatabase,
@@ -261,7 +246,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
specialMode = it.retrieveSpecialMode()
}
}
@@ -312,6 +297,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
}
private fun containsRecycleBin(database: ContextualDatabase?, nodes: List<Node>): Boolean {
return database?.isRecycleBinEnabled == true
&& nodes.any { it == database.recycleBin }
}
fun actionNodesCallback(database: ContextualDatabase,
nodes: List<Node>,
menuListener: NodesActionMenuListener?,
@@ -336,8 +326,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
// Open and Edit for a single item
if (nodes.size == 1) {
// Edition
if (database.isReadOnly
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
menu?.removeItem(R.id.menu_edit)
}
} else {
@@ -357,8 +346,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
// Deletion
if (database.isReadOnly
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
menu?.removeItem(R.id.menu_delete)
}
}

View File

@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
CoroutineScope(Dispatchers.IO).launch {
val populateList = launch {

View File

@@ -48,9 +48,9 @@ class IconPickerFragment : DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (database?.allowCustomIcons == true) 2 else 1)
if (database.allowCustomIcons) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {

View File

@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
super.onDestroyView()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here
}

View File

@@ -34,12 +34,12 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.PasswordEditView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PassphraseGeneratorFragment : DatabaseFragment() {
private lateinit var passKeyView: PassKeyView
private lateinit var passwordEditView: PasswordEditView
private lateinit var sliderWordCount: Slider
private lateinit var wordCountText: EditText
@@ -62,7 +62,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
passKeyView = view.findViewById(R.id.passphrase_view)
passwordEditView = view.findViewById(R.id.passphrase_view)
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
sliderWordCount = view.findViewById(R.id.slider_word_count)
wordCountText = view.findViewById(R.id.word_count)
@@ -80,7 +80,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
passphraseCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(
getString(R.string.passphrase),
passKeyView.passwordString,
passwordEditView.passwordString,
true
)
}
@@ -146,7 +146,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
generatePassphrase()
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
mKeyGeneratorViewModel.setKeyGenerated(passwordEditView.passwordString)
}
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
@@ -219,7 +219,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a passphrase", e)
}
passKeyView.passwordString = passphrase
passwordEditView.passwordString = passphrase
charactersCountText.text = getString(R.string.character_count, passphrase.length)
}
@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here
}

View File

@@ -36,12 +36,12 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.PasswordEditView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PasswordGeneratorFragment : DatabaseFragment() {
private lateinit var passKeyView: PassKeyView
private lateinit var passwordEditView: PasswordEditView
private lateinit var sliderLength: Slider
private lateinit var lengthEditView: EditText
@@ -74,7 +74,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
passKeyView = view.findViewById(R.id.password_view)
passwordEditView = view.findViewById(R.id.password_view)
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
sliderLength = view.findViewById(R.id.slider_length)
@@ -101,7 +101,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
passwordCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(
getString(R.string.password),
passKeyView.passwordString,
passwordEditView.passwordString,
true
)
}
@@ -195,7 +195,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
generatePassword()
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
mKeyGeneratorViewModel.setKeyGenerated(passwordEditView.passwordString)
}
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
@@ -293,24 +293,26 @@ class PasswordGeneratorFragment : DatabaseFragment() {
private fun generatePassword() {
var password = ""
try {
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
uppercaseCompound.isChecked,
lowercaseCompound.isChecked,
digitsCompound.isChecked,
minusCompound.isChecked,
underlineCompound.isChecked,
spaceCompound.isChecked,
specialsCompound.isChecked,
bracketsCompound.isChecked,
extendedCompound.isChecked,
getConsiderChars(),
getIgnoreChars(),
atLeastOneCompound.isChecked,
excludeAmbiguousCompound.isChecked)
password = PasswordGenerator(resources).generatePassword(
length = getPasswordLength(),
upperCase = uppercaseCompound.isChecked,
lowerCase = lowercaseCompound.isChecked,
digits = digitsCompound.isChecked,
minus = minusCompound.isChecked,
underline = underlineCompound.isChecked,
space = spaceCompound.isChecked,
specials = specialsCompound.isChecked,
brackets = bracketsCompound.isChecked,
extended = extendedCompound.isChecked,
considerChars = getConsiderChars(),
ignoreChars = getIgnoreChars(),
atLeastOneFromEach = atLeastOneCompound.isChecked,
excludeAmbiguousChar = excludeAmbiguousCompound.isChecked
)
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e)
}
passKeyView.passwordString = password
passwordEditView.passwordString = password
}
override fun onDestroy() {
@@ -318,7 +320,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Nothing here
}

View File

@@ -1,211 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.helpers
import android.content.Context
import android.content.Intent
import android.os.Build
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.putEnumExtra
object EntrySelectionHelper {
private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
fun startActivityForSearchModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo) {
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun startActivityForSaveModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo) {
addSpecialModeInIntent(intent, SpecialMode.SAVE)
addTypeModeInIntent(intent, TypeMode.DEFAULT)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun startActivityForKeyboardSelectionModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo?) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
addSearchInfoInIntent(intent, searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun startActivityForRegistrationModeResult(context: Context,
intent: Intent,
registerInfo: RegisterInfo?) {
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
// At the moment, only autofill for registration
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
addRegisterInfoInIntent(intent, registerInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
searchInfo?.let {
intent.putExtra(KEY_SEARCH_INFO, it)
}
}
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO)
}
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
registerInfo?.let {
intent.putExtra(KEY_REGISTER_INFO, it)
}
}
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
}
fun removeInfoFromIntent(intent: Intent) {
intent.removeExtra(KEY_SEARCH_INFO)
intent.removeExtra(KEY_REGISTER_INFO)
}
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
}
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return SpecialMode.SELECTION
}
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
}
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
}
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return TypeMode.AUTOFILL
}
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
}
fun removeModesFromIntent(intent: Intent) {
intent.removeExtra(KEY_SPECIAL_MODE)
intent.removeExtra(KEY_TYPE_MODE)
}
fun doSpecialAction(intent: Intent,
defaultAction: () -> Unit,
searchAction: (searchInfo: SearchInfo) -> Unit,
saveAction: (searchInfo: SearchInfo) -> Unit,
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
autofillSelectionAction: (searchInfo: SearchInfo?,
autofillComponent: AutofillComponent) -> Unit,
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
when (retrieveSpecialModeFromIntent(intent)) {
SpecialMode.DEFAULT -> {
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
defaultAction.invoke()
}
SpecialMode.SEARCH -> {
val searchInfo = retrieveSearchInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
if (searchInfo != null)
searchAction.invoke(searchInfo)
else {
defaultAction.invoke()
}
}
SpecialMode.SAVE -> {
val searchInfo = retrieveSearchInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
if (searchInfo != null)
saveAction.invoke(searchInfo)
else {
defaultAction.invoke()
}
}
SpecialMode.SELECTION -> {
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
var autofillComponentInit = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
autofillSelectionAction.invoke(searchInfo, autofillComponent)
autofillComponentInit = true
}
}
if (!autofillComponentInit) {
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
when (retrieveTypeModeFromIntent(intent)) {
TypeMode.DEFAULT -> {
removeModesFromIntent(intent)
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
}
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
else -> {
// In this case, error
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
}
}
} else {
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
}
}
}
SpecialMode.REGISTRATION -> {
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
autofillRegistrationAction.invoke(registerInfo)
}
}
}
}

View File

@@ -1,5 +0,0 @@
package com.kunzisoft.keepass.activities.helpers
enum class TypeMode {
DEFAULT, MAGIKEYBOARD, AUTOFILL
}

View File

@@ -1,96 +1,307 @@
package com.kunzisoft.keepass.activities.legacy
import android.net.Uri
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getBinaryDir
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.tasks.ProgressTaskViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import kotlinx.coroutines.launch
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
protected var mDatabase: ContextualDatabase? = null
protected val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
private val progressTaskViewModel: ProgressTaskViewModel by viewModels()
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
private val mActionDatabaseListener =
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
override fun onDatabaseChangeValidated() {
mDatabaseViewModel.onDatabaseChangeValidated()
}
}
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { _ ->
// Whether or not the user has accepted, the service can be started,
// There just won't be any notification if it's not allowed.
tempServiceParameters.removeFirstOrNull()?.let {
startDatabaseService(it.first, it.second)
}
}
/**
* Useful to only waiting for the activity result and prevent any parallel action
*/
var credentialResultLaunched = false
/**
* Utility activity result launcher,
* Used recursively, close each activity with return data
*/
protected var mCredentialActivityResultLauncher: CredentialActivityResultLauncher =
CredentialActivityResultLauncher(
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
setActivityResult(
lockDatabase = false,
resultCode = it.resultCode,
data = it.data
)
}
)
/**
* Custom ActivityResultLauncher to manage the database action
*/
protected inner class CredentialActivityResultLauncher(
val builder: ActivityResultLauncher<Intent>
) : ActivityResultLauncher<Intent>() {
override fun launch(
input: Intent?,
options: ActivityOptionsCompat?
) {
credentialResultLaunched = true
builder.launch(input, options)
}
override fun unregister() {
builder.unregister()
}
override fun getContract(): ActivityResultContract<Intent?, *> {
return builder.getContract()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
if (savedInstanceState != null
&& savedInstanceState.containsKey(CREDENTIAL_RESULT_LAUNCHER_KEY)
) {
credentialResultLaunched = savedInstanceState.getBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY)
}
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
val databaseWasReloaded = database?.wasReloaded == true
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mDatabaseViewModel.actionState.collect { uiState ->
if (credentialResultLaunched.not()) {
when (uiState) {
is DatabaseViewModel.ActionState.Wait -> {}
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
if (finishActivityIfReloadRequested()) {
finish()
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
database?.wasReloaded = false
}
}
is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> {
if (manageDatabaseInfo()) {
showDatabaseChangedDialog(
uiState.previousDatabaseInfo,
uiState.newDatabaseInfo,
uiState.readOnlyDatabase
)
}
}
is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
startDatabasePermissionService(
uiState.bundle,
uiState.actionTask
)
}
is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
progressTaskViewModel.show(uiState.progressMessage)
}
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
progressTaskViewModel.show(uiState.progressMessage)
}
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
progressTaskViewModel.hide()
}
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
onDatabaseActionFinished(
uiState.database,
uiState.actionTask,
uiState.result
)
progressTaskViewModel.hide()
}
}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
progressTaskViewModel.progressTaskState.collect { state ->
when (state) {
is ProgressTaskViewModel.ProgressTaskState.Show ->
startDialog()
is ProgressTaskViewModel.ProgressTaskState.Hide ->
stopDialog()
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDatabaseViewModel.databaseState.collect { database ->
if (credentialResultLaunched.not()) {
// Nullable function
onUnknownDatabaseRetrieved(database)
database?.let {
onDatabaseRetrieved(database)
}
}
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
onDatabaseActionFinished(database, actionTask, result)
}
}
}
}
protected open fun showDatabaseDialog(): Boolean {
return true
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY, credentialResultLaunched)
super.onSaveInstanceState(outState)
}
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
mDatabase = null
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
/**
* Nullable function to retrieve a database
*/
open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
// optional method implementation
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// optional method implementation
}
open fun manageDatabaseInfo(): Boolean = true
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
mDatabaseViewModel.onActionFinished(database, actionTask, result)
// optional method implementation
}
fun createDatabase(
databaseUri: Uri,
mainCredential: MainCredential
private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED
) {
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
}
fun loadDatabase(
databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
startDatabaseService(bundle, actionTask)
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.POST_NOTIFICATIONS
)
) {
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
// it's not the first time, so the user deliberately chooses not to display the notification
startDatabaseService(bundle, actionTask)
} else {
AlertDialog.Builder(this)
.setMessage(R.string.warning_database_notification_permission)
.setNegativeButton(R.string.later) { _, _ ->
// Refuses the notification, so start the service
startDatabaseService(bundle, actionTask)
}
.setPositiveButton(R.string.ask) { _, _ ->
// Save the temp parameters to ask the permission
tempServiceParameters.add(Pair(bundle, actionTask))
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}.create().show()
}
} else {
startDatabaseService(bundle, actionTask)
}
}
protected fun closeDatabase() {
mDatabase?.clearAndClose(this.getBinaryDir())
private fun showDatabaseChangedDialog(
previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo,
readOnlyDatabase: Boolean
) {
lifecycleScope.launch {
if (databaseChangedDialogFragment == null) {
databaseChangedDialogFragment = supportFragmentManager
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
}
if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
previousDatabaseInfo,
newDatabaseInfo,
readOnlyDatabase
)
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
databaseChangedDialogFragment?.show(
supportFragmentManager,
DATABASE_CHANGED_DIALOG_TAG
)
}
}
}
override fun onResume() {
super.onResume()
mDatabaseTaskProvider?.registerProgressTask()
private fun startDialog() {
lifecycleScope.launch {
if (showDatabaseDialog()) {
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = supportFragmentManager
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
}
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(
supportFragmentManager,
PROGRESS_TASK_DIALOG_TAG
)
}
}
}
}
override fun onPause() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
private fun stopDialog() {
progressTaskDialogFragment?.dismissAllowingStateLoss()
progressTaskDialogFragment = null
}
protected open fun showDatabaseDialog(): Boolean {
return true
}
companion object {
const val CREDENTIAL_RESULT_LAUNCHER_KEY = "com.kunzisoft.keepass.CREDENTIAL_RESULT_LAUNCHER_KEY"
}
}

View File

@@ -34,8 +34,8 @@ import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.element.Entry
@@ -47,10 +47,14 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.LockReceiver
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.utils.registerLockReceiver
import com.kunzisoft.keepass.utils.unregisterLockReceiver
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.NodesViewModel
import java.util.*
import java.util.UUID
abstract class DatabaseLockActivity : DatabaseModeActivity(),
PasswordEncodingDialogFragment.Listener {
@@ -83,109 +87,24 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
deleteDatabaseNodes(nodes)
}
mDatabaseViewModel.saveDatabase.observe(this) { save ->
mDatabaseTaskProvider?.startDatabaseSave(save)
}
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
mDatabaseTaskProvider?.startDatabaseMerge(save)
}
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
}
}
mDatabaseViewModel.saveName.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveDescription.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveDefaultUsername.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveColor.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveCompression.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.removeUnlinkData.observe(this) {
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
}
mDatabaseViewModel.saveRecycleBin.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveEncryption.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveKeyDerivation.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveIterations.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMemoryUsage.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveParallelism.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
}
mExitLock = false
}
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
override fun onDatabaseRetrieved(database: ContextualDatabase) {
// End activity if database not loaded
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
if (database.loaded.not())
finish()
}
// Focus view to reinitialize timeout,
// view is not necessary loaded so retry later in resume
viewToInvalidateTimeout()
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
database?.let {
// check timeout
if (mTimeoutEnable) {
if (mLockReceiver == null) {
mLockReceiver = LockReceiver {
mDatabase = null
closeDatabase(database)
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
mExitLock = true
closeOptionsMenu()
finish()
@@ -207,7 +126,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
checkRegister()
}
}
override fun finish() {
// To fix weird crash
@@ -225,7 +143,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
@@ -247,24 +164,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
databaseUri: Uri?,
mainCredential: MainCredential
) {
assignDatabasePassword(databaseUri, mainCredential)
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
}
private fun assignDatabasePassword(
databaseUri: Uri?,
mainCredential: MainCredential
) {
if (databaseUri != null) {
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
}
}
fun assignPassword(mainCredential: MainCredential) {
fun assignMainCredential(mainCredential: MainCredential) {
mDatabase?.let { database ->
database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
assignDatabasePassword(databaseUri, mainCredential)
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
} else {
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
.show(supportFragmentManager, "passwordEncodingTag")
@@ -274,45 +182,51 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
fun saveDatabase() {
mDatabaseTaskProvider?.startDatabaseSave(true)
mDatabaseViewModel.saveDatabase(save = true)
}
fun saveDatabaseTo(uri: Uri) {
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
}
fun mergeDatabase() {
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
}
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
}
fun reloadDatabase() {
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(false)
}
mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
}
fun createEntry(newEntry: Entry,
parent: Group) {
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
fun createEntry(
newEntry: Entry,
parent: Group
) {
mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
}
fun updateEntry(oldEntry: Entry,
entryToUpdate: Entry) {
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
fun updateEntry(
oldEntry: Entry,
entryToUpdate: Entry
) {
mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
}
fun copyNodes(nodesToCopy: List<Node>,
newParent: Group) {
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
fun copyNodes(
nodesToCopy: List<Node>,
newParent: Group
) {
mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
}
fun moveNodes(nodesToMove: List<Node>,
newParent: Group) {
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
fun moveNodes(
nodesToMove: List<Node>,
newParent: Group
) {
mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
}
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
@@ -328,6 +242,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
// TODO Move in ViewModel
mDatabase?.let { database ->
// If recycle bin enabled, ensure it exists
if (database.isRecycleBinEnabled) {
@@ -348,11 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
private fun deleteDatabaseNodes(nodes: List<Node>) {
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
}
fun createGroup(parent: Group,
groupInfo: GroupInfo?) {
fun createGroup(
parent: Group,
groupInfo: GroupInfo?
) {
// TODO Move in ViewModel
// Build the group
mDatabase?.createGroup()?.let { newGroup ->
groupInfo?.let { info ->
@@ -360,12 +278,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
// Not really needed here because added in runnable but safe
newGroup.parent = parent
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
}
}
fun updateGroup(oldGroup: Group,
groupInfo: GroupInfo) {
fun updateGroup(
oldGroup: Group,
groupInfo: GroupInfo
) {
// TODO Move in ViewModel
// If group updated save it in the database
val updateGroup = Group(oldGroup).let { updateGroup ->
updateGroup.apply {
@@ -375,27 +296,28 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
this.setGroupInfo(groupInfo)
}
}
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
}
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int) {
mDatabaseTaskProvider
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
fun restoreEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int
) {
mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int) {
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
fun deleteEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int
) {
mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
private fun checkRegister() {
// If in ave or registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
&& mDatabaseReadOnly) {
// If in registration mode, don't allow read only
if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
EntrySelectionHelper.removeModesFromIntent(intent)
intent.removeModes()
finish()
}
}
@@ -413,8 +335,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
invalidateOptionsMenu()
LOCKING_ACTIVITY_UI_VISIBLE = true
}
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
@@ -429,8 +349,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
override fun onPause() {
LOCKING_ACTIVITY_UI_VISIBLE = false
super.onPause()
if (mTimeoutEnable) {
@@ -452,9 +370,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.lock) { _, _ ->
sendBroadcast(Intent(LOCK_ACTION))
finish()
}.create().show()
} else {
sendBroadcast(Intent(LOCK_ACTION))
finish()
}
}
@@ -480,9 +400,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
private var LOCKING_ACTIVITY_UI_VISIBLE = false
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
}
}

View File

@@ -5,9 +5,16 @@ import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.TypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.ToolbarSpecial
@@ -19,7 +26,7 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
abstract class DatabaseModeActivity : DatabaseActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
private var mTypeMode: TypeMode = TypeMode.DEFAULT
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
private var mToolbarSpecial: ToolbarSpecial? = null
@@ -42,20 +49,14 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
/**
* Intent sender uses special retains data in callback
*/
private fun isIntentSender(): Boolean {
return (mSpecialMode == SpecialMode.SELECTION
&& mTypeMode == TypeMode.AUTOFILL)
/* TODO Registration callback #765
|| (mSpecialMode == SpecialMode.REGISTRATION
&& mTypeMode == TypeMode.AUTOFILL
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
*/
protected fun isIntentSender(): Boolean {
return isIntentSenderMode(mSpecialMode, mTypeMode)
}
fun onLaunchActivitySpecialMode() {
if (!isIntentSender()) {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
intent.removeModes()
intent.removeInfo()
finish()
}
}
@@ -64,8 +65,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
if (isIntentSender()) {
super.finish()
} else {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
intent.removeModes()
intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish()
}
@@ -77,8 +78,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
// To get the app caller, only for IntentSender
onRegularBackPressed()
} else {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
intent.removeModes()
intent.removeInfo()
if (mSpecialMode != SpecialMode.DEFAULT) {
backToTheMainAppAndFinish()
}
@@ -109,17 +110,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
}
})
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = intent.retrieveTypeMode()
}
override fun onResume() {
super.onResume()
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
mSpecialMode = intent.retrieveSpecialMode()
mTypeMode = intent.retrieveTypeMode()
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
val searchInfo: SearchInfo? = registerInfo?.searchInfo
?: intent.retrieveSearchInfo()
// To show the selection mode
mToolbarSpecial = findViewById(R.id.special_mode_view)
@@ -128,26 +130,25 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
val selectionModeStringId = when (mSpecialMode) {
SpecialMode.DEFAULT, // Not important because hidden
SpecialMode.SEARCH -> R.string.search_mode
SpecialMode.SAVE -> R.string.save_mode
SpecialMode.SELECTION -> R.string.selection_mode
SpecialMode.REGISTRATION -> R.string.registration_mode
SpecialMode.REGISTRATION -> R.string.save_mode // Save is registration mode
}
val typeModeStringId = when (mTypeMode) {
TypeMode.DEFAULT, // Not important because hidden
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
TypeMode.AUTOFILL -> R.string.autofill
TypeMode.PASSKEY -> R.string.passkey
}
title = getString(selectionModeStringId)
if (mTypeMode != TypeMode.DEFAULT)
title = "$title (${getString(typeModeStringId)})"
// Populate subtitle
subtitle = searchInfo?.getName(resources)
subtitle = registerInfo?.getName(resources) ?: searchInfo?.getName(resources)
// Show the toolbar or not
visible = when (mSpecialMode) {
SpecialMode.DEFAULT -> false
SpecialMode.SEARCH -> true
SpecialMode.SAVE -> true
SpecialMode.SELECTION -> true
SpecialMode.REGISTRATION -> true
}

View File

@@ -4,8 +4,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable
interface DatabaseRetrieval {
fun onDatabaseRetrieved(database: ContextualDatabase?)
fun onDatabaseActionFinished(database: ContextualDatabase,
fun onDatabaseRetrieved(database: ContextualDatabase)
fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result)
result: ActionRunnable.Result
)
}

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.stylish
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -77,7 +78,18 @@ abstract class StylishActivity : AppCompatActivity() {
startActivity(intent)
}
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
overrideActivityTransition(
OVERRIDE_TRANSITION_OPEN,
android.R.anim.fade_in,
android.R.anim.fade_out
)
else
overridePendingTransition(
android.R.anim.fade_in,
android.R.anim.fade_out
)
}
override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -10,6 +10,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
@@ -17,7 +18,7 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
class BreadcrumbAdapter(val context: Context)
class BreadcrumbAdapter(val context: Context, val database: Database?)
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
@@ -31,6 +32,8 @@ class BreadcrumbAdapter(val context: Context)
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
private var mNodeFilter: NodeFilter = NodeFilter(context, database)
private var mShowNumberEntries = false
private var mShowUUID = false
private var mIconColor: Int = 0
@@ -112,12 +115,10 @@ class BreadcrumbAdapter(val context: Context)
holder.groupNumbersView?.apply {
if (mShowNumberEntries) {
group.refreshNumberOfChildEntries(
Group.ChildFilter.getDefaults(
PreferencesUtil.showExpiredEntries(context)
)
)
text = group.recursiveNumberOfChildEntries.toString()
text = group.getNumberOfChildEntries(
mNodeFilter.recursiveNumberOfEntries,
mNodeFilter.filter
).toString()
visibility = View.VISIBLE
} else {
visibility = View.GONE

View File

@@ -0,0 +1,30 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.settings.PreferencesUtil
class NodeFilter(
context: Context,
var database: Database? = null
) {
var recursiveNumberOfEntries = PreferencesUtil.recursiveNumberEntries(context)
private set
private var showExpired = PreferencesUtil.showExpiredEntries(context)
private var showTemplate = PreferencesUtil.showTemplates(context)
val filter: (Node) -> Boolean = { node ->
when (node) {
is Entry -> {
node.entryKDB?.isMetaStream() != true
}
is Group -> {
showTemplate || database?.templatesGroup != node
}
else -> true
} && (showExpired || !node.isCurrentlyExpires)
}
}

View File

@@ -66,6 +66,7 @@ class NodesAdapter (
private val mNodeSortedListCallback: NodeSortedListCallback
private val mNodeSortedList: SortedList<Node>
private val mInflater: LayoutInflater = LayoutInflater.from(context)
private val mNodeFilter: NodeFilter = NodeFilter(context, database)
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
@@ -82,7 +83,7 @@ class NodesAdapter (
private var mShowNumberEntries: Boolean = true
private var mShowOTP: Boolean = false
private var mShowUUID: Boolean = false
private var mEntryFilters = arrayOf<Group.ChildFilter>()
private var mNodeFilters: NodeFilter? = null
private var mOldVirtualGroup = false
private var mVirtualGroup = false
@@ -161,9 +162,7 @@ class NodesAdapter (
this.mShowOTP = PreferencesUtil.showOTPToken(context)
this.mShowUUID = PreferencesUtil.showUUID(context)
this.mEntryFilters = Group.ChildFilter.getDefaults(
PreferencesUtil.showExpiredEntries(context)
)
this.mNodeFilters = NodeFilter(context, database)
// Reinit textSize for all view type
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
@@ -176,7 +175,7 @@ class NodesAdapter (
mOldVirtualGroup = mVirtualGroup
mVirtualGroup = group.isVirtual
assignPreferences()
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
mNodeSortedList.replaceAll(group.getChildren(mNodeFilter.filter))
}
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
@@ -205,6 +204,11 @@ class NodesAdapter (
&& oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
&& oldItem.creationTime == newItem.creationTime
&& oldItem.lastModificationTime == newItem.lastModificationTime
&& oldItem.lastAccessTime == newItem.lastAccessTime
&& oldItem.expiryTime == newItem.expiryTime
&& oldItem.expires == newItem.expires
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
}
@@ -413,6 +417,7 @@ class NodesAdapter (
}
}
// OTP
val otpElement = entry.getOtpElement()
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
if (otpElement != null
@@ -435,6 +440,10 @@ class NodesAdapter (
holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE
// Passkey
holder.passkeyIcon?.visibility =
if (entry.getPasskey() != null) View.VISIBLE else View.GONE
// Assign colors
assignBackgroundColor(holder.container, entry)
assignBackgroundColor(holder.otpContainer, entry)
@@ -446,6 +455,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(foregroundColor)
holder.otpProgress?.setIndicatorColor(foregroundColor)
holder.attachmentIcon?.setColorFilter(foregroundColor)
holder.passkeyIcon?.setColorFilter(foregroundColor)
holder.meta.setTextColor(foregroundColor)
iconColor = foregroundColor
} else {
@@ -454,6 +464,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(mTextColorSecondary)
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
holder.passkeyIcon?.setColorFilter(mTextColorSecondary)
holder.meta.setTextColor(mTextColor)
}
} else {
@@ -462,6 +473,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(mColorOnSecondary)
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
holder.passkeyIcon?.setColorFilter(mColorOnSecondary)
holder.meta.setTextColor(mColorOnSecondary)
}
@@ -473,7 +485,10 @@ class NodesAdapter (
if (mShowNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as Group)
.recursiveNumberOfChildEntries
.getNumberOfChildEntries(
mNodeFilter.recursiveNumberOfEntries,
mNodeFilter.filter
)
.toString()
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
@@ -522,6 +537,8 @@ class NodesAdapter (
holder?.otpToken?.apply {
text = otpElement?.tokenString
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
textDirection = View.TEXT_DIRECTION_LTR
}
holder?.otpContainer?.setOnClickListener {
otpElement?.token?.let { token ->
@@ -601,6 +618,7 @@ class NodesAdapter (
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
var passkeyIcon: ImageView? = itemView.findViewById(R.id.node_passkey_icon)
}
companion object {

View File

@@ -19,15 +19,53 @@
*/
package com.kunzisoft.keepass.app
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import com.kunzisoft.keepass.activities.stylish.Stylish
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class App : MultiDexApplication() {
override fun onCreate() {
super.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver)
Stylish.load(this)
PRNGFixes.apply()
}
}
object AppLifecycleObserver : DefaultLifecycleObserver {
var isAppInForeground: Boolean = false
private set
var lockBackgroundEvent = false
private val _appJustLaunched = MutableSharedFlow<Unit>(replay = 0)
val appJustLaunched = _appJustLaunched.asSharedFlow()
@OptIn(DelicateCoroutinesApi::class)
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
val wasPreviouslyInBackground = !isAppInForeground
isAppInForeground = true
if (!lockBackgroundEvent && wasPreviouslyInBackground) {
GlobalScope.launch {
_appJustLaunched.emit(Unit)
}
}
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
isAppInForeground = false
}
}

View File

@@ -1,399 +0,0 @@
package com.kunzisoft.keepass.app;
/*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will Google be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, as long as the origin is not misrepresented.
*/
import android.os.Build;
import android.os.Process;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
import java.security.Security;
import java.util.Locale;
/**
* Fixes for the output of the default PRNG having low entropy.
*
* The fixes need to be applied via {@link #apply()} before any use of Java
* Cryptography Architecture primitives. A good place to invoke them is in the
* application's {@code onCreate}.
*/
public final class PRNGFixes {
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
getBuildFingerprintAndDeviceSerial();
/** Hidden constructor to prevent instantiation. */
private PRNGFixes() {}
/**
* Applies all fixes.
*
* @throws SecurityException if a fix is needed but could not be applied.
*/
public static void apply() {
try {
if (supportedOnThisDevice()) {
applyOpenSSLFix();
installLinuxPRNGSecureRandom();
}
} catch (Exception e) {
// Do nothing, do the best we can to implement the workaround
}
}
private static boolean supportedOnThisDevice() {
// Blacklist on samsung devices
if (Build.MANUFACTURER.toLowerCase(Locale.ENGLISH).contains("samsung")) {
return false;
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
return false;
}
if (onSELinuxEnforce()) {
return false;
}
File urandom = new File("/dev/urandom");
// Test permissions
if ( !(urandom.canRead() && urandom.canWrite()) ) {
return false;
}
// Test actually writing to urandom
try {
FileOutputStream fos = new FileOutputStream(urandom);
fos.write(0);
} catch (Exception e) {
return false;
}
return true;
}
private static boolean onSELinuxEnforce() {
try {
ProcessBuilder builder = new ProcessBuilder("getenforce");
builder.redirectErrorStream(true);
java.lang.Process process = builder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
process.waitFor();
String output = reader.readLine();
if (output == null) {
return false;
}
return output.toLowerCase(Locale.US).startsWith("enforcing");
} catch (Exception e) {
return false;
}
}
/**
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
* fix is not needed.
*
* @throws SecurityException if the fix is needed but could not be applied.
*/
private static void applyOpenSSLFix() throws SecurityException {
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
|| (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)) {
// No need to apply the fix
return;
}
try {
// Mix in the device- and invocation-specific seed.
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_seed", byte[].class)
.invoke(null, generateSeed());
// Mix output of Linux PRNG into OpenSSL's PRNG
int bytesRead = (Integer) Class.forName(
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_load_file", String.class, long.class)
.invoke(null, "/dev/urandom", 1024);
if (bytesRead != 1024) {
throw new IOException(
"Unexpected number of bytes read from Linux PRNG: "
+ bytesRead);
}
} catch (Exception e) {
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
}
}
/**
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
* default. Does nothing if the implementation is already the default or if
* there is not need to install the implementation.
*
* @throws SecurityException if the fix is needed but could not be applied.
*/
private static void installLinuxPRNGSecureRandom()
throws SecurityException {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
// No need to apply the fix
return;
}
// Install a Linux PRNG-based SecureRandom implementation as the
// default, if not yet installed.
Provider[] secureRandomProviders =
Security.getProviders("SecureRandom.SHA1PRNG");
if ((secureRandomProviders == null)
|| (secureRandomProviders.length < 1)
|| (!LinuxPRNGSecureRandomProvider.class.equals(
secureRandomProviders[0].getClass()))) {
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
}
// Assert that new SecureRandom() and
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
// by the Linux PRNG-based SecureRandom implementation.
SecureRandom rng1 = new SecureRandom();
if (!LinuxPRNGSecureRandomProvider.class.equals(
rng1.getProvider().getClass())) {
throw new SecurityException(
"new SecureRandom() backed by wrong Provider: "
+ rng1.getProvider().getClass());
}
SecureRandom rng2;
try {
rng2 = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new SecurityException("SHA1PRNG not available", e);
}
if (!LinuxPRNGSecureRandomProvider.class.equals(
rng2.getProvider().getClass())) {
throw new SecurityException(
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ " Provider: " + rng2.getProvider().getClass());
}
}
/**
* {@code Provider} of {@code SecureRandom} engines which pass through
* all requests to the Linux PRNG.
*/
private static class LinuxPRNGSecureRandomProvider extends Provider {
public LinuxPRNGSecureRandomProvider() {
super("LinuxPRNG",
1.0,
"A Linux-specific random number provider that uses"
+ " /dev/urandom");
// Although /dev/urandom is not a SHA-1 PRNG, some apps
// explicitly request a SHA1PRNG SecureRandom and we thus need to
// prevent them from getting the default implementation whose output
// may have low entropy.
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
}
}
/**
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
* ({@code /dev/urandom}).
*/
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
/*
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
* are passed through to the Linux PRNG (/dev/urandom). Instances of
* this class seed themselves by mixing in the current time, PID, UID,
* build fingerprint, and hardware serial number (where available) into
* Linux PRNG.
*
* Concurrency: Read requests to the underlying Linux PRNG are
* serialized (on sLock) to ensure that multiple threads do not get
* duplicated PRNG output.
*/
private static final File URANDOM_FILE = new File("/dev/urandom");
private static final Object sLock = new Object();
/**
* Input stream for reading from Linux PRNG or {@code null} if not yet
* opened.
*
* @GuardedBy("sLock")
*/
private static DataInputStream sUrandomIn;
/**
* Output stream for writing to Linux PRNG or {@code null} if not yet
* opened.
*
* @GuardedBy("sLock")
*/
private static OutputStream sUrandomOut;
/**
* Whether this engine instance has been seeded. This is needed because
* each instance needs to seed itself if the client does not explicitly
* seed it.
*/
private boolean mSeeded;
@Override
protected void engineSetSeed(byte[] bytes) {
try {
OutputStream out;
synchronized (sLock) {
out = getUrandomOutputStream();
}
out.write(bytes);
out.flush();
mSeeded = true;
} catch (IOException e) {
throw new SecurityException(
"Failed to mix seed into " + URANDOM_FILE, e);
}
}
@Override
protected void engineNextBytes(byte[] bytes) {
if (!mSeeded) {
// Mix in the device- and invocation-specific seed.
engineSetSeed(generateSeed());
}
try {
DataInputStream in;
synchronized (sLock) {
in = getUrandomInputStream();
}
synchronized (in) {
in.readFully(bytes);
}
} catch (IOException e) {
throw new SecurityException(
"Failed to read from " + URANDOM_FILE, e);
}
}
@Override
protected byte[] engineGenerateSeed(int size) {
byte[] seed = new byte[size];
engineNextBytes(seed);
return seed;
}
private DataInputStream getUrandomInputStream() {
synchronized (sLock) {
if (sUrandomIn == null) {
// NOTE: Consider inserting a BufferedInputStream between
// DataInputStream and FileInputStream if you need higher
// PRNG output performance and can live with future PRNG
// output being pulled into this process prematurely.
try {
sUrandomIn = new DataInputStream(
new FileInputStream(URANDOM_FILE));
} catch (IOException e) {
throw new SecurityException("Failed to open "
+ URANDOM_FILE + " for reading", e);
}
}
return sUrandomIn;
}
}
private OutputStream getUrandomOutputStream() {
synchronized (sLock) {
if (sUrandomOut == null) {
try {
sUrandomOut = new FileOutputStream(URANDOM_FILE);
} catch (IOException e) {
throw new SecurityException("Failed to open "
+ URANDOM_FILE + " for writing", e);
}
}
return sUrandomOut;
}
}
}
/**
* Generates a device- and invocation-specific seed to be mixed into the
* Linux PRNG.
*/
private static byte[] generateSeed() {
try {
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
DataOutputStream seedBufferOut =
new DataOutputStream(seedBuffer);
seedBufferOut.writeLong(System.currentTimeMillis());
seedBufferOut.writeLong(System.nanoTime());
seedBufferOut.writeInt(Process.myPid());
seedBufferOut.writeInt(Process.myUid());
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
seedBufferOut.close();
return seedBuffer.toByteArray();
} catch (IOException e) {
throw new SecurityException("Failed to generate seed", e);
}
}
/**
* Gets the hardware serial number of this device.
*
* @return serial number or {@code null} if not available.
*/
private static String getDeviceSerialNumber() {
// We're using the Reflection API because Build.SERIAL is only available
// since API Level 9 (Gingerbread, Android 2.3).
try {
return (String) Build.class.getField("SERIAL").get(null);
} catch (Exception ignored) {
return null;
}
}
private static byte[] getBuildFingerprintAndDeviceSerial() {
StringBuilder result = new StringBuilder();
String fingerprint = Build.FINGERPRINT;
if (fingerprint != null) {
result.append(fingerprint);
}
String serial = getDeviceSerialNumber();
if (serial != null) {
result.append(serial);
}
try {
return result.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 encoding not supported");
}
}
}

View File

@@ -26,10 +26,11 @@ import android.content.Context
import androidx.room.AutoMigration
@Database(
version = 2,
version = 3,
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
autoMigrations = [
AutoMigration (from = 1, to = 2)
AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3)
]
)
abstract class AppDatabase : RoomDatabase() {

View File

@@ -27,9 +27,11 @@ import android.net.Uri
import android.os.IBinder
import android.util.Base64
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.services.DeviceUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.IOActionTask
import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -42,19 +44,19 @@ class CipherDatabaseAction(context: Context) {
AppDatabase.getDatabase(applicationContext).cipherDatabaseDao()
// Temp DAO to easily remove content if object no longer in memory
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
private var useTempDao = PreferencesUtil.isTempDeviceUnlockEnable(applicationContext)
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
private var mBinder: DeviceUnlockNotificationService.DeviceUnlockBinder? = null
private var mServiceConnection: ServiceConnection? = null
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
private var mDeviceUnlockBroadcastReceiver = DeviceUnlockNotificationService.DeviceUnlockReceiver {
deleteAll()
removeAllDataAndDetach()
}
fun reloadPreferences() {
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
private fun reloadPreferences() {
useTempDao = PreferencesUtil.isTempDeviceUnlockEnable(applicationContext)
}
@Synchronized
@@ -69,13 +71,15 @@ class CipherDatabaseAction(context: Context) {
@Synchronized
private fun attachService(performedAction: () -> Unit) {
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
})
ContextCompat.registerReceiver(applicationContext, mDeviceUnlockBroadcastReceiver,
IntentFilter().apply {
addAction(DeviceUnlockNotificationService.REMOVE_DEVICE_UNLOCK_KEY_ACTION)
}, ContextCompat.RECEIVER_EXPORTED
)
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
mBinder = (serviceBinder as DeviceUnlockNotificationService.DeviceUnlockBinder)
performedAction.invoke()
}
@@ -84,7 +88,7 @@ class CipherDatabaseAction(context: Context) {
}
}
try {
AdvancedUnlockNotificationService.bindService(applicationContext,
DeviceUnlockNotificationService.bindService(applicationContext,
mServiceConnection!!,
Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
@@ -96,11 +100,11 @@ class CipherDatabaseAction(context: Context) {
@Synchronized
private fun detachService() {
try {
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
} catch (e: Exception) {}
applicationContext.unregisterReceiver(mDeviceUnlockBroadcastReceiver)
} catch (_: Exception) {}
mServiceConnection?.let {
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
DeviceUnlockNotificationService.unbindService(applicationContext, it)
}
}
@@ -120,23 +124,27 @@ class CipherDatabaseAction(context: Context) {
private fun onClear() {
mBinder = null
mServiceConnection = null
mDatabaseListeners.forEach {
it.onCipherDatabaseCleared()
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseCleared()
}
}
interface CipherDatabaseListener {
fun onCipherDatabaseRetrieved(databaseUri: Uri, cipherDatabase: CipherEncryptDatabase?)
fun onCipherDatabaseAddedOrUpdated(cipherDatabase: CipherEncryptDatabase)
fun onCipherDatabaseDeleted(databaseUri: Uri)
fun onAllCipherDatabasesDeleted()
fun onCipherDatabaseCleared()
}
fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
cipherDatabaseResultListener: ((CipherEncryptDatabase?) -> Unit)? = null) {
if (useTempDao) {
serviceActionTask {
var cipherDatabase: CipherEncryptDatabase? = null
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
cipherDatabase = CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
this.databaseUri = cipherDatabaseEntity.databaseUri.toUri()
this.encryptedValue = Base64.decode(
cipherDatabaseEntity.encryptedValue,
BASE64_FLAG
@@ -147,7 +155,11 @@ class CipherDatabaseAction(context: Context) {
)
}
}
cipherDatabaseResultListener.invoke(cipherDatabase)
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
}
}
}
} else {
IOActionTask(
@@ -155,7 +167,7 @@ class CipherDatabaseAction(context: Context) {
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
?.let { cipherDatabaseEntity ->
CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
this.databaseUri = cipherDatabaseEntity.databaseUri.toUri()
this.encryptedValue = Base64.decode(
cipherDatabaseEntity.encryptedValue,
Base64.NO_WRAP
@@ -167,19 +179,35 @@ class CipherDatabaseAction(context: Context) {
}
}
},
{
cipherDatabaseResultListener.invoke(it)
{ cipherDatabase ->
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
}
}
}
).execute()
}
}
fun containsCipherDatabase(databaseUri: Uri,
private fun containsCipherDatabase(databaseUri: Uri?,
contains: (Boolean) -> Unit) {
if (databaseUri == null) {
contains.invoke(false)
} else {
getCipherDatabase(databaseUri) {
contains.invoke(it != null)
}
}
}
fun resetCipherParameters(databaseUri: Uri?) {
containsCipherDatabase(databaseUri) { contains ->
if (contains) {
mBinder?.resetTimer()
}
}
}
fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase,
cipherDatabaseResultListener: (() -> Unit)? = null) {
@@ -195,7 +223,11 @@ class CipherDatabaseAction(context: Context) {
// The only case to create service (not needed to get an info)
serviceActionTask(true) {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke()
cipherDatabaseResultListener?.invoke() ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
}
}
}
} else {
IOActionTask(
@@ -210,7 +242,11 @@ class CipherDatabaseAction(context: Context) {
}
},
{
cipherDatabaseResultListener?.invoke()
cipherDatabaseResultListener?.invoke() ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
}
}
}
).execute()
}
@@ -222,7 +258,11 @@ class CipherDatabaseAction(context: Context) {
if (useTempDao) {
serviceActionTask {
mBinder?.deleteByDatabaseUri(databaseUri)
cipherDatabaseResultListener?.invoke()
cipherDatabaseResultListener?.invoke() ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseDeleted(databaseUri)
}
}
}
} else {
IOActionTask(
@@ -230,10 +270,15 @@ class CipherDatabaseAction(context: Context) {
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener?.invoke()
cipherDatabaseResultListener?.invoke() ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseDeleted(databaseUri)
}
}
}
).execute()
}
reloadPreferences()
}
fun deleteAll() {
@@ -248,8 +293,12 @@ class CipherDatabaseAction(context: Context) {
cipherDatabaseDao.deleteAll()
}
).execute()
mDatabaseListeners.forEach { listener ->
listener.onAllCipherDatabasesDeleted()
}
// Unbind
removeAllDataAndDetach()
reloadPreferences()
}
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {

View File

@@ -49,6 +49,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
databaseUri,
fileDatabaseHistoryEntity?.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
fileDatabaseHistoryEntity?.readOnly,
fileDatabaseHistoryEntity?.databaseUri?.decodeUri(),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias
?: ""),
@@ -99,6 +100,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
fileDatabaseHistoryEntity.databaseUri.parseUri(),
fileDatabaseHistoryEntity.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
fileDatabaseHistoryEntity.readOnly,
fileDatabaseHistoryEntity.databaseUri.decodeUri(),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.exists,
@@ -147,6 +149,8 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(),
databaseFileToAddOrUpdate.hardwareKey?.value,
databaseFileToAddOrUpdate.readOnly
?: fileDatabaseHistoryRetrieve?.readOnly,
System.currentTimeMillis()
)
@@ -168,6 +172,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
fileDatabaseHistory.databaseUri.parseUri(),
fileDatabaseHistory.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
fileDatabaseHistory.readOnly,
fileDatabaseHistory.databaseUri.decodeUri(),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.exists,
@@ -195,6 +200,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
fileDatabaseHistory.databaseUri.parseUri(),
fileDatabaseHistory.keyFileUri?.parseUri(),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
fileDatabaseHistory.readOnly,
fileDatabaseHistory.databaseUri.decodeUri(),
databaseFileToDelete.databaseAlias
)

View File

@@ -38,6 +38,9 @@ data class FileDatabaseHistoryEntity(
@ColumnInfo(name = "hardware_key")
var hardwareKey: String?,
@ColumnInfo(name = "read_only")
var readOnly: Boolean?,
@ColumnInfo(name = "updated")
val updated: Long
) {

View File

@@ -1,6 +0,0 @@
package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
data class AutofillComponent(val assistStructure: AssistStructure,
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)

View File

@@ -1,400 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.*
import android.util.Log
import android.view.autofill.AutofillId
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.CreditCard
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.WebDomain
import org.joda.time.DateTime
@RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
private var mDatabase: ContextualDatabase? = null
private var applicationIdBlocklist: Set<String>? = null
private var webDomainBlocklist: Set<String>? = null
private var askToSaveData: Boolean = false
private var autofillInlineSuggestionsEnabled: Boolean = false
override fun onCreate() {
super.onCreate()
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.registerProgressTask()
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
this.mDatabase = database
}
getPreferences()
}
override fun onDestroy() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onDestroy()
}
private fun getPreferences() {
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
askToSaveData = PreferencesUtil.askToSaveAutofillData(this)
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
}
override fun onFillRequest(request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback) {
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
Log.d(TAG, "Autofill requested in compatibility mode")
} else {
Log.d(TAG, "Autofill requested in native mode")
}
// Check user's settings for authenticating Responses and Datasets.
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse()?.let { parseResult ->
// Build search info only if applicationId or webDomain are not blocked
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request)
} else {
null
}
launchSelection(mDatabase,
searchInfo,
parseResult,
inlineSuggestionsRequest,
callback)
}
}
}
}
private fun launchSelection(database: ContextualDatabase?,
searchInfo: SearchInfo,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
callback.onSuccess(
AutofillHelper.buildResponse(this, openedDatabase,
items, parseResult, inlineSuggestionsRequest)
)
},
{ openedDatabase ->
// Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, inlineSuggestionsRequest, callback)
},
{
// Show UI if database not open
showUIForEntrySelection(parseResult, null,
searchInfo, inlineSuggestionsRequest, callback)
}
)
}
@SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
database: ContextualDatabase?,
searchInfo: SearchInfo,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
var success = false
parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
searchInfo, inlineSuggestionsRequest).intentSender
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_unlock_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_unlock)
}
} else {
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_select_entry)
}
}
// Tell the autofill framework the interest to save credentials
if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val requiredIds = ArrayList<AutofillId>()
val optionalIds = ArrayList<AutofillId>()
// Only if at least a password
parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
requiredIds.add(usernameInfo)
}
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
requiredIds.add(passwordInfo)
}
// or a credit card form
if (requiredIds.isEmpty()) {
parseResult.creditCardNumberId?.let { numberId ->
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
requiredIds.add(numberId)
Log.d(TAG, "Asking to save credit card number")
}
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
}
if (requiredIds.isNotEmpty()) {
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
if (optionalIds.isNotEmpty()) {
builder.setOptionalIds(optionalIds.toTypedArray())
}
responseBuilder.setSaveInfo(builder.build())
}
}
// Build inline presentation
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0) {
val inlinePresentationSpec = inlinePresentationSpecs[0]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) {
// Build the content for IME UI
inlinePresentation = InlinePresentation(
InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(this,
0,
Intent(this, AutofillSettingsActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
})
).apply {
setContentDescription(getString(R.string.autofill_sign_in_prompt))
setTitle(getString(R.string.autofill_sign_in_prompt))
setStartIcon(Icon.createWithResource(this@KeeAutofillService, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false)
}
}
}
// Build response
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
// Buggy method on some API 33 devices
responseBuilder.setAuthentication(
autofillIds,
intentSender,
Presentations.Builder().apply {
inlinePresentation?.let {
setInlinePresentation(it)
}
setDialogPresentation(remoteViewsUnlock)
}.build()
)
} catch (e: Exception) {
Log.e(TAG, "Unable to use the new setAuthentication method.", e)
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
}
} else {
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
}
} else {
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
}
success = true
callback.onSuccess(responseBuilder.build())
}
}
if (!success)
callback.onFailure("Unable to get Autofill ids for UI selection")
}
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
var success = false
if (askToSaveData) {
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse(true)?.let { parseResult ->
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
Log.d(TAG, "autofill onSaveRequest password")
// Build expiration from date or from year and month
var expiration: DateTime? = parseResult.creditCardExpirationValue
if (parseResult.creditCardExpirationValue == null
&& parseResult.creditCardExpirationYearValue != 0
&& parseResult.creditCardExpirationMonthValue != 0) {
expiration = DateTime()
.withYear(parseResult.creditCardExpirationYearValue)
.withMonthOfYear(parseResult.creditCardExpirationMonthValue)
if (parseResult.creditCardExpirationDayValue != 0) {
expiration = expiration.withDayOfMonth(parseResult.creditCardExpirationDayValue)
}
}
// Show UI to save data
val registerInfo = RegisterInfo(
SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
},
parseResult.usernameValue?.textValue?.toString(),
parseResult.passwordValue?.textValue?.toString(),
CreditCard(
parseResult.creditCardHolder,
parseResult.creditCardNumber,
expiration,
parseResult.cardVerificationValue
))
// TODO Callback in each activity #765
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
// registerInfo))
//} else {
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
success = true
callback.onSuccess()
//}
}
}
}
if (!success) {
callback.onFailure("Saving form values is not allowed")
}
}
override fun onConnected() {
Log.d(TAG, "onConnected")
getPreferences()
}
override fun onDisconnected() {
Log.d(TAG, "onDisconnected")
}
companion object {
private val TAG = KeeAutofillService::class.java.name
fun autofillAllowedFor(element: String?, blockList: Set<String>?): Boolean {
element?.let { elementNotNull ->
if (blockList?.any { appIdBlocked ->
elementNotNull.contains(appIdBlocked)
} == true
) {
Log.d(TAG, "Autofill not allowed for $elementNotNull")
return false
}
}
return true
}
}
}

View File

@@ -1,10 +0,0 @@
package com.kunzisoft.keepass.biometric
import androidx.annotation.StringRes
import javax.crypto.Cipher
data class AdvancedUnlockCryptoPrompt(var cipher: Cipher,
@StringRes var promptTitleId: Int,
@StringRes var promptDescriptionId: Int? = null,
var isDeviceCredentialOperation: Boolean,
var isBiometricOperation: Boolean)

View File

@@ -1,677 +0,0 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
private var mBuilderListener: BuilderListener? = null
private var mAdvancedUnlockEnabled = false
private var mAutoOpenPromptEnabled = false
private var advancedUnlockManager: AdvancedUnlockManager? = null
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
private var mAdvancedUnlockInfoView: AdvancedUnlockInfoView? = null
var databaseFileUri: Uri? = null
private set
// TODO Retrieve credential storage from app database
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
// Variable to check if the prompt can be open (if the right activity is currently shown)
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
private var allowOpenBiometricPrompt = false
private lateinit var cipherDatabaseAction : CipherDatabaseAction
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
private var mAddBiometricMenuInProgress = false
// Only keep connection when we request a device credential activity
private var keepConnection = false
private var mDeviceCredentialResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
// To wait resume
if (keepConnection) {
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded =
result.resultCode == Activity.RESULT_OK
}
keepConnection = false
}
private val menuProvider: MenuProvider = object: MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// biometric menu
if (mAllowAdvancedUnlockMenu)
menuInflater.inflate(R.menu.advanced_unlock, menu)
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEncryptedDatabaseKey()
}
}
return false
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context)
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBuilderListener = context as BuilderListener
}
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + BuilderListener::class.java.name)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
initAdvancedUnlockMode()
}
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
checkUnlockAvailability()
}
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
onDatabaseLoaded(it)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.inflate(R.layout.fragment_advanced_unlock, container, false)
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
}
override fun onResume() {
super.onResume()
context?.let {
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it)
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
}
keepConnection = false
}
private fun onDatabaseLoaded(databaseUri: Uri?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// To get device credential unlock result, only if same database uri
if (databaseUri != null
&& mAdvancedUnlockEnabled) {
val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
deviceCredentialAuthSucceeded?.let {
if (databaseUri == databaseFileUri) {
if (deviceCredentialAuthSucceeded == true) {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
}
} else {
disconnect()
}
} ?: run {
if (databaseUri != databaseFileUri) {
connect(databaseUri)
}
}
} else {
disconnect()
}
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
}
}
/**
* Check unlock availability and change the current mode depending of device's state
*/
private fun checkUnlockAvailability() {
context?.let { context ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true
if (PreferencesUtil.isBiometricUnlockEnable(context)) {
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context)
if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else {
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
} else {
selectMode()
}
}
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
if (AdvancedUnlockManager.isDeviceSecure(context)) {
selectMode()
} else {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun selectMode() {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
advancedUnlockManager = AdvancedUnlockManager { requireActivity() }
// callback for fingerprint findings
advancedUnlockManager?.advancedUnlockCallback = this
}
// Recheck to change the mode
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
} else {
if (mBuilderListener?.conditionToStoreCredential() == true) {
// listen for encryption
toggleMode(Mode.STORE_CREDENTIAL)
} else {
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
// biometric available but no stored password found yet for this DB so show info don't listen
toggleMode(if (containsCipher) {
// listen for decryption
Mode.EXTRACT_CREDENTIAL
} else {
// wait for typing
Mode.WAIT_CREDENTIAL
})
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun toggleMode(newBiometricMode: Mode) {
if (newBiometricMode != biometricMode) {
biometricMode = newBiometricMode
initAdvancedUnlockMode()
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initNotAvailable() {
showViews(false)
mAdvancedUnlockInfoView?.setIconViewClickListener(null)
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openBiometricSetting() {
mAdvancedUnlockInfoView?.setIconViewClickListener {
try {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL))
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
@Suppress("DEPRECATION") context
?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL))
}
else -> {
context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
} catch (e: Exception) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initSecurityUpdateRequired() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initNotConfigured() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.configure_biometric)
setAdvancedUnlockedMessageView("")
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initKeyManagerNotAvailable() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initWaitData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.unavailable)
setAdvancedUnlockedMessageView("")
context?.let { context ->
mAdvancedUnlockInfoView?.setIconViewClickListener {
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
context.getString(R.string.credential_before_click_advanced_unlock_button))
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
lifecycleScope.launch(Dispatchers.Main) {
if (allowOpenBiometricPrompt) {
if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true
try {
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
mDeviceCredentialResultLauncher)
} catch (e: Exception) {
Log.e(TAG, "Unable to open advanced unlock prompt", e)
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initEncryptData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric)
setAdvancedUnlockedMessageView("")
advancedUnlockManager?.initEncryptData { cryptoPrompt ->
// Set listener to open the biometric dialog and save credential
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
openAdvancedUnlockPrompt(cryptoPrompt)
}
} ?: throw Exception("AdvancedUnlockManager not initialized")
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initDecryptData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.unlock)
setAdvancedUnlockedMessageView("")
advancedUnlockManager?.let { unlockHelper ->
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
cipherDatabase?.let {
unlockHelper.initDecryptData(it.specParameters) { cryptoPrompt ->
// Set listener to open the biometric dialog and check credential
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
openAdvancedUnlockPrompt(cryptoPrompt)
}
// Auto open the biometric prompt
if (mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt
&& mAutoOpenPromptEnabled) {
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
openAdvancedUnlockPrompt(cryptoPrompt)
}
}
} ?: deleteEncryptedDatabaseKey()
}
} ?: throw UnknownDatabaseLocationException()
} ?: throw Exception("AdvancedUnlockManager not initialized")
}
private fun initAdvancedUnlockMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mAllowAdvancedUnlockMenu = false
try {
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
Mode.WAIT_CREDENTIAL -> initWaitData()
Mode.STORE_CREDENTIAL -> initEncryptData()
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
}
} catch (e: Exception) {
onGenericException(e)
}
invalidateBiometricMenu()
}
}
private fun invalidateBiometricMenu() {
// Show fingerprint key deletion
if (!mAddBiometricMenuInProgress) {
mAddBiometricMenuInProgress = true
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
mAllowAdvancedUnlockMenu = containsCipher
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
mAddBiometricMenuInProgress = false
activity?.invalidateOptionsMenu()
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun connect(databaseUri: Uri) {
showViews(true)
this.databaseFileUri = databaseUri
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
override fun onCipherDatabaseCleared() {
advancedUnlockManager?.closeBiometricPrompt()
checkUnlockAvailability()
}
}
cipherDatabaseAction.apply {
reloadPreferences()
cipherDatabaseListener?.let {
registerDatabaseListener(it)
}
}
checkUnlockAvailability()
}
@RequiresApi(Build.VERSION_CODES.M)
fun disconnect(hideViews: Boolean = true,
closePrompt: Boolean = true) {
this.databaseFileUri = null
// Close the biometric prompt
allowOpenBiometricPrompt = false
if (closePrompt)
advancedUnlockManager?.closeBiometricPrompt()
cipherDatabaseListener?.let {
cipherDatabaseAction.unregisterDatabaseListener(it)
}
biometricMode = Mode.BIOMETRIC_UNAVAILABLE
if (hideViews) {
showViews(false)
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() {
mAllowAdvancedUnlockMenu = false
advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
checkUnlockAvailability()
}
} ?: checkUnlockAvailability()
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationFailed() {
lifecycleScope.launch(Dispatchers.Main) {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationSucceeded() {
lifecycleScope.launch(Dispatchers.Main) {
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> {
}
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
}
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> {
}
Mode.KEY_MANAGER_UNAVAILABLE -> {
}
Mode.WAIT_CREDENTIAL -> {
}
Mode.STORE_CREDENTIAL -> {
// newly store the entered password in encrypted way
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
advancedUnlockManager?.encryptData(credential)
}
}
Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
cipherDatabase?.encryptedValue?.let { value ->
advancedUnlockManager?.decryptData(value)
} ?: deleteEncryptedDatabaseKey()
}
} ?: run {
onAuthenticationError(-1, getString(R.string.error_database_uri_null))
}
}
}
}
}
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialEncrypted(
CipherEncryptDatabase().apply {
this.databaseUri = databaseUri
this.credentialStorage = credentialDatabaseStorage
this.encryptedValue = encryptedValue
this.specParameters = ivSpec
}
)
}
}
override fun handleDecryptedResult(decryptedValue: ByteArray) {
// Load database directly with password retrieve
databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialDecrypted(
CipherDecryptDatabase().apply {
this.databaseUri = databaseUri
this.credentialStorage = credentialDatabaseStorage
this.decryptedValue = decryptedValue
}
)
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onUnrecoverableKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onGenericException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage)
}
private fun showViews(show: Boolean) {
lifecycleScope.launch(Dispatchers.Main) {
if (show) {
if (mAdvancedUnlockInfoView?.visibility != View.VISIBLE)
mAdvancedUnlockInfoView?.showByFading()
}
else {
if (mAdvancedUnlockInfoView?.visibility == View.VISIBLE)
mAdvancedUnlockInfoView?.hideByFading()
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedTitleView(textId: Int) {
lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.setTitle(textId)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(textId: Int) {
lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.setMessage(textId)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.setMessage(text)
}
}
enum class Mode {
BIOMETRIC_UNAVAILABLE,
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
KEY_MANAGER_UNAVAILABLE,
WAIT_CREDENTIAL,
STORE_CREDENTIAL,
EXTRACT_CREDENTIAL
}
interface BuilderListener {
fun retrieveCredentialForEncryption(): ByteArray
fun conditionToStoreCredential(): Boolean
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
}
override fun onPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!keepConnection) {
// If close prompt, bug "user not authenticated in Android R"
disconnect(false)
advancedUnlockManager = null
}
}
super.onPause()
}
override fun onDestroyView() {
mAdvancedUnlockInfoView = null
super.onDestroyView()
}
override fun onDestroy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
disconnect()
advancedUnlockManager = null
mBuilderListener = null
}
super.onDestroy()
}
override fun onDetach() {
mBuilderListener = null
super.onDetach()
}
companion object {
private val TAG = AdvancedUnlockFragment::class.java.name
}
}

View File

@@ -1,506 +0,0 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.biometric
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.security.KeyStore
import java.security.UnrecoverableKeyException
import java.util.concurrent.Executors
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
@RequiresApi(api = Build.VERSION_CODES.M)
class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) {
private var keyStore: KeyStore? = null
private var keyGenerator: KeyGenerator? = null
private var cipher: Cipher? = null
private var biometricPrompt: BiometricPrompt? = null
private var authenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
advancedUnlockCallback?.onAuthenticationSucceeded()
}
override fun onAuthenticationFailed() {
advancedUnlockCallback?.onAuthenticationFailed()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
advancedUnlockCallback?.onAuthenticationError(errorCode, errString)
}
}
var advancedUnlockCallback: AdvancedUnlockCallback? = null
private var isKeyManagerInit = false
private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext())
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext())
val isKeyManagerInitialized: Boolean
get() {
if (!isKeyManagerInit) {
advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized"))
}
return isKeyManagerInit
}
private fun isBiometricOperation(): Boolean {
return biometricUnlockEnable || isDeviceCredentialBiometricOperation()
}
// Since Android 30, device credential is also a biometric operation
private fun isDeviceCredentialOperation(): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
private fun isDeviceCredentialBiometricOperation(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
init {
if (isDeviceSecure(retrieveContext())
&& (biometricUnlockEnable || deviceCredentialUnlockEnable)) {
try {
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE)
this.cipher = Cipher.getInstance(
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING)
isKeyManagerInit = (keyStore != null
&& keyGenerator != null
&& cipher != null)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize the keystore", e)
isKeyManagerInit = false
advancedUnlockCallback?.onGenericException(e)
}
} else {
// really not much to do when no fingerprint support found
isKeyManagerInit = false
}
}
@Synchronized private fun getSecretKey(): SecretKey? {
if (!isKeyManagerInitialized) {
return null
}
try {
// Create new key if needed
keyStore?.let { keyStore ->
keyStore.load(null)
try {
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init(
KeyGenParameterSpec.Builder(
ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
.apply {
// Require the user to authenticate with a fingerprint to authorize every use
// of the key, don't use it for device credential because it's the user authentication
if (biometricUnlockEnable) {
setUserAuthenticationRequired(true)
}
// To store in the security chip
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& retrieveContext().packageManager.hasSystemFeature(
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
setIsStrongBoxBacked(true)
}
}
.build())
keyGenerator?.generateKey()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create a key in keystore", e)
advancedUnlockCallback?.onGenericException(e)
}
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve the key in keystore", e)
advancedUnlockCallback?.onGenericException(e)
}
return null
}
@Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
initEncryptData(actionIfCypherInit, true)
}
@Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean) {
if (!isKeyManagerInitialized) {
return
}
try {
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
actionIfCypherInit.invoke(
AdvancedUnlockCryptoPrompt(
cipher,
R.string.advanced_unlock_prompt_store_credential_title,
R.string.advanced_unlock_prompt_store_credential_message,
isDeviceCredentialOperation(), isBiometricOperation())
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initEncryptData(actionIfCypherInit, false)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Synchronized fun encryptData(value: ByteArray) {
if (!isKeyManagerInitialized) {
return
}
try {
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
// passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to encrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Synchronized fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
initDecryptData(ivSpecValue, actionIfCypherInit, true)
}
@Synchronized private fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) {
return
}
try {
// important to restore spec here that was used for decryption
val spec = IvParameterSpec(ivSpecValue)
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
actionIfCypherInit.invoke(
AdvancedUnlockCryptoPrompt(
cipher,
R.string.advanced_unlock_prompt_extract_credential_title,
null,
isDeviceCredentialOperation(), isBiometricOperation())
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
if (firstLaunch) {
deleteKeystoreKey()
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
}
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Synchronized fun decryptData(encryptedValue: ByteArray) {
if (!isKeyManagerInitialized) {
return
}
try {
// actual decryption here
cipher?.doFinal(encryptedValue)?.let { decrypted ->
advancedUnlockCallback?.handleDecryptedResult(decrypted)
}
} catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException)
advancedUnlockCallback?.onInvalidKeyException(badPaddingException)
} catch (e: Exception) {
Log.e(TAG, "Unable to decrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Synchronized fun deleteKeystoreKey() {
try {
keyStore?.load(null)
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete entry key in keystore", e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Synchronized fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
) {
// Init advanced unlock prompt
if (biometricPrompt == null) {
biometricPrompt = BiometricPrompt(retrieveContext(),
Executors.newSingleThreadExecutor(),
authenticationCallback)
}
val promptTitle = retrieveContext().getString(cryptoPrompt.promptTitleId)
val promptDescription = cryptoPrompt.promptDescriptionId?.let { descriptionId ->
retrieveContext().getString(descriptionId)
} ?: ""
if (cryptoPrompt.isBiometricOperation) {
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
setTitle(promptTitle)
if (promptDescription.isNotEmpty())
setDescription(promptDescription)
setConfirmationRequired(false)
if (isDeviceCredentialBiometricOperation()) {
setAllowedAuthenticators(DEVICE_CREDENTIAL)
} else {
setNegativeButtonText(retrieveContext().getString(android.R.string.cancel))
}
}.build()
biometricPrompt?.authenticate(
promptInfoExtractCredential,
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
}
else if (cryptoPrompt.isDeviceCredentialOperation) {
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
@Suppress("DEPRECATION")
deviceCredentialResultLauncher.launch(
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
)
}
}
@Synchronized fun closeBiometricPrompt() {
biometricPrompt?.cancelAuthentication()
}
interface AdvancedUnlockErrorCallback {
fun onUnrecoverableKeyException(e: Exception)
fun onInvalidKeyException(e: Exception)
fun onGenericException(e: Exception)
}
interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback {
fun onAuthenticationSucceeded()
fun onAuthenticationFailed()
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
fun handleDecryptedResult(decryptedValue: ByteArray)
}
companion object {
private val TAG = AdvancedUnlockManager::class.java.name
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
@RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int {
return try {
BiometricManager.from(context).canAuthenticate(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
} else {
BIOMETRIC_STRONG
}
)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
} else {
BIOMETRIC_WEAK
}
)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
}
}
fun isDeviceSecure(context: Context): Boolean {
return ContextCompat.getSystemService(context, KeyguardManager::class.java)
?.isDeviceSecure ?: false
}
fun biometricUnlockSupported(context: Context): Boolean {
val biometricCanAuthenticate = try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
}
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
}
fun deviceCredentialUnlockSupported(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
(biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
} else {
true
}
}
/**
* Remove entry key in keystore
*/
fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
advancedCallback: AdvancedUnlockErrorCallback) {
AdvancedUnlockManager{ fragmentActivity }.apply {
advancedUnlockCallback = object : AdvancedUnlockCallback {
override fun onAuthenticationSucceeded() {}
override fun onAuthenticationFailed() {}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {}
override fun handleDecryptedResult(decryptedValue: ByteArray) {}
override fun onUnrecoverableKeyException(e: Exception) {
advancedCallback.onUnrecoverableKeyException(e)
}
override fun onInvalidKeyException(e: Exception) {
advancedCallback.onInvalidKeyException(e)
}
override fun onGenericException(e: Exception) {
advancedCallback.onGenericException(e)
}
}
deleteKeystoreKey()
}
}
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEntryKeyInKeystoreForBiometric(
activity,
object : AdvancedUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(activity,
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show()
}
override fun onUnrecoverableKeyException(e: Exception) {
showException(e)
}
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onGenericException(e: Exception) {
showException(e)
}
})
}
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
}
}
}

View File

@@ -0,0 +1,21 @@
package com.kunzisoft.keepass.biometric
import androidx.annotation.StringRes
import javax.crypto.Cipher
data class DeviceUnlockCryptoPrompt(
var type: DeviceUnlockCryptoPromptType,
var cipher: Cipher,
@StringRes var titleId: Int,
@StringRes var descriptionId: Int? = null,
var isDeviceCredentialOperation: Boolean,
var isBiometricOperation: Boolean
) {
fun isOldCredentialOperation(): Boolean {
return !isBiometricOperation && isDeviceCredentialOperation
}
}
enum class DeviceUnlockCryptoPromptType {
CREDENTIAL_ENCRYPTION, CREDENTIAL_DECRYPTION
}

View File

@@ -0,0 +1,363 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.app.KeyguardManager
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.view.DeviceUnlockView
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.DeviceUnlockPromptMode
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.concurrent.Executors
@RequiresApi(Build.VERSION_CODES.M)
class DeviceUnlockFragment: Fragment() {
private var mDeviceUnlockView: DeviceUnlockView? = null
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by activityViewModels()
private var mBiometricPrompt: BiometricPrompt? = null
// Only to fix multiple fingerprint menu #332
private var mAllowDeviceUnlockMenu = false
private var mDeviceCredentialResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
mDeviceUnlockViewModel.onAuthenticationSucceeded()
} else {
setAuthenticationFailed()
}
mDeviceUnlockViewModel.biometricPromptClosed()
}
private var biometricAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
mDeviceUnlockViewModel.onAuthenticationSucceeded(result)
mDeviceUnlockViewModel.biometricPromptClosed()
}
override fun onAuthenticationFailed() {
setAuthenticationFailed()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
setAuthenticationError(errorCode, errString)
}
}
private val menuProvider: MenuProvider = object: MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
// biometric menu
if (mAllowDeviceUnlockMenu)
menuInflater.inflate(R.menu.device_unlock, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.menu_keystore_remove_key ->
deleteEncryptedDatabaseKey()
}
return false
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.inflate(R.layout.fragment_device_unlock, container, false)
mDeviceUnlockView = rootView.findViewById(R.id.device_unlock_view)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Init device unlock prompt
mBiometricPrompt = BiometricPrompt(
this@DeviceUnlockFragment,
Executors.newSingleThreadExecutor(),
biometricAuthenticationCallback
)
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
mDeviceUnlockViewModel.uiState.collect { uiState ->
// Change mode
toggleDeviceCredentialMode(uiState.newDeviceUnlockMode)
// Prompt
manageDeviceCredentialPrompt(uiState.cryptoPromptState)
// Advanced menu
mAllowDeviceUnlockMenu = uiState.allowDeviceUnlockMenu
activity?.invalidateOptionsMenu()
}
}
}
}
fun cancelBiometricPrompt() {
lifecycleScope.launch(Dispatchers.Main) {
mBiometricPrompt?.cancelAuthentication()
}
}
private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode) {
lifecycleScope.launch(Dispatchers.Main) {
try {
when (deviceUnlockMode) {
DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode()
DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode()
DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode()
DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode()
DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode()
DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode()
DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode()
}
} catch (e: Exception) {
mDeviceUnlockViewModel.setException(e)
}
}
}
private fun manageDeviceCredentialPrompt(
state: DeviceUnlockPromptMode
) {
mDeviceUnlockViewModel.cryptoPrompt?.let { prompt ->
when (state) {
DeviceUnlockPromptMode.SHOW -> {
openPrompt(prompt)
mDeviceUnlockViewModel.promptShown()
}
DeviceUnlockPromptMode.CLOSE -> {
cancelBiometricPrompt()
mDeviceUnlockViewModel.biometricPromptClosed()
}
else -> {}
}
}
}
private fun openPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) {
lifecycleScope.launch(Dispatchers.Main) {
try {
val promptTitle = getString(cryptoPrompt.titleId)
val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId ->
getString(descriptionId)
} ?: ""
if (cryptoPrompt.isBiometricOperation) {
mBiometricPrompt?.authenticate(
BiometricPrompt.PromptInfo.Builder().apply {
setTitle(promptTitle)
if (promptDescription.isNotEmpty())
setDescription(promptDescription)
setConfirmationRequired(false)
if (isDeviceCredentialBiometricOperation(context)) {
setAllowedAuthenticators(DEVICE_CREDENTIAL)
} else {
setNegativeButtonText(getString(android.R.string.cancel))
}
}.build(),
BiometricPrompt.CryptoObject(cryptoPrompt.cipher)
)
} else if (cryptoPrompt.isDeviceCredentialOperation) {
context?.let { context ->
@Suppress("DEPRECATION")
mDeviceCredentialResultLauncher?.launch(
ContextCompat.getSystemService(
context,
KeyguardManager::class.java
)?.createConfirmDeviceCredentialIntent(
promptTitle,
promptDescription
)
)
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to open prompt", e)
mDeviceUnlockViewModel.setException(e)
}
}
}
private fun setNotAvailableMode() {
showViews(false)
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener(null)
}
private fun openBiometricSetting() {
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
try {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL))
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
@Suppress("DEPRECATION") context
?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL))
}
else -> {
context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
} catch (e: Exception) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
}
private fun setSecurityUpdateRequiredMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.biometric_security_update_required)
openBiometricSetting()
}
private fun setNotConfiguredMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.configure_biometric)
openBiometricSetting()
}
private fun setKeyManagerNotAvailableMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.keystore_not_accessible)
openBiometricSetting()
}
private fun setWaitCredentialMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.unavailable)
context?.let { context ->
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
mDeviceUnlockViewModel.setException(SecurityException(
context.getString(R.string.credential_before_click_device_unlock_button)
))
}
}
}
private fun setStoreCredentialMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.unlock_and_link_biometric)
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ ->
mDeviceUnlockViewModel.showPrompt()
}
}
private fun setExtractCredentialMode() {
showViews(true)
setDeviceUnlockedTitleView(R.string.unlock)
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ ->
mDeviceUnlockViewModel.showPrompt()
}
}
fun deleteEncryptedDatabaseKey() {
mDeviceUnlockViewModel.deleteEncryptedDatabaseKey()
}
private fun showViews(show: Boolean) {
if (show) {
if (mDeviceUnlockView?.visibility != View.VISIBLE)
mDeviceUnlockView?.showByFading()
}
else {
if (mDeviceUnlockView?.visibility == View.VISIBLE)
mDeviceUnlockView?.hideByFading()
}
}
private fun setDeviceUnlockedTitleView(textId: Int) {
mDeviceUnlockView?.setTitle(textId)
}
private fun setAuthenticationError(errorCode: Int, errString: CharSequence) {
mDeviceUnlockViewModel.biometricPromptClosed()
when (errorCode) {
BiometricPrompt.ERROR_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
BiometricPrompt.ERROR_USER_CANCELED -> {
// No operation
Log.i(TAG, "$errString")
}
else -> {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
mDeviceUnlockViewModel.setException(SecurityException(errString.toString()))
}
}
}
private fun setAuthenticationFailed() {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
mDeviceUnlockViewModel.setException(
SecurityException(getString(R.string.device_unlock_not_recognized))
)
}
override fun onPause() {
super.onPause()
cancelBiometricPrompt()
mDeviceUnlockViewModel.clear()
}
override fun onDestroyView() {
mDeviceUnlockView = null
super.onDestroyView()
}
companion object {
private val TAG = DeviceUnlockFragment::class.java.name
}
}

View File

@@ -0,0 +1,430 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.biometric
import android.app.KeyguardManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.security.KeyStore
import java.security.UnrecoverableKeyException
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
@RequiresApi(api = Build.VERSION_CODES.M)
class DeviceUnlockManager(private var appContext: Context) {
private var keyStore: KeyStore? = null
private var keyGenerator: KeyGenerator? = null
private var cipher: Cipher? = null
private var biometricUnlockEnable = isBiometricUnlockEnable(appContext)
private var deviceCredentialUnlockEnable = isDeviceCredentialUnlockEnable(appContext)
init {
if (biometricUnlockEnable || deviceCredentialUnlockEnable) {
if (isDeviceSecure(appContext)) {
try {
this.keyStore = KeyStore.getInstance(DEVICE_UNLOCK_KEYSTORE)
this.keyGenerator = KeyGenerator.getInstance(
DEVICE_UNLOCK_KEY_ALGORITHM,
DEVICE_UNLOCK_KEYSTORE
)
this.cipher = Cipher.getInstance(
DEVICE_UNLOCK_KEY_ALGORITHM + "/"
+ DEVICE_UNLOCK_BLOCKS_MODES + "/"
+ DEVICE_UNLOCK_ENCRYPTION_PADDING
)
if (keyStore == null) {
throw SecurityException("Unable to initialize the keystore")
}
if (keyGenerator == null) {
throw SecurityException("Unable to initialize the key generator")
}
if (cipher == null) {
throw SecurityException("Unable to initialize the cipher")
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize the device unlock manager", e)
throw e
}
} else {
throw SecurityException("Device not secure enough")
}
}
}
@Synchronized private fun getSecretKey(): SecretKey? {
try {
// Create new key if needed
keyStore?.let { keyStore ->
keyStore.load(null)
try {
if (!keyStore.containsAlias(DEVICE_UNLOCK_KEYSTORE_KEY)) {
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init(
KeyGenParameterSpec.Builder(
DEVICE_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(DEVICE_UNLOCK_BLOCKS_MODES)
.setEncryptionPaddings(DEVICE_UNLOCK_ENCRYPTION_PADDING)
.apply {
// Require the user to authenticate with a fingerprint to authorize every use
// of the key, don't use it for device credential because it's the user authentication
if (biometricUnlockEnable) {
setUserAuthenticationRequired(true)
}
// To store in the security chip
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& appContext.packageManager.hasSystemFeature(
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
setIsStrongBoxBacked(true)
}
}
.build())
keyGenerator?.generateKey()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create a key in keystore", e)
throw e
}
return keyStore.getKey(DEVICE_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve the key in keystore", e)
throw e
}
return null
}
@Synchronized fun initEncryptData(
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
) {
initEncryptData(true, actionIfCypherInit)
}
@Synchronized private fun initEncryptData(
firstLaunch: Boolean,
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
) {
try {
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
actionIfCypherInit.invoke(
DeviceUnlockCryptoPrompt(
type = DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION,
cipher = cipher,
titleId = R.string.device_unlock_prompt_store_credential_title,
descriptionId = R.string.device_unlock_prompt_store_credential_message,
isDeviceCredentialOperation = isDeviceCredentialOperation(
deviceCredentialUnlockEnable
),
isBiometricOperation = isBiometricOperation(
biometricUnlockEnable, deviceCredentialUnlockEnable
)
)
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
throw unrecoverableKeyException
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(appContext)
initEncryptData(false, actionIfCypherInit)
} else {
throw invalidKeyException
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e)
throw e
}
}
@Synchronized fun encryptData(
value: ByteArray,
cipher: Cipher?,
handleEncryptedResult: (encryptedValue: ByteArray, ivSpec: ByteArray) -> Unit
) {
try {
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
// passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
handleEncryptedResult.invoke(encrypted, spec.iv)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to encrypt data", e)
throw e
}
}
@Synchronized fun initDecryptData(
ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
) {
initDecryptData(ivSpecValue, true, actionIfCypherInit)
}
@Synchronized private fun initDecryptData(
ivSpecValue: ByteArray,
firstLaunch: Boolean = true,
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
) {
try {
// important to restore spec here that was used for decryption
val spec = IvParameterSpec(ivSpecValue)
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
actionIfCypherInit.invoke(
DeviceUnlockCryptoPrompt(
type = DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION,
cipher = cipher,
titleId = R.string.device_unlock_prompt_extract_credential_title,
descriptionId = null,
isDeviceCredentialOperation = isDeviceCredentialOperation(
deviceCredentialUnlockEnable
),
isBiometricOperation = isBiometricOperation(
biometricUnlockEnable, deviceCredentialUnlockEnable
)
)
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
if (firstLaunch) {
deleteKeystoreKey()
initDecryptData(ivSpecValue, false, actionIfCypherInit)
} else {
throw unrecoverableKeyException
}
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(appContext)
initDecryptData(ivSpecValue, false, actionIfCypherInit)
} else {
throw invalidKeyException
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e)
throw e
}
}
@Synchronized fun decryptData(
encryptedValue: ByteArray,
cipher: Cipher?,
handleDecryptedResult: (decryptedValue: ByteArray) -> Unit
) {
try {
// actual decryption here
cipher?.doFinal(encryptedValue)?.let { decrypted ->
handleDecryptedResult.invoke(decrypted)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to decrypt data", e)
throw e
}
}
@Synchronized fun deleteKeystoreKey() {
try {
keyStore?.load(null)
keyStore?.deleteEntry(DEVICE_UNLOCK_KEYSTORE_KEY)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete entry key in keystore", e)
throw e
}
}
companion object {
private val TAG = DeviceUnlockManager::class.java.name
private const val DEVICE_UNLOCK_KEYSTORE = "AndroidKeyStore"
private const val DEVICE_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
private const val DEVICE_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val DEVICE_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val DEVICE_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
@RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int {
return try {
BiometricManager.from(context).canAuthenticate(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
} else {
BIOMETRIC_STRONG
}
)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
} else {
BIOMETRIC_WEAK
}
)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
}
}
fun isDeviceSecure(context: Context): Boolean {
return ContextCompat.getSystemService(context, KeyguardManager::class.java)
?.isDeviceSecure ?: false
}
fun biometricUnlockSupported(context: Context): Boolean {
val biometricCanAuthenticate = try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
}
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
}
fun deviceCredentialUnlockSupported(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
(biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
} else {
true
}
}
/**
* Remove entry key in keystore
*/
fun deleteEntryKeyInKeystoreForBiometric(
appContext: Context
) {
DeviceUnlockManager(appContext).apply {
deleteKeystoreKey()
}
}
fun deleteAllEntryKeysInKeystoreForBiometric(appContext: Context) {
try {
deleteEntryKeyInKeystoreForBiometric(appContext)
} catch (e: Exception) {
Toast.makeText(appContext,
deviceUnlockError(e, appContext),
Toast.LENGTH_SHORT).show()
} finally {
CipherDatabaseAction.getInstance(appContext).deleteAll()
}
}
}
}
fun deviceUnlockError(error: Throwable, context: Context): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& (error is UnrecoverableKeyException
|| error is KeyPermanentlyInvalidatedException)) {
context.getString(R.string.device_unlock_invalid_key)
} else
error.cause?.localizedMessage
?: error.localizedMessage
?: error.toString()
}
fun isBiometricUnlockEnable(appContext: Context) =
PreferencesUtil.isBiometricUnlockEnable(appContext)
fun isDeviceCredentialUnlockEnable(appContext: Context) =
PreferencesUtil.isDeviceCredentialUnlockEnable(appContext)
private fun isBiometricOperation(
biometricUnlockEnable: Boolean,
deviceCredentialUnlockEnable: Boolean
): Boolean {
return biometricUnlockEnable
|| isDeviceCredentialBiometricOperation(deviceCredentialUnlockEnable)
}
// Since Android 30, device credential is also a biometric operation
private fun isDeviceCredentialOperation(
deviceCredentialUnlockEnable: Boolean
): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
private fun isDeviceCredentialBiometricOperation(
deviceCredentialUnlockEnable: Boolean
): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
fun isDeviceCredentialBiometricOperation(context: Context?): Boolean {
if (context == null) {
return false
}
return isDeviceCredentialBiometricOperation(
isDeviceCredentialUnlockEnable(context)
)
}

View File

@@ -0,0 +1,11 @@
package com.kunzisoft.keepass.biometric
enum class DeviceUnlockMode {
BIOMETRIC_UNAVAILABLE,
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
KEY_MANAGER_UNAVAILABLE,
WAIT_CREDENTIAL,
STORE_CREDENTIAL,
EXTRACT_CREDENTIAL
}

View File

@@ -0,0 +1,424 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.util.Log
import android.widget.RemoteViews
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.getEnum
import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.getParcelableList
import com.kunzisoft.keepass.utils.putEnum
import com.kunzisoft.keepass.utils.putEnumExtra
import com.kunzisoft.keepass.utils.putParcelableList
import java.io.IOException
import java.util.UUID
object EntrySelectionHelper {
private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
private const val EXTRA_NODES_IDS = "com.kunzisoft.keepass.extra.NODES_IDS"
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.NODE_ID"
/**
* Finish the activity by passing the result code and by locking the database if necessary
*/
fun Activity.setActivityResult(
lockDatabase: Boolean = false,
resultCode: Int,
data: Intent? = null
) {
when (resultCode) {
Activity.RESULT_OK ->
this.setResult(resultCode, data)
Activity.RESULT_CANCELED ->
this.setResult(resultCode)
}
this.finish()
if (lockDatabase) {
// Close the database
this.sendBroadcast(Intent(LOCK_ACTION))
}
}
fun startActivityForSearchModeResult(
context: Context,
intent: Intent,
searchInfo: SearchInfo
) {
intent.addSpecialMode(SpecialMode.SEARCH)
intent.addSearchInfo(searchInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}
fun startActivityForSelectionModeResult(
context: Context,
intent: Intent,
typeMode: TypeMode,
searchInfo: SearchInfo?,
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
) {
intent.addSpecialMode(SpecialMode.SELECTION)
intent.addTypeMode(typeMode)
intent.addSearchInfo(searchInfo)
if (activityResultLauncher == null) {
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
}
fun startActivityForRegistrationModeResult(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>?,
intent: Intent,
registerInfo: RegisterInfo?,
typeMode: TypeMode
) {
intent.addSpecialMode(SpecialMode.REGISTRATION)
intent.addTypeMode(typeMode)
intent.addRegisterInfo(registerInfo)
if (activityResultLauncher == null) {
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
}
/**
* Build the special mode response for internal entry selection for one entry
*/
fun Activity.buildSpecialModeResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
this.buildSpecialModeResponseAndSetResult(listOf(entryInfo), extras)
}
/**
* Build the special mode response for internal entry selection for multiple entries
*/
fun Activity.buildSpecialModeResponseAndSetResult(
entriesInfo: List<EntryInfo>,
extras: Bundle? = null
) {
try {
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success special mode manual selection")
mReplyIntent.addNodesIds(entriesInfo.map { it.id })
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the result", e)
setResult(Activity.RESULT_CANCELED)
}
}
fun Intent.addSearchInfo(searchInfo: SearchInfo?): Intent {
searchInfo?.let {
putExtra(KEY_SEARCH_INFO, it)
}
return this
}
fun Bundle.addSearchInfo(searchInfo: SearchInfo?): Bundle {
searchInfo?.let {
putParcelable(KEY_SEARCH_INFO, it)
}
return this
}
fun Intent.retrieveSearchInfo(): SearchInfo? {
return getParcelableExtraCompat(KEY_SEARCH_INFO)
}
fun Bundle.getSearchInfo(): SearchInfo? {
return getParcelableCompat(KEY_SEARCH_INFO)
}
fun Intent.addRegisterInfo(registerInfo: RegisterInfo?): Intent {
registerInfo?.let {
putExtra(KEY_REGISTER_INFO, it)
}
return this
}
fun Bundle.addRegisterInfo(registerInfo: RegisterInfo?): Bundle {
registerInfo?.let {
putParcelable(KEY_REGISTER_INFO, it)
}
return this
}
fun Intent.retrieveRegisterInfo(): RegisterInfo? {
return getParcelableExtraCompat(KEY_REGISTER_INFO)
}
fun Bundle.getRegisterInfo(): RegisterInfo? {
return getParcelableCompat(KEY_REGISTER_INFO)
}
fun Intent.removeInfo() {
removeExtra(KEY_SEARCH_INFO)
removeExtra(KEY_REGISTER_INFO)
}
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
return this
}
fun Bundle.addSpecialMode(specialMode: SpecialMode): Bundle {
this.putEnum(KEY_SPECIAL_MODE, specialMode)
return this
}
fun Intent.retrieveSpecialMode(): SpecialMode {
return this.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
}
fun Bundle.getSpecialMode(): SpecialMode {
return this.getEnum<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
}
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
return this
}
fun Intent.retrieveTypeMode(): TypeMode {
return getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
}
fun Intent.removeModes() {
removeExtra(KEY_SPECIAL_MODE)
removeExtra(KEY_TYPE_MODE)
}
fun Intent.addNodesIds(nodesIds: List<UUID>): Intent {
this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
return this
}
fun Intent.retrieveNodesIds(): List<UUID>? {
return getParcelableList<ParcelUuid>(EXTRA_NODES_IDS)?.map { it.uuid }
}
fun Intent.removeNodesIds() {
removeExtra(EXTRA_NODES_IDS)
}
/**
* Add the node id to the intent
*/
fun Intent.addNodeId(nodeId: UUID?) {
nodeId?.let {
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
}
}
/**
* Retrieve the node id from the intent
*/
fun Intent.retrieveNodeId(): UUID? {
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
}
fun Intent.removeNodeId() {
removeExtra(EXTRA_NODE_ID)
}
/**
* Retrieve nodes ids from intent and get the corresponding entry info list in [database]
*/
fun Intent.retrieveAndRemoveEntries(database: ContextualDatabase): List<EntryInfo> {
val nodesIds = retrieveNodesIds()
?: throw IOException("NodesIds is null")
removeNodesIds()
return nodesIds.mapNotNull { nodeId ->
database
.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
}
}
/**
* Intent sender uses special retains data in callback
*/
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
return (specialMode == SpecialMode.SELECTION
&& (typeMode == TypeMode.MAGIKEYBOARD || typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|| (specialMode == SpecialMode.REGISTRATION
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
}
fun doSpecialAction(
intent: Intent,
defaultAction: () -> Unit,
searchAction: (searchInfo: SearchInfo) -> Unit,
selectionAction: (
intentSenderMode: Boolean,
typeMode: TypeMode,
searchInfo: SearchInfo?
) -> Unit,
registrationAction: (
intentSenderMode: Boolean,
typeMode: TypeMode,
registerInfo: RegisterInfo?
) -> Unit
) {
when (val specialMode = intent.retrieveSpecialMode()) {
SpecialMode.DEFAULT -> {
intent.removeModes()
intent.removeInfo()
defaultAction.invoke()
}
SpecialMode.SEARCH -> {
val searchInfo = intent.retrieveSearchInfo()
intent.removeModes()
intent.removeInfo()
if (searchInfo != null)
searchAction.invoke(searchInfo)
else {
defaultAction.invoke()
}
}
SpecialMode.SELECTION -> {
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
when (val typeMode = intent.retrieveTypeMode()) {
TypeMode.DEFAULT -> {
intent.removeModes()
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
}
TypeMode.MAGIKEYBOARD -> selectionAction.invoke(
isIntentSenderMode(specialMode, typeMode),
typeMode,
searchInfo
)
TypeMode.PASSKEY ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
selectionAction.invoke(
isIntentSenderMode(specialMode, typeMode),
typeMode,
searchInfo
)
} else
defaultAction.invoke()
TypeMode.AUTOFILL -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
selectionAction.invoke(
isIntentSenderMode(specialMode, typeMode),
typeMode,
searchInfo
)
} else
defaultAction.invoke()
}
}
} else {
if (searchInfo != null)
searchAction.invoke(searchInfo)
else
defaultAction.invoke()
}
}
SpecialMode.REGISTRATION -> {
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
val typeMode = intent.retrieveTypeMode()
val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
if (!intentSenderMode) {
intent.removeModes()
intent.removeInfo()
}
if (registerInfo != null)
registrationAction.invoke(
intentSenderMode,
typeMode,
registerInfo
)
else {
defaultAction.invoke()
}
}
}
}
fun performSelection(items: List<EntryInfo>,
actionPopulateCredentialProvider: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
if (items.size == 1) {
val itemFound = items[0]
actionPopulateCredentialProvider.invoke(itemFound)
} else if (items.size > 1) {
// Select the one we want in the selection
actionEntrySelection.invoke(true)
} else {
// Select an arbitrary one
actionEntrySelection.invoke(false)
}
}
/**
* Method to assign a drawable to a new icon from a database icon
*/
@RequiresApi(Build.VERSION_CODES.M)
fun EntryInfo.buildIcon(
context: Context,
database: ContextualDatabase
): Icon? {
try {
database.iconDrawableFactory.getBitmapFromIcon(context,
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
return IconCompat.createWithBitmap(bitmap).toIcon(context)
}
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
return null
}
}

View File

@@ -1,9 +1,8 @@
package com.kunzisoft.keepass.activities.helpers
package com.kunzisoft.keepass.credentialprovider
enum class SpecialMode {
DEFAULT,
SEARCH,
SAVE,
SELECTION,
REGISTRATION;
}

View File

@@ -0,0 +1,5 @@
package com.kunzisoft.keepass.credentialprovider
enum class TypeMode {
DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.retrieveAutofillComponent
import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() {
private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageSelectionResult(it)
}
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
autofillLauncherViewModel.manageRegistrationResult(it)
}
override fun applyCustomStyle(): Boolean = false
override fun finishActivityIfReloadRequested(): Boolean = true
override fun manageDatabaseInfo(): Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
// To apply the bypass https://github.com/Kunzisoft/KeePassDX/issues/2238
// before managing intent in super class
intent.retrieveSelectionBundle()?.apply {
intent.addSpecialMode(getSpecialMode())
intent.addSearchInfo(getSearchInfo())
intent.addRegisterInfo(getRegisterInfo())
intent.addAutofillComponent(retrieveAutofillComponent())
}
super.onCreate(savedInstanceState)
autofillLauncherViewModel.initialize()
lifecycleScope.launch {
// Initialize the parameters
autofillLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
AutofillLauncherViewModel.UIState.Loading -> {}
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
showBlockRestartMessage()
autofillLauncherViewModel.cancelResult()
}
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
showAutofillSuggestionMessage()
}
}
}
}
lifecycleScope.launch {
// Retrieve the UI
autofillLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.CredentialState.Loading -> {}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@AutofillLauncherActivity,
database = uiState.database,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillSelectionActivityResultLauncher,
)
}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@AutofillLauncherActivity,
database = uiState.database,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillRegistrationActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection(
context = this@AutofillLauncherActivity,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillSelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@AutofillLauncherActivity,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
)
}
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
autofillLauncherViewModel.cancelResult()
}
}
}
}
}
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
private fun showBlockRestartMessage() {
// If item not allowed, show a toast
Toast.makeText(
applicationContext,
R.string.autofill_block_restart,
Toast.LENGTH_LONG
).show()
}
private fun showAutofillSuggestionMessage() {
Toast.makeText(
applicationContext,
R.string.autofill_inline_suggestions_keyboard,
Toast.LENGTH_SHORT
).show()
}
companion object {
private const val KEY_PENDING_INTENT_BUNDLE = "com.kunzisoft.keepass.extra.BUNDLE"
private val TAG = AutofillLauncherActivity::class.java.name
fun Intent.retrieveSelectionBundle(): Bundle? {
return this.getBundleExtra(KEY_PENDING_INTENT_BUNDLE)
}
fun getPendingIntentForSelection(
context: Context,
searchInfo: SearchInfo? = null,
autofillComponent: AutofillComponent
): PendingIntent? {
try {
// Doesn't work with direct extra Parcelable in Android 11 (don't know why?)
// https://github.com/Kunzisoft/KeePassDX/issues/2238
// Wrap into a bundle to bypass the problem
val tempBundle = Bundle().apply {
addSpecialMode(SpecialMode.SELECTION)
addSearchInfo(searchInfo)
addAutofillComponent(autofillComponent)
}
return PendingIntent.getActivity(
context,
randomRequestCode(),
Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
} catch (e: RuntimeException) {
Log.e(TAG, "Unable to create pending intent for selection", e)
return null
}
}
fun getPendingIntentForRegistration(
context: Context,
registerInfo: RegisterInfo
): PendingIntent? {
try {
// Bypass intent issue
val tempBundle = Bundle().apply {
addSpecialMode(SpecialMode.REGISTRATION)
addRegisterInfo(registerInfo)
}
return PendingIntent.getActivity(
context,
randomRequestCode(),
Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
)
} catch (e: RuntimeException) {
Log.e(TAG, "Unable to create pending intent for registration", e)
return null
}
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.EntrySelectionViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
/**
* Activity to search or select entry in database,
* Commonly used with Magikeyboard
*/
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
private val entrySelectionViewModel: EntrySelectionViewModel by viewModels()
private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
entrySelectionViewModel.manageSelectionResult(it)
}
override fun applyCustomStyle() = false
override fun finishActivityIfReloadRequested() = false
override fun manageDatabaseInfo(): Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
entrySelectionViewModel.initialize()
lifecycleScope.launch {
// Initialize the parameters
entrySelectionViewModel.uiState.collect { uiState ->
when (uiState) {
is EntrySelectionViewModel.UIState.Loading -> {}
is EntrySelectionViewModel.UIState.PopulateKeyboard -> {
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(
context = this@EntrySelectionLauncherActivity,
entry = uiState.entryInfo,
toast = true
)
}
is EntrySelectionViewModel.UIState.LaunchFileDatabaseSelectForSearch -> {
FileDatabaseSelectActivity.launchForSearch(
context = this@EntrySelectionLauncherActivity,
searchInfo = uiState.searchInfo
)
finish()
}
is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> {
GroupActivity.launchForSearch(
context = this@EntrySelectionLauncherActivity,
database = uiState.database,
searchInfo = uiState.searchInfo
)
finish()
}
}
}
}
lifecycleScope.launch {
// Retrieve the UI
entrySelectionViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.CredentialState.Loading -> {}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@EntrySelectionLauncherActivity,
database = uiState.database,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mEntrySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@EntrySelectionLauncherActivity,
database = uiState.database,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = null // Null to not get any callback
)
finish()
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection(
context = this@EntrySelectionLauncherActivity,
searchInfo = uiState.searchInfo,
typeMode = uiState.typeMode,
activityResultLauncher = mEntrySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@EntrySelectionLauncherActivity,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode,
activityResultLauncher = null // Null to not get any callback
)
finish()
}
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
entrySelectionViewModel.cancelResult()
}
}
}
}
}
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
entrySelectionViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
override fun onDestroy() {
super.onDestroy()
}
companion object {
fun launch(
context: Context,
searchInfo: SearchInfo? = null
) {
context.startActivity(Intent(
context,
EntrySelectionLauncherActivity::class.java
).apply {
addSearchInfo(searchInfo)
// New task needed because don't launch from an Activity context
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
})
}
}
}

View File

@@ -0,0 +1,170 @@
package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addHardwareKey
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addSeed
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.buildHardwareKeyChallenge
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.isYubikeyDriverAvailable
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.UIState
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
/**
* Special activity to deal with hardware key drivers,
* return the response to the database service once finished
*/
class HardwareKeyActivity: DatabaseModeActivity(){
private val mHardwareKeyLauncherViewModel: HardwareKeyLauncherViewModel by viewModels()
private var activityResultLauncher: ActivityResultLauncher<Intent> =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
mHardwareKeyLauncherViewModel.manageSelectionResult(it)
}
override fun applyCustomStyle(): Boolean = false
override fun showDatabaseDialog(): Boolean = false
override fun manageDatabaseInfo(): Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
mHardwareKeyLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
is UIState.Loading -> {}
is UIState.ShowHardwareKeyDriverNeeded -> {
showHardwareKeyDriverNeeded(
this@HardwareKeyActivity,
uiState.hardwareKey
) {
mDatabaseViewModel.onChallengeResponded(null)
finish()
}
}
is UIState.LaunchChallengeActivityForResponse -> {
// Send to the driver
activityResultLauncher.launch(
buildHardwareKeyChallenge(uiState.challenge)
)
}
is UIState.OnChallengeResponded -> {
mDatabaseViewModel.onChallengeResponded(uiState.response)
}
}
}
}
lifecycleScope.launch {
mHardwareKeyLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
mHardwareKeyLauncherViewModel.cancelResult()
}
else -> {}
}
}
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mHardwareKeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
finish()
}
private fun showHardwareKeyDriverNeeded(
context: Context,
hardwareKey: HardwareKey?,
onDialogDismissed: DialogInterface.OnDismissListener
) {
val builder = AlertDialog.Builder(context)
builder
.setMessage(
context.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
context.openExternalApp(
context.getString(R.string.key_driver_app_id),
context.getString(R.string.key_driver_url)
)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setOnDismissListener(onDialogDismissed)
builder.create().show()
}
companion object {
private val TAG = HardwareKeyActivity::class.java.simpleName
fun launchHardwareKeyActivity(
context: Context,
hardwareKey: HardwareKey,
seed: ByteArray?
) {
context.startActivity(
Intent(
context,
HardwareKeyActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
addHardwareKey(hardwareKey)
addSeed(seed)
})
}
fun isHardwareKeyAvailable(
context: Context,
hardwareKey: HardwareKey?
): Boolean {
if (hardwareKey == null)
return false
return when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
false
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
isYubikeyDriverAvailable(context)
}
}
}
}
}

View File

@@ -0,0 +1,298 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
import java.util.UUID
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class PasskeyLauncherActivity : DatabaseLockActivity() {
private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels()
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
passkeyLauncherViewModel.manageSelectionResult(it)
}
private var mPasskeyRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
passkeyLauncherViewModel.manageRegistrationResult(it)
}
override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Initialize the parameters
passkeyLauncherViewModel.initialize()
// Retrieve the UI
passkeyLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
is PasskeyLauncherViewModel.UIState.Loading -> {
// Nothing to do
}
is PasskeyLauncherViewModel.UIState.ShowAppPrivilegedDialog -> {
showAppPrivilegedDialog(
temptingApp = uiState.temptingApp
)
}
is PasskeyLauncherViewModel.UIState.ShowAppSignatureDialog -> {
showAppSignatureDialog(
temptingApp = uiState.temptingApp,
nodeId = uiState.nodeId
)
}
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
updateEntry(uiState.oldEntry, uiState.newEntry)
}
}
}
}
lifecycleScope.launch {
passkeyLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.CredentialState.Loading -> {}
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.CredentialState.ShowError -> {
toastError(uiState.error)
passkeyLauncherViewModel.cancelResult()
}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForSelection(
context = this@PasskeyLauncherActivity,
database = uiState.database,
typeMode = uiState.typeMode,
searchInfo = uiState.searchInfo,
activityResultLauncher = mPasskeySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
database = uiState.database,
typeMode = uiState.typeMode,
registerInfo = uiState.registerInfo,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForSelection(
context = this@PasskeyLauncherActivity,
typeMode = uiState.typeMode,
searchInfo = uiState.searchInfo,
activityResultLauncher = mPasskeySelectionActivityResultLauncher
)
}
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
typeMode = uiState.typeMode,
registerInfo = uiState.registerInfo,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
)
}
}
}
}
}
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
// TODO When auto save is enabled, WARNING filter by the calling activity
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
}
}
}
override fun viewToInvalidateTimeout(): View? {
return null
}
/**
* Display a dialog that asks the user to add an app to the list of privileged apps.
*/
private fun showAppPrivilegedDialog(
temptingApp: AndroidPrivilegedApp
) {
Log.w(javaClass.simpleName, "No privileged apps file found")
AlertDialog.Builder(this@PasskeyLauncherActivity).apply {
setTitle(getString(R.string.passkeys_privileged_apps_ask_title))
setMessage(StringBuilder()
.append(
getString(
R.string.passkeys_privileged_apps_ask_message,
temptingApp.toString()
)
)
.append("\n\n")
.append(getString(R.string.passkeys_privileged_apps_explanation))
.toString()
)
setPositiveButton(android.R.string.ok) { _, _ ->
passkeyLauncherViewModel.saveCustomPrivilegedApp(
intent = intent,
specialMode = mSpecialMode,
database = mDatabase,
temptingApp = temptingApp
)
}
setNegativeButton(android.R.string.cancel) { _, _ ->
passkeyLauncherViewModel.cancelResult()
}
setOnCancelListener {
passkeyLauncherViewModel.cancelResult()
}
}.create().show()
}
/**
* Display a dialog that asks the user to add an app signature in an existing passkey
*/
private fun showAppSignatureDialog(
temptingApp: AppOrigin,
nodeId: UUID
) {
AlertDialog.Builder(this@PasskeyLauncherActivity).apply {
setTitle(getString(R.string.passkeys_missing_signature_app_ask_title))
setMessage(StringBuilder()
.append(
getString(
R.string.passkeys_missing_signature_app_ask_message,
temptingApp.toString()
)
)
.append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
.append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_question))
.toString()
)
setPositiveButton(android.R.string.ok) { _, _ ->
passkeyLauncherViewModel.saveAppSignature(
database = mDatabase,
temptingApp = temptingApp,
nodeId = nodeId
)
}
setNegativeButton(android.R.string.cancel) { _, _ ->
passkeyLauncherViewModel.cancelResult()
}
setOnCancelListener {
passkeyLauncherViewModel.cancelResult()
}
}.create().show()
}
companion object {
private val TAG = PasskeyLauncherActivity::class.java.name
/**
* Get a pending intent to launch the passkey launcher activity
* [nodeId] can be :
* - null if manual selection is requested
* - null if manual registration is requested
* - an entry node id if direct selection is requested
* - a group node id if direct registration is requested in a default group
* - an entry node id if overwriting is requested in an existing entry
*/
fun getPendingIntent(
context: Context,
specialMode: SpecialMode,
searchInfo: SearchInfo? = null,
appOrigin: AppOrigin? = null,
nodeId: UUID? = null
): PendingIntent? {
return PendingIntent.getActivity(
context,
randomRequestCode(),
Intent(context, PasskeyLauncherActivity::class.java).apply {
addSpecialMode(specialMode)
addTypeMode(TypeMode.PASSKEY)
addSearchInfo(searchInfo)
addAppOrigin(appOrigin)
addNodeId(nodeId)
addAuthCode(nodeId)
},
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
}
}

View File

@@ -0,0 +1,8 @@
package com.kunzisoft.keepass.credentialprovider.autofill
import android.app.assist.AssistStructure
data class AutofillComponent(
val assistStructure: AssistStructure,
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?
)

View File

@@ -17,10 +17,9 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill
package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent
import android.app.assist.AssistStructure
import android.content.Context
@@ -28,6 +27,7 @@ import android.content.Intent
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
import android.service.autofill.Dataset
import android.service.autofill.Field
import android.service.autofill.FillResponse
@@ -38,19 +38,14 @@ import android.view.autofill.AutofillId
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import android.widget.Toast
import android.widget.inline.InlinePresentationSpec
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.template.TemplateField
@@ -58,21 +53,63 @@ import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import java.io.IOException
import kotlin.math.min
@RequiresApi(api = Build.VERSION_CODES.O)
object AutofillHelper {
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
private const val EXTRA_BASE_STRUCTURE = "com.kunzisoft.keepass.autofill.BASE_STRUCTURE"
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
fun Intent.addAutofillComponent(autofillComponent: AutofillComponent?): Intent {
autofillComponent?.let {
this.putExtra(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
autofillComponent.compatInlineSuggestionsRequest?.let {
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
}
return this
}
fun Intent.retrieveAutofillComponent(): AutofillComponent? {
this.getParcelableExtraCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AutofillComponent(assistStructure,
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
AutofillComponent(
assistStructure,
this.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
} else {
AutofillComponent(assistStructure, null)
}
}
return null
}
fun Bundle.addAutofillComponent(autofillComponent: AutofillComponent?): Bundle {
autofillComponent?.let {
this.putParcelable(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
autofillComponent.compatInlineSuggestionsRequest?.let {
this.putParcelable(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
}
return this
}
fun Bundle.retrieveAutofillComponent(): AutofillComponent? {
this.getParcelableCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AutofillComponent(
assistStructure,
this.getParcelableCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)
)
} else {
AutofillComponent(assistStructure, null)
}
@@ -131,11 +168,13 @@ object AutofillHelper {
return this
}
private fun buildDatasetForEntry(context: Context,
private fun buildDatasetForEntry(
context: Context,
database: ContextualDatabase,
entryInfo: EntryInfo,
struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset {
inlinePresentation: InlinePresentation?
): Dataset {
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -175,8 +214,8 @@ object AutofillHelper {
}
if (entryInfo.expires) {
val year = entryInfo.expiryTime.getYearInt()
val month = entryInfo.expiryTime.getMonthInt()
val year = entryInfo.expiryTime.getYear()
val month = entryInfo.expiryTime.getMonth()
val monthString = month.toString().padStart(2, '0')
val day = entryInfo.expiryTime.getDay()
val dayString = day.toString().padStart(2, '0')
@@ -191,7 +230,7 @@ object AutofillHelper {
} else {
datasetBuilder.addValueToDatasetBuilder(
it,
AutofillValue.forDate(entryInfo.expiryTime.date.time)
AutofillValue.forDate(entryInfo.expiryTime.toMilliseconds())
)
}
}
@@ -262,7 +301,7 @@ object AutofillHelper {
}
}
}
for (field in entryInfo.customFields) {
for (field in entryInfo.getCustomFieldsForFilling()) {
if (field.name == TemplateField.LABEL_HOLDER) {
struct.creditCardHolderId?.let { ccNameId ->
datasetBuilder.addValueToDatasetBuilder(
@@ -293,38 +332,24 @@ object AutofillHelper {
return dataset
}
/**
* Method to assign a drawable to a new icon from a database icon
*/
private fun buildIconFromEntry(context: Context,
database: ContextualDatabase,
entryInfo: EntryInfo): Icon? {
try {
database.iconDrawableFactory.getBitmapFromIcon(context,
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
return Icon.createWithBitmap(bitmap)
}
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
return null
}
@SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R)
private fun buildInlinePresentationForEntry(context: Context,
private fun buildInlinePresentationForEntry(
context: Context,
database: ContextualDatabase,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? {
entryInfo: EntryInfo
): InlinePresentation? {
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem
) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
if (positionItem <= maxSuggestion - 1) {
// If positionItem is larger than the number of specs in the list, then
// the last spec is used for the remainder of the suggestions
val inlinePresentationSpec = inlinePresentationSpecs[min(positionItem, inlinePresentationSpecs.size - 1)]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
@@ -334,13 +359,9 @@ object AutofillHelper {
// Build the content for IME UI
val pendingIntent = PendingIntent.getActivity(
context,
0,
randomRequestCode(),
Intent(context, AutofillSettingsActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
)
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
@@ -351,7 +372,7 @@ object AutofillHelper {
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
entryInfo.buildIcon(context, database)?.let { icon ->
setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST)
})
@@ -365,9 +386,11 @@ object AutofillHelper {
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
private fun buildInlinePresentationForManualSelection(context: Context,
private fun buildInlinePresentationForManualSelection(
context: Context,
inlinePresentationSpec: InlinePresentationSpec,
pendingIntent: PendingIntent): InlinePresentation? {
pendingIntent: PendingIntent
): InlinePresentation? {
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
@@ -384,11 +407,13 @@ object AutofillHelper {
}.build().slice, inlinePresentationSpec, false)
}
fun buildResponse(context: Context,
fun buildResponse(
context: Context,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
autofillComponent: AutofillComponent
): FillResponse? {
val responseBuilder = FillResponse.Builder()
// Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -409,7 +434,8 @@ object AutofillHelper {
// Add inline suggestion for new IME and dataset
var numberInlineSuggestions = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
@@ -425,21 +451,27 @@ object AutofillHelper {
var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& numberInlineSuggestions > 0
&& compatInlineSuggestionsRequest != null) {
&& autofillComponent.compatInlineSuggestionsRequest != null) {
inlinePresentation = buildInlinePresentationForEntry(
context,
database,
compatInlineSuggestionsRequest,
autofillComponent.compatInlineSuggestionsRequest,
numberInlineSuggestions--,
entry
)
}
// Create dataset for each entry
responseBuilder.addDataset(
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
buildDatasetForEntry(
context = context,
database = database,
entryInfo = entry,
struct = parseResult,
inlinePresentation = inlinePresentation
)
)
} catch (e: Exception) {
Log.e(TAG, "Unable to add dataset")
Log.e(TAG, "Unable to add dataset", e)
}
}
@@ -451,15 +483,27 @@ object AutofillHelper {
webScheme = parseResult.webScheme
manualSelection = true
}
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
searchInfo, compatInlineSuggestionsRequest)
val manualSelectionView = RemoteViews(
context.packageName,
R.layout.item_autofill_select_entry
)
AutofillLauncherActivity.getPendingIntentForSelection(
context,
searchInfo,
autofillComponent
)?.let { pendingIntent ->
var inlinePresentation: InlinePresentation? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpec =
inlineSuggestionsRequest.inlinePresentationSpecs[0]
inlinePresentation = buildInlinePresentationForManualSelection(
context,
inlinePresentationSpec,
pendingIntent
)
}
}
@@ -494,6 +538,7 @@ object AutofillHelper {
responseBuilder.addDataset(dataset)
}
}
}
return try {
responseBuilder.build()
@@ -504,92 +549,33 @@ object AutofillHelper {
}
/**
* Build the Autofill response for one entry
* Build the Autofill response
*/
fun buildResponseAndSetResult(activity: Activity,
database: ContextualDatabase,
entryInfo: EntryInfo) {
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
}
/**
* Build the Autofill response for many entry
*/
fun buildResponseAndSetResult(activity: Activity,
database: ContextualDatabase,
entriesInfo: List<EntryInfo>) {
if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED)
} else {
var setResultOk = false
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
StructureParser(structure).parse()?.let { result ->
// New Response
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
if (compatInlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
}
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
} else {
buildResponse(activity, database, entriesInfo, result, null)
}
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Success Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
response)
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
}
}
if (!setResultOk) {
Log.w(activity.javaClass.name, "Failed Autofill auth.")
activity.setResult(Activity.RESULT_CANCELED)
}
}
}
fun buildActivityResultLauncher(activity: AppCompatActivity,
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
// Utility method to loop and close each activity with return data
if (it.resultCode == Activity.RESULT_OK) {
activity.setResult(it.resultCode, it.data)
}
if (it.resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED)
}
activity.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
// Close the database
activity.sendBroadcast(Intent(LOCK_ACTION))
}
}
}
/**
* Utility method to start an activity with an Autofill for result
*/
fun startActivityForAutofillResult(activity: AppCompatActivity,
intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
fun buildResponse(
context: Context,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
autofillComponent.compatInlineSuggestionsRequest?.let {
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
database: ContextualDatabase,
entriesInfo: List<EntryInfo>,
onIntentCreated: (Intent) -> Unit
) {
if (entriesInfo.isEmpty()) {
throw IOException("No entries found")
} else {
StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
// New Response
onIntentCreated(Intent().putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
buildResponse(
context = context,
database = database,
entriesInfo = entriesInfo,
parseResult = result,
autofillComponent = autofillComponent
)
))
} ?: throw IOException("Unable to parse the structure")
}
}
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
activityResultLauncher?.launch(intent)
}
private val TAG = AutofillHelper::class.java.name
}

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill
package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.TargetApi
import android.os.Build

View File

@@ -0,0 +1,470 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.AutofillService
import android.service.autofill.FillCallback
import android.service.autofill.FillRequest
import android.service.autofill.FillResponse
import android.service.autofill.InlinePresentation
import android.service.autofill.Presentations
import android.service.autofill.SaveCallback
import android.service.autofill.SaveInfo
import android.service.autofill.SaveRequest
import android.util.Log
import android.view.autofill.AutofillId
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
import com.kunzisoft.keepass.credentialprovider.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.CreditCard
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import org.joda.time.DateTime
@RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
private var mDatabase: ContextualDatabase? = null
private var applicationIdBlocklist: Set<String>? = null
private var webDomainBlocklist: Set<String>? = null
private var askToSaveData: Boolean = false
private var autofillInlineSuggestionsEnabled: Boolean = false
override fun onCreate() {
super.onCreate()
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.registerProgressTask()
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
this.mDatabase = database
}
getPreferences()
}
override fun onDestroy() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onDestroy()
}
private fun getPreferences() {
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
askToSaveData = PreferencesUtil.askToSaveAutofillData(this)
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
}
override fun onFillRequest(
request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback
) {
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
Log.d(TAG, "Autofill requested in compatibility mode")
} else {
Log.d(TAG, "Autofill requested in native mode")
}
// Check user's settings for authenticating Responses and Datasets.
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse()?.let { parseResult ->
// Build search info only if applicationId or webDomain are not blocked
if (autofillAllowedFor(
applicationId = parseResult.applicationId,
applicationIdBlocklist = applicationIdBlocklist,
webDomain = parseResult.webDomain,
webDomainBlocklist = webDomainBlocklist)
) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
CompatInlineSuggestionsRequest(request)
} else {
null
}
val autofillComponent = AutofillComponent(
latestStructure,
inlineSuggestionsRequest
)
SearchHelper.checkAutoSearchInfo(
context = this,
database = mDatabase,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
callback.onSuccess(
AutofillHelper.buildResponse(
context = this,
database = openedDatabase,
entriesInfo = items,
parseResult = parseResult,
autofillComponent = autofillComponent
)
)
},
onItemNotFound = { openedDatabase ->
// Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, autofillComponent, callback)
},
onDatabaseClosed = {
// Show UI if database not open
showUIForEntrySelection(parseResult, null,
searchInfo, autofillComponent, callback)
}
)
}
}
}
@SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(
parseResult: StructureParser.Result,
database: ContextualDatabase?,
searchInfo: SearchInfo,
autofillComponent: AutofillComponent,
callback: FillCallback
) {
var success = false
parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
AutofillLauncherActivity.getPendingIntentForSelection(
this,
searchInfo,
autofillComponent
)?.intentSender?.let { intentSender ->
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (database == null) {
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_unlock_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_unlock)
}
} else {
if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_app_id
).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_select_entry)
}
}
// Tell the autofill framework the interest to save credentials
if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val requiredIds = ArrayList<AutofillId>()
val optionalIds = ArrayList<AutofillId>()
// Only if at least a password
parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
requiredIds.add(usernameInfo)
}
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
requiredIds.add(passwordInfo)
}
// or a credit card form
if (requiredIds.isEmpty()) {
parseResult.creditCardNumberId?.let { numberId ->
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
requiredIds.add(numberId)
Log.d(TAG, "Asking to save credit card number")
}
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
}
if (requiredIds.isNotEmpty()) {
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
if (optionalIds.isNotEmpty()) {
builder.setOptionalIds(optionalIds.toTypedArray())
}
responseBuilder.setSaveInfo(builder.build())
}
}
// Build inline presentation
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled
) {
var inlinePresentation: InlinePresentation? = null
autofillComponent.compatInlineSuggestionsRequest
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs =
inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.isNotEmpty()
) {
val inlinePresentationSpec = inlinePresentationSpecs[0]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (UiVersions.getVersions(imeStyle)
.contains(UiVersions.INLINE_UI_VERSION_1)
) {
// Build the content for IME UI
inlinePresentation = InlinePresentation(
InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(
this,
randomRequestCode(),
Intent(this, AutofillSettingsActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
).apply {
setContentDescription(getString(R.string.autofill_sign_in_prompt))
setTitle(getString(R.string.autofill_sign_in_prompt))
setStartIcon(
Icon.createWithResource(
this@KeeAutofillService,
R.mipmap.ic_launcher_round
).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false
)
}
}
}
// Build response
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
// Buggy method on some API 33 devices
responseBuilder.setAuthentication(
autofillIds,
intentSender,
Presentations.Builder().apply {
inlinePresentation?.let {
setInlinePresentation(it)
}
setDialogPresentation(remoteViewsUnlock)
}.build()
)
} catch (e: Exception) {
Log.e(TAG, "Unable to use the new setAuthentication method.", e)
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(
autofillIds,
intentSender,
remoteViewsUnlock,
inlinePresentation
)
}
} else {
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(
autofillIds,
intentSender,
remoteViewsUnlock,
inlinePresentation
)
}
} else {
@Suppress("DEPRECATION")
responseBuilder.setAuthentication(
autofillIds,
intentSender,
remoteViewsUnlock
)
}
success = true
callback.onSuccess(responseBuilder.build())
}
}
}
if (!success)
callback.onFailure("Unable to get Autofill ids for UI selection")
}
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
var success = false
if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val latestStructure = request.fillContexts.last().structure
StructureParser(latestStructure).parse(true)?.let { parseResult ->
if (autofillAllowedFor(
applicationId = parseResult.applicationId,
applicationIdBlocklist = applicationIdBlocklist,
webDomain = parseResult.webDomain,
webDomainBlocklist = webDomainBlocklist)
) {
Log.d(TAG, "autofill onSaveRequest password")
// Build expiration from date or from year and month
var expiration: DateTime? = parseResult.creditCardExpirationValue
if (parseResult.creditCardExpirationValue == null
&& parseResult.creditCardExpirationYearValue != 0
&& parseResult.creditCardExpirationMonthValue != 0) {
expiration = DateTime()
.withYear(parseResult.creditCardExpirationYearValue)
.withMonthOfYear(parseResult.creditCardExpirationMonthValue)
if (parseResult.creditCardExpirationDayValue != 0) {
expiration = expiration.withDayOfMonth(parseResult.creditCardExpirationDayValue)
}
}
// Show UI to save data
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
val registerInfo = RegisterInfo(
searchInfo = searchInfo,
username = parseResult.usernameValue?.textValue?.toString(),
password = parseResult.passwordValue?.textValue?.toString(),
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
CreditCard(
parseResult.creditCardHolder,
cardNumber,
expiration,
parseResult.cardVerificationValue
)
}
)
AutofillLauncherActivity.getPendingIntentForRegistration(
this,
registerInfo
)?.intentSender?.let { intentSender ->
success = true
callback.onSuccess(intentSender)
}
}
}
}
if (!success) {
callback.onFailure("Saving form values is not allowed")
}
}
override fun onConnected() {
Log.d(TAG, "onConnected")
getPreferences()
}
override fun onDisconnected() {
Log.d(TAG, "onDisconnected")
}
companion object {
private val TAG = KeeAutofillService::class.java.name
fun autofillAllowedFor(applicationId: String?,
webDomain: String?,
context: Context
): Boolean {
return autofillAllowedFor(
applicationId = applicationId,
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(context),
webDomain = webDomain,
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(context))
}
fun autofillAllowedFor(applicationId: String?,
applicationIdBlocklist: Set<String>?,
webDomain: String?,
webDomainBlocklist: Set<String>?
): Boolean {
return autofillAllowedFor(applicationId, applicationIdBlocklist)
// To prevent unrecognized autofill popup id
&& applicationId?.contains(APPLICATION_ID_POPUP_WINDOW) != true
&& autofillAllowedFor(webDomain, webDomainBlocklist)
}
fun autofillAllowedFor(element: String?, blockList: Set<String>?): Boolean {
element?.let { elementNotNull ->
if (blockList?.any { appIdBlocked ->
elementNotNull.contains(appIdBlocked)
} == true
) {
Log.d(TAG, "Autofill not allowed for $elementNotNull")
return false
}
}
return true
}
}
}

View File

@@ -16,7 +16,7 @@
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.autofill
package com.kunzisoft.keepass.credentialprovider.autofill
import android.app.assist.AssistStructure
import android.os.Build
@@ -27,8 +27,7 @@ import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
import androidx.annotation.RequiresApi
import org.joda.time.DateTime
import java.util.*
import kotlin.collections.ArrayList
import java.util.Locale
/**
@@ -37,7 +36,6 @@ import kotlin.collections.ArrayList
@RequiresApi(api = Build.VERSION_CODES.O)
class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null
private var usernameNeeded = true
private var usernameIdCandidate: AutofillId? = null
private var usernameValueCandidate: AutofillValue? = null
@@ -53,7 +51,7 @@ class StructureParser(private val structure: AssistStructure) {
applicationId = windowNode.title.toString().split("/")[0]
Log.d(TAG, "Autofill applicationId: $applicationId")
if (applicationId?.contains("PopupWindow:") == false) {
if (applicationId?.contains(APPLICATION_ID_POPUP_WINDOW) == false) {
if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
}
@@ -105,7 +103,7 @@ class StructureParser(private val structure: AssistStructure) {
if (node.autofillId != null) {
// Parse methods
val hints = node.autofillHints
if (hints != null && hints.isNotEmpty()) {
if (!hints.isNullOrEmpty()) {
if (parseNodeByAutofillHint(node))
returnValue = true
} else if (parseNodeByHtmlAttributes(node))
@@ -134,16 +132,37 @@ class StructureParser(private val structure: AssistStructure) {
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|| it.contains("email", true)
|| it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
|| it.contains("login", true) -> {
// Replace username until we have a password
if (result?.passwordId == null) {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username hint")
Log.d(TAG, "Autofill username hint if no password")
} else {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username hint if password")
}
}
it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
if (usernameIdCandidate == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill phone")
}
}
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
// Password Id changed if it's the second times we are here,
// So the last username candidate is most appropriate
if (result?.passwordId != null && usernameIdCandidate != null) {
result?.usernameId = usernameIdCandidate
result?.usernameValue = usernameValueCandidate
}
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password hint")
return true
// Comment "return" to check all the tree
// return true
}
it.equals("cc-name", true) -> {
Log.d(TAG, "Autofill credit card name hint")
@@ -279,15 +298,20 @@ class StructureParser(private val structure: AssistStructure) {
"type" -> {
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
"tel", "email" -> {
if (result?.passwordId == null) {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
}
"text" -> {
// Assume username is before password
if (result?.passwordId == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
}
"password" -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
@@ -315,31 +339,37 @@ class StructureParser(private val structure: AssistStructure) {
return "0x${"%08x".format(inputType)}"
}
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
val inputType = node.inputType
when (inputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_TEXT -> {
private fun manageTypeText(
node: AssistStructure.ViewNode,
autofillId: AutofillId?,
inputType: Int
): Boolean {
when {
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
if (result?.passwordId == null) {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
}
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_NORMAL,
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
// Assume the username field is before the password field
if (result?.passwordId == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
// Some forms used visible password as username
if (usernameIdCandidate == null && usernameValueCandidate == null) {
if (result?.passwordId == null &&
usernameIdCandidate == null && usernameValueCandidate == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
@@ -347,7 +377,6 @@ class StructureParser(private val structure: AssistStructure) {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
usernameNeeded = false
}
}
inputIsVariationType(inputType,
@@ -367,13 +396,20 @@ class StructureParser(private val structure: AssistStructure) {
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
InputType.TYPE_TEXT_VARIATION_URI) -> {
// Type not used
Log.d(TAG, "Autofill not used android text type: ${showHexInputType(inputType)}")
}
else -> {
Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
}
}
return false
}
InputType.TYPE_CLASS_NUMBER -> {
private fun manageTypeNumber(
node: AssistStructure.ViewNode,
autofillId: AutofillId?,
inputType: Int
): Boolean {
when {
inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
@@ -394,6 +430,37 @@ class StructureParser(private val structure: AssistStructure) {
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
}
}
return false
}
private fun manageTypeNull(
node: AssistStructure.ViewNode,
autofillId: AutofillId?,
inputType: Int
): Boolean {
if (node.className == "android.widget.EditText") {
Log.d(TAG, "Autofill null android input type class: ${showHexInputType(inputType)}" +
", get the EditText node class name!")
if (result?.passwordId == null) {
usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue
}
}
return false
}
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
val inputType = node.inputType
when (inputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_TEXT -> {
return manageTypeText(node, autofillId, inputType)
}
InputType.TYPE_CLASS_NUMBER -> {
return manageTypeNumber(node, autofillId, inputType)
}
InputType.TYPE_NULL -> {
return manageTypeNull(node, autofillId, inputType)
}
}
return false
@@ -422,58 +489,14 @@ class StructureParser(private val structure: AssistStructure) {
var creditCardExpirationDayOptions: Array<CharSequence>? = null
var usernameId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var passwordId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardHolderId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardNumberId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationDateId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationYearId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationMonthId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationDayId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var cardVerificationValueId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>()
@@ -500,13 +523,13 @@ class StructureParser(private val structure: AssistStructure) {
var usernameValue: AutofillValue? = null
set(value) {
if (allowSaveValues && field == null)
if (allowSaveValues)
field = value
}
var passwordValue: AutofillValue? = null
set(value) {
if (allowSaveValues && field == null)
if (allowSaveValues)
field = value
}
@@ -559,5 +582,7 @@ class StructureParser(private val structure: AssistStructure) {
companion object {
private val TAG = StructureParser::class.java.name
const val APPLICATION_ID_POPUP_WINDOW = "PopupWindow:"
}
}

View File

@@ -14,7 +14,7 @@
* the License.
*/
package com.kunzisoft.keepass.magikeyboard;
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
import android.content.Context;
import android.content.res.Resources;

View File

@@ -14,14 +14,14 @@
* the License.
*/
package com.kunzisoft.keepass.magikeyboard;
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY;
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP;
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
import android.content.Context;
import android.content.res.TypedArray;
@@ -52,7 +52,7 @@ import android.widget.TextView;
import androidx.annotation.RequiresApi;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.magikeyboard.Keyboard.Key;
import com.kunzisoft.keepass.credentialprovider.magikeyboard.Keyboard.Key;
import java.util.Arrays;
import java.util.HashMap;

View File

@@ -18,7 +18,7 @@
*
*/
package com.kunzisoft.keepass.magikeyboard
package com.kunzisoft.keepass.credentialprovider.magikeyboard
import android.app.Activity
import android.content.Context
@@ -41,9 +41,10 @@ import androidx.core.graphics.BlendModeCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.adapters.FieldsAdapter
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Field
@@ -324,9 +325,9 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
actionGoAutomatically()
}
KEY_FIELDS -> {
getEntryInfo()?.customFields?.let { customFields ->
getEntryInfo()?.getCustomFieldsForFilling()?.let { customFields ->
fieldsAdapter?.apply {
setFields(customFields.filter { it.name != OTP_TOKEN_FIELD})
setFields(customFields)
notifyDataSetChanged()
}
}
@@ -341,10 +342,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
}
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
SearchHelper.checkAutoSearchInfo(this,
mDatabase,
searchInfo,
{ _, items ->
SearchHelper.checkAutoSearchInfo(
context = this,
database = mDatabase,
searchInfo = searchInfo,
onItemsFound = { _, items ->
performSelection(
items,
{
@@ -361,11 +363,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
}
)
},
{
onItemNotFound = {
// Select if not found
launchEntrySelection(searchInfo)
},
{
onDatabaseClosed = {
// Select if database not opened
removeEntryInfo()
launchEntrySelection(searchInfo)
@@ -460,34 +462,23 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
}
fun performSelection(items: List<EntryInfo>,
fun performSelection(
items: List<EntryInfo>,
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
if (items.size == 1) {
val itemFound = items[0]
actionEntrySelection: (autoSearch: Boolean) -> Unit
) {
EntrySelectionHelper.performSelection(
items = items,
actionPopulateCredentialProvider = { itemFound ->
if (entryUUID != itemFound.id) {
actionPopulateKeyboard.invoke(itemFound)
} else {
// Force selection if magikeyboard already populated
actionEntrySelection.invoke(false)
}
} else if (items.size > 1) {
// Select the one we want in the selection
actionEntrySelection.invoke(true)
} else {
// Select an arbitrary one
actionEntrySelection.invoke(false)
}
}
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
entry: EntryInfo,
toast: Boolean = true) {
// Populate Magikeyboard with entry
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode
EntrySelectionHelper.removeModesFromIntent(activity.intent)
activity.moveTaskToBack(true)
},
actionEntrySelection = actionEntrySelection
)
}
}
}

View File

@@ -0,0 +1,372 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build
import android.os.CancellationSignal
import android.os.OutcomeReceiver
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.BeginCreateCredentialRequest
import androidx.credentials.provider.BeginCreateCredentialResponse
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.CredentialEntry
import androidx.credentials.provider.CredentialProviderService
import androidx.credentials.provider.ProviderClearCredentialStateRequest
import androidx.credentials.provider.PublicKeyCredentialEntry
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil.isPasskeyAutoSelectEnable
import com.kunzisoft.keepass.view.toastError
import java.io.IOException
import java.time.Instant
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class PasskeyProviderService : CredentialProviderService() {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
private var mDatabase: ContextualDatabase? = null
private lateinit var defaultIcon: Icon
private var isAutoSelectAllowed: Boolean = false
override fun onCreate() {
super.onCreate()
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.registerProgressTask()
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
this.mDatabase = database
}
defaultIcon = Icon.createWithResource(
this@PasskeyProviderService,
R.mipmap.ic_launcher_round
).apply {
setTintBlendMode(BlendMode.DST)
}
isAutoSelectAllowed = isPasskeyAutoSelectEnable(this)
}
override fun onDestroy() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onDestroy()
}
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
return SearchInfo().apply {
this.relyingParty = relyingParty
}
}
override fun onBeginGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
) {
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
try {
processGetCredentialsRequest(request) { response ->
callback.onResult(response)
}
} catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
callback.onError(GetCredentialUnknownException())
}
}
private fun processGetCredentialsRequest(
request: BeginGetCredentialRequest,
callback: (BeginGetCredentialResponse?) -> Unit
) {
var knownOption = false
for (option in request.beginGetCredentialOptions) {
when (option) {
is BeginGetPublicKeyCredentialOption -> {
knownOption = true
populatePasskeyData(option) { listCredentials ->
callback(BeginGetCredentialResponse(listCredentials))
}
}
}
}
if (knownOption.not()) {
throw IOException("unknown type of beginGetCredentialOption")
}
}
private fun populatePasskeyData(
option: BeginGetPublicKeyCredentialOption,
callback: (List<CredentialEntry>) -> Unit
) {
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
SearchHelper.checkAutoSearchInfo(
context = this,
database = mDatabase,
searchInfo = searchInfo,
onItemsFound = { database, items ->
Log.d(TAG, "Add pending intent for passkey selection with found items")
for (passkeyEntry in items) {
PasskeyLauncherActivity.getPendingIntent(
context = applicationContext,
specialMode = SpecialMode.SELECTION,
nodeId = passkeyEntry.id,
appOrigin = passkeyEntry.appOrigin
)?.let { usagePendingIntent ->
val passkey = passkeyEntry.passkey
passkeyEntries.add(
PublicKeyCredentialEntry(
context = applicationContext,
username = passkey?.username ?: "Unknown",
icon = passkeyEntry.buildIcon(this@PasskeyProviderService, database)?.apply {
setTintBlendMode(BlendMode.DST)
} ?: defaultIcon,
pendingIntent = usagePendingIntent,
beginGetPublicKeyCredentialOption = option,
displayName = passkeyEntry.getVisualTitle(),
isAutoSelectAllowed = isAutoSelectAllowed
)
)
}
}
callback(passkeyEntries)
},
onItemNotFound = { _ ->
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
Log.d(TAG, "Add pending intent for passkey selection in opened database")
PasskeyLauncherActivity.getPendingIntent(
context = applicationContext,
specialMode = SpecialMode.SELECTION,
searchInfo = searchInfo
)?.let { pendingIntent ->
passkeyEntries.add(
PublicKeyCredentialEntry(
context = applicationContext,
username = getString(R.string.passkey_database_username),
displayName = getString(R.string.passkey_selection_description),
icon = defaultIcon,
pendingIntent = pendingIntent,
beginGetPublicKeyCredentialOption = option,
lastUsedTime = Instant.now(),
isAutoSelectAllowed = isAutoSelectAllowed
)
)
}
callback(passkeyEntries)
},
onDatabaseClosed = {
Log.d(TAG, "Add pending intent for passkey selection in closed database")
// Database is locked, a public key credential entry is shown to unlock it
PasskeyLauncherActivity.getPendingIntent(
context = applicationContext,
specialMode = SpecialMode.SELECTION,
searchInfo = searchInfo
)?.let { pendingIntent ->
passkeyEntries.add(
PublicKeyCredentialEntry(
context = applicationContext,
username = getString(R.string.passkey_database_username),
displayName = getString(R.string.passkey_locked_database_description),
icon = defaultIcon,
pendingIntent = pendingIntent,
beginGetPublicKeyCredentialOption = option,
lastUsedTime = Instant.now(),
isAutoSelectAllowed = isAutoSelectAllowed
)
)
}
callback(passkeyEntries)
}
)
}
override fun onBeginCreateCredentialRequest(
request: BeginCreateCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
) {
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
try {
processCreateCredentialRequest(request) {
callback.onResult(BeginCreateCredentialResponse(it))
}
} catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
toastError(e)
callback.onError(CreateCredentialUnknownException(e.localizedMessage))
}
}
private fun processCreateCredentialRequest(
request: BeginCreateCredentialRequest,
callback: (List<CreateEntry>) -> Unit
) {
when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
// Request is passkey type
handleCreatePasskeyQuery(request, callback)
}
else -> {
// request type not supported
throw IOException("unknown type of BeginCreateCredentialRequest")
}
}
}
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
accountName: String,
searchInfo: SearchInfo?
) {
Log.d(TAG, "Add pending intent for registration in opened database to create new item")
// TODO add a setting to directly store in a specific group
PasskeyLauncherActivity.getPendingIntent(
context = applicationContext,
specialMode = SpecialMode.REGISTRATION,
searchInfo = searchInfo
)?.let { pendingIntent ->
this.add(
CreateEntry(
accountName = accountName,
icon = defaultIcon,
pendingIntent = pendingIntent,
description = getString(R.string.passkey_creation_description)
)
)
}
}
private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
callback: (List<CreateEntry>) -> Unit
) {
val databaseName = mDatabase?.name
val accountName =
if (databaseName?.isBlank() != false)
getString(R.string.passkey_database_username)
else databaseName
val createEntries: MutableList<CreateEntry> = mutableListOf()
val relyingPartyId = PublicKeyCredentialCreationOptions(
requestJson = request.requestJson,
clientDataHash = request.clientDataHash
).relyingPartyEntity.id
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
SearchHelper.checkAutoSearchInfo(
context = this,
database = mDatabase,
searchInfo = searchInfo,
onItemsFound = { database, items ->
if (database.isReadOnly) {
throw RegisterInReadOnlyDatabaseException()
} else {
// To create a new entry
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
/* TODO Overwrite
// To select an existing entry and permit an overwrite
Log.w(TAG, "Passkey already registered")
for (entryInfo in items) {
PasskeyHelper.getPendingIntent(
context = applicationContext,
specialMode = SpecialMode.REGISTRATION,
searchInfo = searchInfo,
passkeyEntryNodeId = entryInfo.id
)?.let { createPendingIntent ->
createEntries.add(
CreateEntry(
accountName = accountName,
pendingIntent = createPendingIntent,
description = getString(
R.string.passkey_update_description,
entryInfo.passkey?.displayName
)
)
)
}
}*/
}
callback(createEntries)
},
onItemNotFound = { database ->
// To create a new entry
if (database.isReadOnly) {
throw RegisterInReadOnlyDatabaseException()
} else {
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
}
callback(createEntries)
},
onDatabaseClosed = {
// Launch the passkey launcher activity to open the database
Log.d(TAG, "Add pending intent for passkey registration in closed database")
PasskeyLauncherActivity.getPendingIntent(
context = applicationContext,
specialMode = SpecialMode.REGISTRATION
)?.let { pendingIntent ->
createEntries.add(
CreateEntry(
accountName = accountName,
icon = defaultIcon,
pendingIntent = pendingIntent,
description = getString(R.string.passkey_locked_database_description)
)
)
}
callback(createEntries)
}
)
}
override fun onClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<Void?, ClearCredentialException>
) {
// nothing to do
}
companion object {
private val TAG = PasskeyProviderService::class.java.simpleName
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2025 AOSP modified by Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import android.os.Build
import android.os.Parcelable
import android.util.Log
import kotlinx.parcelize.Parcelize
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
/**
* Represents an Android privileged app, based on AOSP code
*/
@Parcelize
data class AndroidPrivilegedApp(
val packageName: String,
val fingerprints: Set<String>
): Parcelable {
override fun toString(): String {
return "$packageName ($fingerprints)"
}
companion object {
private const val PACKAGE_NAME_KEY = "package_name"
private const val SIGNATURES_KEY = "signatures"
private const val FINGERPRINT_KEY = "cert_fingerprint_sha256"
private const val BUILD_KEY = "build"
private const val USER_DEBUG_KEY = "userdebug"
private const val TYPE_KEY = "type"
private const val APP_INFO_KEY = "info"
private const val ANDROID_TYPE_KEY = "android"
private const val USER_BUILD_TYPE = "userdebug"
private const val APPS_KEY = "apps"
/**
* Extracts a list of AndroidPrivilegedApp objects from a JSONObject.
*/
@JvmStatic
fun extractPrivilegedApps(jsonObject: JSONObject): List<AndroidPrivilegedApp> {
val apps = mutableListOf<AndroidPrivilegedApp>()
if (!jsonObject.has(APPS_KEY)) {
return apps
}
val appsJsonArray = jsonObject.getJSONArray(APPS_KEY)
for (i in 0 until appsJsonArray.length()) {
try {
val appJsonObject = appsJsonArray.getJSONObject(i)
if (appJsonObject.getString(TYPE_KEY) != ANDROID_TYPE_KEY) {
continue
}
if (!appJsonObject.has(APP_INFO_KEY)) {
continue
}
apps.add(
createFromJSONObject(
appJsonObject.getJSONObject(APP_INFO_KEY)
)
)
} catch (e: JSONException) {
Log.e(AndroidPrivilegedApp::class.simpleName, "Error parsing privileged app", e)
}
}
return apps
}
/**
* Creates an AndroidPrivilegedApp object from a JSONObject.
*/
@JvmStatic
private fun createFromJSONObject(
appInfoJsonObject: JSONObject,
filterUserDebug: Boolean = true
): AndroidPrivilegedApp {
val signaturesJson = appInfoJsonObject.getJSONArray(SIGNATURES_KEY)
val fingerprints = mutableSetOf<String>()
for (j in 0 until signaturesJson.length()) {
if (filterUserDebug) {
if (USER_DEBUG_KEY == signaturesJson.getJSONObject(j)
.optString(BUILD_KEY) && USER_BUILD_TYPE != Build.TYPE
) {
continue
}
}
fingerprints.add(signaturesJson.getJSONObject(j).getString(FINGERPRINT_KEY))
}
return AndroidPrivilegedApp(
packageName = appInfoJsonObject.getString(PACKAGE_NAME_KEY),
fingerprints = fingerprints
)
}
/**
* Creates a JSONObject from a list of AndroidPrivilegedApp objects.
* The structure will be similar to what `extractPrivilegedApps` expects.
*
* @param privilegedApps The list of AndroidPrivilegedApp objects.
* @return A JSONObject representing the list.
*/
@JvmStatic
fun toJsonObject(privilegedApps: List<AndroidPrivilegedApp>): JSONObject {
val rootJsonObject = JSONObject()
val appsJsonArray = JSONArray()
for (app in privilegedApps) {
val appInfoObject = JSONObject()
appInfoObject.put(PACKAGE_NAME_KEY, app.packageName)
val signaturesArray = JSONArray()
for (fingerprint in app.fingerprints) {
val signatureObject = JSONObject()
signatureObject.put(FINGERPRINT_KEY, fingerprint)
// If needed: signatureObject.put(BUILD_KEY, "user")
signaturesArray.put(signatureObject)
}
appInfoObject.put(SIGNATURES_KEY, signaturesArray)
val appContainerObject = JSONObject()
appContainerObject.put(TYPE_KEY, ANDROID_TYPE_KEY)
appContainerObject.put(APP_INFO_KEY, appInfoObject)
appsJsonArray.put(appContainerObject)
}
rootJsonObject.put(APPS_KEY, appsJsonArray)
return rootJsonObject
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import android.util.Log
import androidx.credentials.exceptions.GetCredentialUnknownException
import com.kunzisoft.encrypt.Signature
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import org.json.JSONObject
class AuthenticatorAssertionResponse(
private val requestOptions: PublicKeyCredentialRequestOptions,
private val userPresent: Boolean,
private val userVerified: Boolean,
private val backupEligibility: Boolean,
private val backupState: Boolean,
private var userHandle: String,
privateKey: String,
private val clientDataResponse: ClientDataResponse,
) : AuthenticatorResponse {
override var clientJson = JSONObject()
private var authenticatorData: ByteArray = AuthenticatorData.buildAuthenticatorData(
relyingPartyId = requestOptions.rpId.toByteArray(),
userPresent = userPresent,
userVerified = userVerified,
backupEligibility = backupEligibility,
backupState = backupState
)
private var signature: ByteArray = byteArrayOf()
init {
try {
signature = Signature.sign(privateKey, dataToSign())
} catch (e: Exception) {
Log.e(this::class.java.simpleName, "Unable to sign: ${e.message}")
throw GetCredentialUnknownException("Signing failed")
}
}
private fun dataToSign(): ByteArray {
return authenticatorData + clientDataResponse.hashData()
}
override fun json(): JSONObject {
// https://www.w3.org/TR/webauthn-3/#authdata-flags
return clientJson.apply {
put("clientDataJSON", clientDataResponse.buildResponse())
put("authenticatorData", b64Encode(authenticatorData))
put("signature", b64Encode(signature))
put("userHandle", userHandle)
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.keepass.utils.UUIDUtils.asBytes
import org.json.JSONArray
import org.json.JSONObject
import java.util.UUID
class AuthenticatorAttestationResponse(
private val requestOptions: PublicKeyCredentialCreationOptions,
private val credentialId: ByteArray,
private val credentialPublicKey: ByteArray,
private val userPresent: Boolean,
private val userVerified: Boolean,
private val backupEligibility: Boolean,
private val backupState: Boolean,
private val publicKeyTypeId: Long,
private val publicKeyCbor: ByteArray,
private val clientDataResponse: ClientDataResponse,
) : AuthenticatorResponse {
override var clientJson = JSONObject()
var attestationObject: ByteArray
init {
attestationObject = defaultAttestationObject()
}
private fun buildAuthData(): ByteArray {
return AuthenticatorData.buildAuthenticatorData(
relyingPartyId = requestOptions.relyingPartyEntity.id.toByteArray(),
userPresent = userPresent,
userVerified = userVerified,
backupEligibility = backupEligibility,
backupState = backupState,
attestedCredentialData = true
) + AAGUID +
//credIdLen
byteArrayOf((credentialId.size shr 8).toByte(), credentialId.size.toByte()) +
credentialId +
credentialPublicKey
}
internal fun defaultAttestationObject(): ByteArray {
// https://www.w3.org/TR/webauthn-3/#attestation-object
val ao = mutableMapOf<String, Any>()
ao.put("fmt", "none")
ao.put("attStmt", emptyMap<Any, Any>())
ao.put("authData", buildAuthData())
return Cbor().encode(ao)
}
override fun json(): JSONObject {
// See AuthenticatorAttestationResponseJSON at
// https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
return clientJson.apply {
put("clientDataJSON", clientDataResponse.buildResponse())
put("authenticatorData", b64Encode(buildAuthData()))
put("transports", JSONArray(listOf("internal", "hybrid")))
put("publicKey", b64Encode(publicKeyCbor))
put("publicKeyAlgorithm", publicKeyTypeId)
put("attestationObject", b64Encode(attestationObject))
}
}
companion object {
// Authenticator Attestation Global Unique Identifier
private val AAGUID: ByteArray = UUID.fromString("eaecdef2-1c31-5634-8639-f1cbd9c00a08").asBytes()
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.encrypt.HashManager
class AuthenticatorData {
companion object {
fun buildAuthenticatorData(
relyingPartyId: ByteArray,
userPresent: Boolean,
userVerified: Boolean,
backupEligibility: Boolean,
backupState: Boolean,
attestedCredentialData: Boolean = false
): ByteArray {
// https://www.w3.org/TR/webauthn-3/#table-authData
var flags = 0
if (userPresent)
flags = flags or 0x01
// bit at index 1 is reserved
if (userVerified)
flags = flags or 0x04
if (backupEligibility)
flags = flags or 0x08
if (backupState)
flags = flags or 0x10
// bit at index 5 is reserved
if (attestedCredentialData) {
flags = flags or 0x40
}
// bit at index 7: Extension data included == false
return HashManager.hashSha256(relyingPartyId) +
byteArrayOf(flags.toByte()) +
byteArrayOf(0, 0, 0, 0)
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import org.json.JSONObject
interface AuthenticatorResponse {
var clientJson: JSONObject
fun json(): JSONObject
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright 2023 The Android Open Source Project
*
* 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.
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
class Cbor {
data class Item(val item: Any, val len: Int)
data class Arg(val arg: Long, val len: Int)
val TYPE_UNSIGNED_INT = 0x00
val TYPE_NEGATIVE_INT = 0x01
val TYPE_BYTE_STRING = 0x02
val TYPE_TEXT_STRING = 0x03
val TYPE_ARRAY = 0x04
val TYPE_MAP = 0x05
val TYPE_TAG = 0x06
val TYPE_FLOAT = 0x07
fun decode(data: ByteArray): Any {
val ret = parseItem(data, 0)
return ret.item
}
fun encode(data: Any): ByteArray {
if (data is Number) {
if (data is Double) {
throw IllegalArgumentException("Don't support doubles yet")
} else {
val value = data.toLong()
if (value >= 0) {
return createArg(TYPE_UNSIGNED_INT, value)
} else {
return createArg(TYPE_NEGATIVE_INT, -1 - value)
}
}
}
if (data is ByteArray) {
return createArg(TYPE_BYTE_STRING, data.size.toLong()) + data
}
if (data is String) {
return createArg(TYPE_TEXT_STRING, data.length.toLong()) + data.encodeToByteArray()
}
if (data is List<*>) {
var ret = createArg(TYPE_ARRAY, data.size.toLong())
for (i in data) {
ret += encode(i!!)
}
return ret
}
if (data is Map<*, *>) {
// See:
// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#ctap2-canonical-cbor-encoding-form
var ret = createArg(TYPE_MAP, data.size.toLong())
var byteMap: MutableMap<ByteArray, ByteArray> = mutableMapOf()
for (i in data) {
// Convert to byte arrays so we can sort them.
byteMap.put(encode(i.key!!), encode(i.value!!))
}
var keysList = ArrayList<ByteArray>(byteMap.keys)
keysList.sortedWith(
Comparator<ByteArray> { a, b ->
// If two keys have different lengths, the shorter one sorts earlier;
// If two keys have the same length, the one with the lower value in (byte-wise)
// lexical order sorts earlier.
var aBytes = byteMap.get(a)!!
var bBytes = byteMap.get(b)!!
when {
a.size > b.size -> 1
a.size < b.size -> -1
aBytes.size > bBytes.size -> 1
aBytes.size < bBytes.size -> -1
else -> 0
}
}
)
for (key in keysList) {
ret += key
ret += byteMap.get(key)!!
}
return ret
}
throw IllegalArgumentException("Bad type")
}
private fun getType(data: ByteArray, offset: Int): Int {
val d = data[offset].toInt()
return (d and 0xFF) shr 5
}
private fun getArg(data: ByteArray, offset: Int): Arg {
val arg = data[offset].toLong() and 0x1F
if (arg < 24) {
return Arg(arg, 1)
}
if (arg == 24L) {
return Arg(data[offset + 1].toLong() and 0xFF, 2)
}
if (arg == 25L) {
var ret = (data[offset + 1].toLong() and 0xFF) shl 8
ret = ret or (data[offset + 2].toLong() and 0xFF)
return Arg(ret, 3)
}
if (arg == 26L) {
var ret = (data[offset + 1].toLong() and 0xFF) shl 24
ret = ret or ((data[offset + 2].toLong() and 0xFF) shl 16)
ret = ret or ((data[offset + 3].toLong() and 0xFF) shl 8)
ret = ret or (data[offset + 4].toLong() and 0xFF)
return Arg(ret, 5)
}
throw IllegalArgumentException("Bad arg")
}
private fun parseItem(data: ByteArray, offset: Int): Item {
val itemType = getType(data, offset)
val arg = getArg(data, offset)
println("Type $itemType ${arg.arg} ${arg.len}")
when (itemType) {
TYPE_UNSIGNED_INT -> {
return Item(arg.arg, arg.len)
}
TYPE_NEGATIVE_INT -> {
return Item(-1 - arg.arg, arg.len)
}
TYPE_BYTE_STRING -> {
val ret =
data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
return Item(ret, arg.len + arg.arg.toInt())
}
TYPE_TEXT_STRING -> {
val ret =
data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
return Item(ret.toString(Charsets.UTF_8), arg.len + arg.arg.toInt())
}
TYPE_ARRAY -> {
val ret = mutableListOf<Any>()
var consumed = arg.len
for (i in 0 until arg.arg.toInt()) {
val item = parseItem(data, offset + consumed)
ret.add(item.item)
consumed += item.len
}
return Item(ret.toList(), consumed)
}
TYPE_MAP -> {
val ret = mutableMapOf<Any, Any>()
var consumed = arg.len
for (i in 0 until arg.arg.toInt()) {
val key = parseItem(data, offset + consumed)
consumed += key.len
val value = parseItem(data, offset + consumed)
consumed += value.len
ret[key.item] = value.item
}
return Item(ret.toMap(), consumed)
}
else -> {
throw IllegalArgumentException("Bad type")
}
}
}
private fun createArg(type: Int, arg: Long): ByteArray {
val t = type shl 5
val a = arg.toInt()
if (arg < 24) {
return byteArrayOf(((t or a) and 0xFF).toByte())
}
if (arg <= 0xFF) {
return byteArrayOf(((t or 24) and 0xFF).toByte(), (a and 0xFF).toByte())
}
if (arg <= 0xFFFF) {
return byteArrayOf(
((t or 25) and 0xFF).toByte(),
((a shr 8) and 0xFF).toByte(),
(a and 0xFF).toByte()
)
}
if (arg <= 0xFFFFFFFF) {
return byteArrayOf(
((t or 26) and 0xFF).toByte(),
((a shr 24) and 0xFF).toByte(),
((a shr 16) and 0xFF).toByte(),
((a shr 8) and 0xFF).toByte(),
(a and 0xFF).toByte()
)
}
throw IllegalArgumentException("bad Arg")
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import org.json.JSONObject
open class ClientDataBuildResponse(
type: Type,
challenge: ByteArray,
origin: String,
crossOrigin: Boolean? = false,
topOrigin: String? = null,
): AuthenticatorResponse, ClientDataResponse {
override var clientJson = JSONObject()
init {
// https://w3c.github.io/webauthn/#client-data
clientJson.put("type", type.value)
clientJson.put("challenge", b64Encode(challenge))
clientJson.put("origin", origin)
crossOrigin?.let {
clientJson.put("crossOrigin", it)
}
topOrigin?.let {
clientJson.put("topOrigin", it)
}
}
override fun json(): JSONObject {
return clientJson
}
enum class Type(val value: String) {
GET("webauthn.get"), CREATE("webauthn.create")
}
override fun buildResponse(): String {
return b64Encode(json().toString().toByteArray())
}
override fun hashData(): ByteArray {
return HashManager.hashSha256(json().toString().toByteArray())
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
open class ClientDataDefinedResponse(
private val clientDataHash: ByteArray
): ClientDataResponse {
override fun hashData(): ByteArray {
return clientDataHash
}
override fun buildResponse(): String {
return CLIENT_DATA_JSON_PRIVILEGED
}
companion object {
private const val CLIENT_DATA_JSON_PRIVILEGED = "<placeholder>"
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
interface ClientDataResponse {
fun hashData(): ByteArray
fun buildResponse(): String
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2023 The Android Open Source Project
*
* 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.
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
data class PublicKeyCredentialRpEntity(val name: String, val id: String)
data class PublicKeyCredentialUserEntity(
val name: String,
val id: ByteArray,
val displayName: String
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PublicKeyCredentialUserEntity
if (name != other.name) return false
if (!id.contentEquals(other.id)) return false
if (displayName != other.displayName) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + id.contentHashCode()
result = 31 * result + displayName.hashCode()
return result
}
}
data class PublicKeyCredentialParameters(val type: String, val alg: Long)
data class PublicKeyCredentialDescriptor(
val type: String,
val id: ByteArray,
val transports: List<String>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PublicKeyCredentialDescriptor
if (type != other.type) return false
if (!id.contentEquals(other.id)) return false
if (transports != other.transports) return false
return true
}
override fun hashCode(): Int {
var result = type.hashCode()
result = 31 * result + id.contentHashCode()
result = 31 * result + transports.hashCode()
return result
}
}
data class AuthenticatorSelectionCriteria(
val authenticatorAttachment: String,
val residentKey: String,
val requireResidentKey: Boolean = false,
val userVerification: String = "preferred"
)

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import org.json.JSONObject
class FidoPublicKeyCredential(
val id: String,
val response: AuthenticatorResponse,
val authenticatorAttachment: String
) {
fun json(): String {
// see at https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
val discoverableCredential = true
val rk = JSONObject()
rk.put("rk", discoverableCredential)
val credProps = JSONObject()
credProps.put("credProps", rk)
// See RegistrationResponseJSON at
// https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
val ret = JSONObject()
ret.put("id", id)
ret.put("rawId", id)
ret.put("type", "public-key")
ret.put("authenticatorAttachment", authenticatorAttachment)
ret.put("response", response.json())
ret.put("clientExtensionResults", JSONObject()) // TODO credProps
return ret.toString()
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.encrypt.Base64Helper
import org.json.JSONObject
class PublicKeyCredentialCreationOptions(
requestJson: String,
var clientDataHash: ByteArray?
) {
val json: JSONObject = JSONObject(requestJson)
val relyingPartyEntity: PublicKeyCredentialRpEntity
val userEntity: PublicKeyCredentialUserEntity
val challenge: ByteArray
val pubKeyCredParams: List<PublicKeyCredentialParameters>
var timeout: Long
var excludeCredentials: List<PublicKeyCredentialDescriptor>
var authenticatorSelection: AuthenticatorSelectionCriteria
var attestation: String
init {
val rpJson = json.getJSONObject("rp")
relyingPartyEntity = PublicKeyCredentialRpEntity(rpJson.getString("name"), rpJson.getString("id"))
val rpUser = json.getJSONObject("user")
val userId = Base64Helper.b64Decode(rpUser.getString("id"))
userEntity =
PublicKeyCredentialUserEntity(
rpUser.getString("name"),
userId,
rpUser.getString("displayName")
)
challenge = Base64Helper.b64Decode(json.getString("challenge"))
val pubKeyCredParamsJson = json.getJSONArray("pubKeyCredParams")
val pubKeyCredParamsTmp: MutableList<PublicKeyCredentialParameters> = mutableListOf()
for (i in 0 until pubKeyCredParamsJson.length()) {
val e = pubKeyCredParamsJson.getJSONObject(i)
pubKeyCredParamsTmp.add(
PublicKeyCredentialParameters(e.getString("type"), e.getLong("alg"))
)
}
pubKeyCredParams = pubKeyCredParamsTmp.toList()
timeout = json.optLong("timeout", 0)
// TODO: Fix excludeCredentials and authenticatorSelection
excludeCredentials = emptyList()
authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required")
attestation = json.optString("attestation", "none")
}
companion object {
private val TAG = PublicKeyCredentialCreationOptions::class.simpleName
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import java.security.KeyPair
data class PublicKeyCredentialCreationParameters(
val publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions,
val credentialId: ByteArray,
val signatureKey: Pair<KeyPair, Long>,
val clientDataResponse: ClientDataResponse
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PublicKeyCredentialCreationParameters
if (publicKeyCredentialCreationOptions != other.publicKeyCredentialCreationOptions) return false
if (!credentialId.contentEquals(other.credentialId)) return false
if (signatureKey != other.signatureKey) return false
if (clientDataResponse != other.clientDataResponse) return false
return true
}
override fun hashCode(): Int {
var result = publicKeyCredentialCreationOptions.hashCode()
result = 31 * result + credentialId.contentHashCode()
result = 31 * result + signatureKey.hashCode()
result = 31 * result + clientDataResponse.hashCode()
return result
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.encrypt.Base64Helper
import org.json.JSONObject
class PublicKeyCredentialRequestOptions(requestJson: String) {
val json: JSONObject = JSONObject(requestJson)
val challenge: ByteArray = Base64Helper.b64Decode(json.getString("challenge"))
val timeout: Long = json.optLong("timeout", 0)
val rpId: String = json.optString("rpId", "")
val userVerification: String = json.optString("userVerification", "preferred")
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.keepass.model.AppOrigin
data class PublicKeyCredentialUsageParameters(
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
val clientDataResponse: ClientDataResponse,
var appOrigin: AppOrigin
)

View File

@@ -0,0 +1,604 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.util
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.CreatePublicKeyCredentialResponse
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.PublicKeyCredential
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.provider.ProviderGetCredentialRequest
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.Signature
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataBuildResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataDefinedResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.FidoPublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.getOriginFromPrivilegedAllowLists
import com.kunzisoft.keepass.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import java.security.KeyStore
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.Instant
import java.util.UUID
import javax.crypto.KeyGenerator
import javax.crypto.Mac
import javax.crypto.SecretKey
/**
* Utility class to manage the passkey elements,
* allows to add and retrieve intent values with preconfigured keys,
* and makes it easy to create creation and usage requests
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
object PasskeyHelper {
private const val EXTRA_PASSKEY = "com.kunzisoft.keepass.passkey.extra.passkey"
private const val HMAC_TYPE = "HmacSHA256"
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
private const val SEPARATOR = "_"
private const val NAME_OF_HMAC_KEY = "KeePassDXCredentialProviderHMACKey"
private const val KEYSTORE_TYPE = "AndroidKeyStore"
private val PLACEHOLDER_FOR_NEW_NODE_ID = "0".repeat(32)
private val REGEX_TIMESTAMP = "[0-9]{10}".toRegex()
private val REGEX_AUTHENTICATION_CODE = "[A-F0-9]{64}".toRegex() // 256 bits = 64 hex chars
private const val MAX_DIFF_IN_SECONDS = 60
private val internalSecureRandom: SecureRandom = SecureRandom()
/**
* Add an authentication code generated by an entry to the intent
*/
fun Intent.addAuthCode(passkeyEntryNodeId: UUID? = null) {
putExtras(Bundle().apply {
val timestamp = Instant.now().epochSecond
putString(EXTRA_TIMESTAMP, timestamp.toString())
putString(
EXTRA_AUTHENTICATION_CODE,
generatedAuthenticationCode(
passkeyEntryNodeId, timestamp
).toHexString()
)
})
}
/**
* Add the passkey to the intent
*/
fun Intent.addPasskey(passkey: Passkey?) {
passkey?.let {
putExtra(EXTRA_PASSKEY, passkey)
}
}
/**
* Retrieve the passkey from the intent
*/
fun Intent.retrievePasskey(): Passkey? {
return this.getParcelableExtraCompat(EXTRA_PASSKEY)
}
/**
* Remove the passkey from the intent
*/
fun Intent.removePasskey() {
return this.removeExtra(EXTRA_PASSKEY)
}
/**
* Add the app origin to the intent
*/
fun Intent.addAppOrigin(appOrigin: AppOrigin?) {
appOrigin?.let {
putExtra(EXTRA_APP_ORIGIN, appOrigin)
}
}
/**
* Retrieve the app origin from the intent
*/
fun Intent.retrieveAppOrigin(): AppOrigin? {
return this.getParcelableExtraCompat(EXTRA_APP_ORIGIN)
}
/**
* Remove the app origin from the intent
*/
fun Intent.removeAppOrigin() {
return this.removeExtra(EXTRA_APP_ORIGIN)
}
/**
* Build the Passkey response for one entry
*/
fun Activity.buildPasskeyResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
try {
entryInfo.passkey?.let { passkey ->
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.addPasskey(passkey)
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
mReplyIntent.addNodeId(entryInfo.id)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
throw IOException("No passkey found")
}
} catch (e: Exception) {
Log.e(javaClass.name, "Unable to add the passkey as result", e)
Toast.makeText(
this,
getString(R.string.error_passkey_result),
Toast.LENGTH_SHORT
).show()
setResult(Activity.RESULT_CANCELED)
}
}
/**
* Check the timestamp and authentication code transmitted via PendingIntent
*/
fun checkSecurity(intent: Intent, nodeId: UUID?) {
val timestampString = intent.getStringExtra(EXTRA_TIMESTAMP)
if (timestampString.isNullOrEmpty())
throw CreateCredentialUnknownException("Timestamp null")
if (timestampString.matches(REGEX_TIMESTAMP).not()) {
throw CreateCredentialUnknownException("Timestamp not valid")
}
val timestamp = timestampString.toLong()
val diff = Instant.now().epochSecond - timestamp
if (diff < 0 || diff > MAX_DIFF_IN_SECONDS) {
throw CreateCredentialUnknownException("Out of time")
}
verifyAuthenticationCode(
intent.getStringExtra(EXTRA_AUTHENTICATION_CODE),
generatedAuthenticationCode(nodeId, timestamp)
)
}
/**
* Verify the authentication code from the encrypted message received from the intent
*/
private fun verifyAuthenticationCode(
valueToCheck: String?,
authenticationCode: ByteArray
) {
if (valueToCheck.isNullOrEmpty())
throw CreateCredentialUnknownException("Authentication code empty")
if (valueToCheck.matches(REGEX_AUTHENTICATION_CODE).not())
throw CreateCredentialUnknownException("Authentication not valid")
if (MessageDigest.isEqual(authenticationCode, generateAuthenticationCode(valueToCheck)))
throw CreateCredentialUnknownException("Authentication code incorrect")
}
/**
* Generate the authentication code base on the entry [nodeId] and [timestamp]
*/
private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray {
return generateAuthenticationCode(
(nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString()
)
}
/**
* Generate the authentication code base on the entry [message]
*/
private fun generateAuthenticationCode(message: String): ByteArray {
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
keyStore.load(null)
val hmacKey = try {
keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey
} catch (_: Exception) {
// key not found
generateKey()
}
val mac = Mac.getInstance(HMAC_TYPE)
mac.init(hmacKey)
val authenticationCode = mac.doFinal(message.toByteArray())
return authenticationCode
}
/**
* Generate the HMAC key if cannot be found in the KeyStore
*/
private fun generateKey(): SecretKey? {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE
)
val keySizeInBits = 128
keyGenerator.init(
KeyGenParameterSpec.Builder(NAME_OF_HMAC_KEY, KeyProperties.PURPOSE_SIGN)
.setKeySize(keySizeInBits)
.build()
)
val key = keyGenerator.generateKey()
return key
}
/**
* Retrieve the [PublicKeyCredentialCreationOptions] from the intent
*/
fun ProviderCreateCredentialRequest.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
val request = this
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
}
val createPublicKeyCredentialRequest = request.callingRequest as CreatePublicKeyCredentialRequest
return PublicKeyCredentialCreationOptions(
requestJson = createPublicKeyCredentialRequest.requestJson,
clientDataHash = createPublicKeyCredentialRequest.clientDataHash
)
}
/**
* Retrieve the [GetPublicKeyCredentialOption] from the intent
*/
fun ProviderGetCredentialRequest.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
val request = this
if (request.credentialOptions.size != 1) {
throw GetCredentialUnknownException("not exact one credentialOption")
}
if (request.credentialOptions[0] !is GetPublicKeyCredentialOption) {
throw CreateCredentialUnknownException("credentialOptions is of wrong type: ${request.credentialOptions[0]}")
}
return request.credentialOptions[0] as GetPublicKeyCredentialOption
}
/**
* Utility method to retrieve the origin asynchronously,
* checks for the presence of the application in the privilege lists
*
* @param providedClientDataHash Client data hash precalculated by the system
* @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
* @param context Context for file operations.
* call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list, return the clientDataHash
* call [onOriginNotRetrieved] if the origin is not retrieved from the system, return a new Android Origin
*/
suspend fun getOrigin(
providedClientDataHash: ByteArray?,
callingAppInfo: CallingAppInfo?,
context: Context,
onOriginRetrieved: suspend (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginNotRetrieved: suspend (appOrigin: AppOrigin, androidOriginString: String) -> Unit
) {
if (callingAppInfo == null) {
throw SecurityException("Calling app info cannot be retrieved")
}
withContext(Dispatchers.IO) {
// For trusted browsers like Chrome and Firefox
val callOrigin = try {
getOriginFromPrivilegedAllowLists(callingAppInfo, context)
} catch (e: Exception) {
// Throw the Privileged Exception only if it's a browser
if (e is PrivilegedAllowLists.PrivilegedException
&& AppUtil.getInstalledBrowsersWithSignatures(context).any {
it.packageName == e.temptingApp.packageName
}
) throw e
null
}
// Build the default Android origin
val androidOrigin = AndroidOrigin(
packageName = callingAppInfo.packageName,
fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
)
// Check if the webDomain is validated by the system
withContext(Dispatchers.Main) {
if (callOrigin != null && providedClientDataHash != null) {
// Origin already defined by the system
Log.d(javaClass.simpleName, "Origin $callOrigin retrieved from callingAppInfo")
onOriginRetrieved(
AppOrigin.fromOrigin(callOrigin, androidOrigin, verified = true),
providedClientDataHash
)
} else {
// Add Android origin by default
onOriginNotRetrieved(
AppOrigin(verified = false).apply {
addAndroidOrigin(androidOrigin)
},
androidOrigin.toOriginValue()
)
}
}
}
}
/**
* Generate a credential id randomly
*/
private fun generateCredentialId(): ByteArray {
// see https://w3c.github.io/webauthn/#credential-id
val size = 16
val credentialId = ByteArray(size)
internalSecureRandom.nextBytes(credentialId)
return credentialId
}
/**
* Utility method to create a passkey and the associated creation request parameters
* [intent] allows to retrieve the request
* [context] context to manage package verification files
* [defaultBackupEligibility] the default backup eligibility to add the the passkey entry
* [defaultBackupState] the default backup state to add the the passkey entry
* [passkeyCreated] is called asynchronously when the passkey has been created
*/
suspend fun retrievePasskeyCreationRequestParameters(
intent: Intent,
context: Context,
defaultBackupEligibility: Boolean?,
defaultBackupState: Boolean?,
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
) {
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
if (createCredentialRequest == null)
throw CreateCredentialUnknownException("could not retrieve request from intent")
val callingAppInfo = createCredentialRequest.callingAppInfo
val creationOptions = createCredentialRequest.retrievePasskeyCreationComponent()
val relyingParty = creationOptions.relyingPartyEntity.id
val username = creationOptions.userEntity.name
val userHandle = creationOptions.userEntity.id
val pubKeyCredParams = creationOptions.pubKeyCredParams
val clientDataHash = creationOptions.clientDataHash
val credentialId = generateCredentialId()
val (keyPair, keyTypeId) = Signature.generateKeyPair(
pubKeyCredParams.map { params -> params.alg }
) ?: throw CreateCredentialUnknownException("no known public key type found")
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
// Create the passkey element
val passkey = Passkey(
username = username,
privateKeyPem = privateKeyPem,
credentialId = b64Encode(credentialId),
userHandle = b64Encode(userHandle),
relyingParty = relyingParty,
backupEligibility = defaultBackupEligibility,
backupState = defaultBackupState
)
// create new entry in database
getOrigin(
providedClientDataHash = clientDataHash,
callingAppInfo = callingAppInfo,
context = context,
onOriginRetrieved = { appInfoToStore, clientDataHash ->
passkeyCreated.invoke(
passkey,
appInfoToStore,
PublicKeyCredentialCreationParameters(
publicKeyCredentialCreationOptions = creationOptions,
credentialId = credentialId,
signatureKey = Pair(keyPair, keyTypeId),
clientDataResponse = ClientDataDefinedResponse(clientDataHash)
)
)
},
onOriginNotRetrieved = { appInfoToStore, origin ->
passkeyCreated.invoke(
passkey,
appInfoToStore,
PublicKeyCredentialCreationParameters(
publicKeyCredentialCreationOptions = creationOptions,
credentialId = credentialId,
signatureKey = Pair(keyPair, keyTypeId),
clientDataResponse = ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.CREATE,
challenge = creationOptions.challenge,
origin = origin
)
)
)
}
)
}
/**
* Build the passkey public key credential response,
* by calling this method the user is always recognized as present and verified
*/
fun buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters,
backupEligibility: Boolean,
backupState: Boolean
): CreatePublicKeyCredentialResponse {
val keyPair = publicKeyCredentialCreationParameters.signatureKey.first
val keyTypeId = publicKeyCredentialCreationParameters.signatureKey.second
val responseJson = FidoPublicKeyCredential(
id = b64Encode(publicKeyCredentialCreationParameters.credentialId),
response = AuthenticatorAttestationResponse(
requestOptions = publicKeyCredentialCreationParameters.publicKeyCredentialCreationOptions,
credentialId = publicKeyCredentialCreationParameters.credentialId,
credentialPublicKey = Cbor().encode(
Signature.convertPublicKeyToMap(
publicKeyIn = keyPair.public,
keyTypeId = keyTypeId
) ?: mapOf<Int, Any>()),
userPresent = true,
userVerified = true,
backupEligibility = backupEligibility,
backupState = backupState,
publicKeyTypeId = keyTypeId,
publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!,
clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse
),
authenticatorAttachment = "platform"
).json()
// log only the length to prevent logging sensitive information
Log.d(javaClass.simpleName, "Json response for key creation")
return CreatePublicKeyCredentialResponse(responseJson)
}
/**
* Utility method to use a passkey and create the associated usage request parameters
* [intent] allows to retrieve the request
* [context] context to manage package verification files
* [result] is called asynchronously after the creation of PublicKeyCredentialUsageParameters, the origin associated with it may or may not be verified
*/
suspend fun retrievePasskeyUsageRequestParameters(
intent: Intent,
context: Context,
result: suspend (PublicKeyCredentialUsageParameters) -> Unit
) {
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
if (getCredentialRequest == null)
throw CreateCredentialUnknownException("could not retrieve request from intent")
val callingAppInfo = getCredentialRequest.callingAppInfo
val credentialOption = getCredentialRequest.retrievePasskeyUsageComponent()
val clientDataHash = credentialOption.clientDataHash
val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
getOrigin(
providedClientDataHash = clientDataHash,
callingAppInfo = callingAppInfo,
context = context,
onOriginRetrieved = { appOrigin, clientDataHash ->
result.invoke(
PublicKeyCredentialUsageParameters(
publicKeyCredentialRequestOptions = requestOptions,
clientDataResponse = ClientDataDefinedResponse(clientDataHash),
appOrigin = appOrigin
)
)
},
onOriginNotRetrieved = { appOrigin, androidOriginString ->
// By default we crate an usage parameter with Android origin
result.invoke(
PublicKeyCredentialUsageParameters(
publicKeyCredentialRequestOptions = requestOptions,
clientDataResponse = ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET,
challenge = requestOptions.challenge,
origin = androidOriginString
),
appOrigin = appOrigin
)
)
}
)
}
/**
* Build the passkey public key credential response,
* by calling this method the user is always recognized as present and verified
*/
fun buildPasskeyPublicKeyCredential(
requestOptions: PublicKeyCredentialRequestOptions,
clientDataResponse: ClientDataResponse,
passkey: Passkey,
defaultBackupEligibility: Boolean,
defaultBackupState: Boolean
): PublicKeyCredential {
val getCredentialResponse = FidoPublicKeyCredential(
id = passkey.credentialId,
response = AuthenticatorAssertionResponse(
requestOptions = requestOptions,
userPresent = true,
userVerified = true,
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
backupState = passkey.backupState ?: defaultBackupState,
userHandle = passkey.userHandle,
privateKey = passkey.privateKeyPem,
clientDataResponse = clientDataResponse
),
authenticatorAttachment = "platform"
).json()
Log.d(javaClass.simpleName, "Json response for key usage")
return PublicKeyCredential(getCredentialResponse)
}
/**
* Verify that the application signature is contained in the [appOrigin]
*/
fun getVerifiedGETClientDataResponse(
usageParameters: PublicKeyCredentialUsageParameters,
appOrigin: AppOrigin
): ClientDataResponse {
val appToCheck = usageParameters.appOrigin
return if (appToCheck.verified) {
usageParameters.clientDataResponse
} else {
// Origin checked by Android app signature
ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET,
challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
origin = appToCheck.checkAppOrigin(appOrigin)
)
}
}
}

Some files were not shown because too many files have changed in this diff Show More