Compare commits

...

353 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
Stefan Hauser
a7a93fa2a2 Use local date/time in filename for saving a copy of the database 2025-01-16 09:15:32 +01:00
J-Jamet
67b09014aa Merge branch 'develop' into feature/Passkeys 2024-11-18 16:40:46 +01:00
cali
c907750446 implement creation and update of passkeys 2024-10-13 20:37:46 +02:00
cali
69114c3cc0 first version credential provider 2024-09-08 18:49:27 +02:00
308 changed files with 12428 additions and 5500 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/

View File

@@ -1,3 +1,32 @@
KeePassDX(4.2.0)
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 (Thx @Dev-ClayP)
* Passkeys management #1421 #2097 (Thx @cali-95)
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
* 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

View File

@@ -9,31 +9,35 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1009.0)
aws-sdk-core (3.213.0)
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.95.0)
aws-sdk-core (~> 3, >= 3.210.0)
logger
aws-sdk-kms (1.110.0)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.171.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-s3 (1.196.1)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.1)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
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.6.20240107)
dotenv (2.8.1)
@@ -55,11 +59,11 @@ 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-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)
@@ -67,8 +71,8 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.225.0)
fastimage (2.4.0)
fastlane (2.228.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -108,7 +112,7 @@ GEM
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 (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-versioning_android (0.1.1)
fastlane-sirp (1.0.0)
@@ -130,12 +134,12 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.1)
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.4.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -151,36 +155,39 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.7)
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.8.2)
jwt (2.9.3)
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.15.0)
multi_json (1.17.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.2.1)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.1)
public_suffix (6.0.1)
rake (13.2.1)
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.3.9)
rouge (2.0.7)
rexml (3.4.1)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
rubyzip (2.4.1)
security (0.1.5)
signet (0.19.0)
signet (0.20.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@@ -207,8 +214,8 @@ GEM
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
@@ -220,4 +227,4 @@ DEPENDENCIES
fastlane-plugin-versioning_android
BUNDLED WITH
2.5.10
2.6.9

View File

@@ -54,7 +54,7 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|--------|--------|---------|
| [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 |
| [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

View File

@@ -5,14 +5,14 @@ apply plugin: 'kotlin-kapt'
android {
namespace 'com.kunzisoft.keepass'
compileSdkVersion 34
compileSdkVersion 36
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 34
versionCode = 134
versionName = "4.1.2"
minSdkVersion 19
targetSdkVersion 35
versionCode = 140
versionName = "4.2.0beta01"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -35,6 +35,10 @@ android {
}
}
buildFeatures {
buildConfig true
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
@@ -101,6 +105,11 @@ android {
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')
}
}
def room_version = "2.5.1"
@@ -118,6 +127,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
@@ -139,6 +149,9 @@ dependencies {
implementation 'commons-codec:commons-codec:1.15'
// Password generator
implementation 'me.gosimple:nbvcxz:1.5.0'
// Credentials Provider
implementation "androidx.credentials:credentials:1.2.2"
// Modules import
implementation project(path: ':database')

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,64 @@
{
"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.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

@@ -45,7 +45,8 @@
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}" />
@@ -159,21 +160,33 @@
<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" />
<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:excludeFromRecents="true"/>
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent"
android:launchMode="singleInstance"
android:exported="true">
@@ -192,14 +205,12 @@
</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:excludeFromRecents="true"
android:exported="false"
tools:targetApi="upside_down_cake" />
<service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
android:foregroundServiceType="dataSync"
@@ -221,14 +232,14 @@
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
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
@@ -239,7 +250,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" >
@@ -249,6 +260,22 @@
<action android:name="android.view.InputMethod" />
</intent-filter>
</service>
<service
android:name="com.kunzisoft.keepass.credentialprovider.passkey.PasskeyProviderService"
android:enabled="true"
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

@@ -358,78 +358,41 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
val imageView = imageViewRef.get() ?: return
isViewTranslateAnimationRunning = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
imageView.run {
val translationY = if (velY > 0) {
originalViewBounds.top + height - top
} else {
originalViewBounds.top - height - top
}
animate()
.setDuration(dismissAnimationDuration)
.setInterpolator(dismissAnimationInterpolator)
.translationY(translationY.toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(imageView, amount)
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
}
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
}
})
}
} else {
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) {
originalViewBounds.top + imageView.height - imageView.top
imageView.run {
val translationY = if (velY > 0) {
originalViewBounds.top + height - 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()
originalViewBounds.top - height - top
}
animate()
.setDuration(dismissAnimationDuration)
.setInterpolator(dismissAnimationInterpolator)
.translationY(translationY.toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(imageView, amount)
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
}
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
}
})
}
}
@@ -657,137 +620,76 @@ 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)
.setInterpolator(restoreAnimationInterpolator)
.translationY((originalViewBounds.top - top).toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(this, amount)
imageView.run {
animate()
.setDuration(restoreAnimationDuration)
.setInterpolator(restoreAnimationInterpolator)
.translationY((originalViewBounds.top - top).toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(this, amount)
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
// no op
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
// no op
}
override fun onAnimationEnd(p0: Animator) {
onViewTranslateListener?.onRestore(imageView)
}
override fun onAnimationEnd(p0: Animator) {
onViewTranslateListener?.onRestore(imageView)
}
override fun onAnimationCancel(p0: Animator) {
// no op
}
override fun onAnimationCancel(p0: Animator) {
// no op
}
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
}
} 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()
}
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
}
}
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
} else {
originalViewBounds.top - height - top
}
animate()
.setDuration(dismissAnimationDuration)
.setInterpolator(AccelerateDecelerateInterpolator())
.translationY(translationY.toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(this, amount)
imageView.run {
val translationY = if (y - initialY > 0) {
originalViewBounds.top + height - top
} else {
originalViewBounds.top - height - top
}
animate()
.setDuration(dismissAnimationDuration)
.setInterpolator(AccelerateDecelerateInterpolator())
.translationY(translationY.toFloat())
.setUpdateListener {
val amount = calcTranslationAmount()
changeBackgroundAlpha(amount)
onViewTranslateListener?.onViewTranslate(this, amount)
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
isViewTranslateAnimationRunning = true
}
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
isViewTranslateAnimationRunning = true
}
override fun onAnimationEnd(p0: Animator) {
isViewTranslateAnimationRunning = false
onViewTranslateListener?.onDismiss(imageView)
cleanup()
}
override fun onAnimationEnd(p0: Animator) {
isViewTranslateAnimationRunning = false
onViewTranslateListener?.onDismiss(imageView)
cleanup()
}
override fun onAnimationCancel(p0: Animator) {
isViewTranslateAnimationRunning = false
}
override fun onAnimationCancel(p0: Animator) {
isViewTranslateAnimationRunning = false
}
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
}
} 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()
}
override fun onAnimationRepeat(p0: Animator) {
// no op
}
})
}
}
private fun processFlingToDismiss(velocityY: Float) {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Build
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.util.Log
@@ -32,7 +31,7 @@ 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
@@ -58,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)
@@ -78,9 +77,8 @@ class AboutActivity : StylishActivity() {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
HtmlCompat.FROM_HTML_MODE_LEGACY)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
textDirection = View.TEXT_DIRECTION_ANY_RTL
}
textDirection = View.TEXT_DIRECTION_ANY_RTL
}
findViewById<TextView>(R.id.activity_about_privacy_text).apply {

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
@@ -261,7 +257,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

View File

@@ -55,12 +55,15 @@ 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.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
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
@@ -70,7 +73,6 @@ 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
@@ -376,18 +378,25 @@ class EntryEditActivity : DatabaseLockActivity(),
// Don't wait for saving if it's to provide autofill
mDatabase?.let { database ->
EntrySelectionHelper.doSpecialAction(intent,
{},
{},
{},
{
EntrySelectionHelper.doSpecialAction(
intent = intent,
defaultAction = {},
searchAction = {},
saveAction = {},
keyboardSelectionAction = {
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
},
{ _, _ ->
autofillSelectionAction = { _, _ ->
entryValidatedForAutofillSelection(database, entrySave.newEntry)
},
{
autofillRegistrationAction = {
entryValidatedForAutofillRegistration(entrySave.newEntry)
},
passkeySelectionAction = {
entryValidatedForPasskeySelection(database, entrySave.newEntry)
},
passkeyRegistrationAction = {
entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
}
)
}
@@ -430,25 +439,32 @@ class EntryEditActivity : DatabaseLockActivity(),
}
if (newNodes.size == 1) {
(newNodes[0] as? Entry?)?.let { entry ->
EntrySelectionHelper.doSpecialAction(intent,
{
EntrySelectionHelper.doSpecialAction(
intent = intent,
defaultAction = {
// Finish naturally
finishForEntryResult(entry)
},
{
searchAction = {
// Nothing when search retrieved
},
{
saveAction = {
entryValidatedForSave(entry)
},
{
keyboardSelectionAction = {
entryValidatedForKeyboardSelection(database, entry)
},
{ _, _ ->
autofillSelectionAction = { _, _ ->
entryValidatedForAutofillSelection(database, entry)
},
{
autofillRegistrationAction = {
entryValidatedForAutofillRegistration(entry)
},
passkeySelectionAction = {
entryValidatedForPasskeySelection(database, entry)
},
passkeyRegistrationAction = {
entryValidatedForPasskeyRegistration(database, entry)
}
)
}
@@ -488,9 +504,33 @@ class EntryEditActivity : DatabaseLockActivity(),
onValidateSpecialMode()
}
private fun entryValidatedForAutofillRegistration(entry: Entry) {
private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
this.buildPasskeyResponseAndSetResult(
entryInfo = entry.getEntryInfo(database)
)
}
onValidateSpecialMode()
}
private fun entryValidatedForAutofillRegistration(entry: Entry) {
//if (isIntentSender()) {
// TODO Autofill Callback #765
//}
onValidateSpecialMode()
if (!isIntentSender()) {
finishForEntryResult(entry)
}
}
private fun entryValidatedForPasskeyRegistration(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() {
@@ -604,16 +644,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)
@@ -742,12 +778,17 @@ 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)
super.finish()
@@ -892,7 +933,7 @@ class EntryEditActivity : DatabaseLockActivity(),
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
AutofillHelper.startActivityForAutofillResult(
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
activity,
intent,
activityResultLauncher,
@@ -903,21 +944,48 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
/**
* Launch EntryEditActivity to add a new passkey entry
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun launchForPasskeySelectionResult(context: Context,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
context,
intent,
activityResultLauncher,
searchInfo
)
}
}
}
/**
* Launch EntryEditActivity to register an updated entry (from autofill)
*/
fun launchToUpdateForRegistration(context: Context,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
entryId: NodeId<UUID>,
registerInfo: RegisterInfo? = null) {
registerInfo: RegisterInfo?,
typeMode: TypeMode) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
activityResultLauncher,
intent,
registerInfo
registerInfo,
typeMode
)
}
}
@@ -928,16 +996,20 @@ class EntryEditActivity : DatabaseLockActivity(),
*/
fun launchToCreateForRegistration(context: Context,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
groupId: NodeId<*>,
registerInfo: RegisterInfo? = null) {
registerInfo: RegisterInfo? = null,
typeMode: TypeMode) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
activityResultLauncher,
intent,
registerInfo
registerInfo,
typeMode
)
}
}

View File

@@ -44,15 +44,16 @@ 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.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
@@ -65,12 +66,11 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
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.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
@@ -99,10 +99,8 @@ 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
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -263,7 +261,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
GroupActivity.launch(
this@FileDatabaseSelectActivity,
database,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
false
)
}
ACTION_DATABASE_LOAD_TASK -> {
@@ -299,7 +297,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
},
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher)
mCredentialActivityResultLauncher)
}
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
@@ -309,13 +307,14 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher)
mCredentialActivityResultLauncher)
}
}
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null, null)
// Delete flickering for kitkat <=
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
overridePendingTransition(0, 0)
}
@@ -329,13 +328,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
}
createDatabaseButtonView?.visibility = View.VISIBLE
}
else -> {
// Disable create button if in selection mode or request for autofill
@@ -493,23 +486,46 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) {
AutofillHelper.startActivityForAutofillResult(activity,
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
autofillComponent,
searchInfo)
}
/*
* -------------------------
* Passkey Launch
* -------------------------
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun launchForPasskeySelectionResult(activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
searchInfo: SearchInfo? = null) {
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
searchInfo
)
}
/*
* -------------------------
* Registration Launch
* -------------------------
*/
fun launchForRegistration(context: Context,
registerInfo: RegisterInfo? = null) {
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
Intent(context, FileDatabaseSelectActivity::class.java),
registerInfo)
activityResultLauncher: ActivityResultLauncher<Intent>?,
registerInfo: RegisterInfo? = null,
typeMode: TypeMode) {
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
activityResultLauncher,
Intent(context, FileDatabaseSelectActivity::class.java),
registerInfo,
typeMode
)
}
}
}

View File

@@ -63,13 +63,17 @@ import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.fragments.GroupFragment
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.adapters.BreadcrumbAdapter
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.element.DateInstant
@@ -83,7 +87,6 @@ import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo
@@ -116,7 +119,7 @@ import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.kunzisoft.keepass.viewmodels.GroupViewModel
import org.joda.time.Instant
import org.joda.time.LocalDateTime
class GroupActivity : DatabaseLockActivity(),
@@ -264,10 +267,8 @@ class GroupActivity : DatabaseLockActivity(),
mGroupEditViewModel.selectIcon(icon)
}
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -343,7 +344,7 @@ class GroupActivity : DatabaseLockActivity(),
mExternalFileHelper?.createDocument(
getString(R.string.database_file_name_default) +
"_" +
Instant.now().toString() +
LocalDateTime.now().toString() +
mDatabase?.defaultFileExtension)
}
R.id.menu_lock_all -> {
@@ -484,59 +485,87 @@ class GroupActivity : DatabaseLockActivity(),
addNodeButtonView?.setAddEntryClickListener {
mDatabase?.let { database ->
mMainGroup?.let { currentGroup ->
EntrySelectionHelper.doSpecialAction(intent,
{
EntrySelectionHelper.doSpecialAction(
intent = intent,
defaultAction = {
mMainGroup?.nodeId?.let { currentParentGroupId ->
EntryEditActivity.launchToCreate(
this@GroupActivity,
database,
currentParentGroupId,
mEntryActivityResultLauncher
activity = this@GroupActivity,
database = database,
groupId = currentParentGroupId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
},
{
searchAction = {
// Search not used
},
{ searchInfo ->
saveAction = { searchInfo ->
EntryEditActivity.launchToCreateForSave(
this@GroupActivity,
database,
currentGroup.nodeId,
searchInfo
context = this@GroupActivity,
database = database,
groupId = currentGroup.nodeId,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo ->
keyboardSelectionAction = { searchInfo ->
EntryEditActivity.launchForKeyboardSelectionResult(
this@GroupActivity,
database,
currentGroup.nodeId,
searchInfo
context = this@GroupActivity,
database = database,
groupId = currentGroup.nodeId,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo, autofillComponent ->
autofillSelectionAction = { searchInfo, autofillComponent ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
EntryEditActivity.launchForAutofillResult(
this@GroupActivity,
database,
mAutofillActivityResultLauncher,
autofillComponent,
currentGroup.nodeId,
searchInfo
activity = this@GroupActivity,
database = database,
activityResultLauncher = mCredentialActivityResultLauncher,
autofillComponent = autofillComponent,
groupId = currentGroup.nodeId,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
{ searchInfo ->
autofillRegistrationAction = { registerInfo ->
EntryEditActivity.launchToCreateForRegistration(
this@GroupActivity,
database,
currentGroup.nodeId,
searchInfo
context = this@GroupActivity,
database = database,
activityResultLauncher = null,
groupId = currentGroup.nodeId,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
onLaunchActivitySpecialMode()
},
passkeySelectionAction = { searchInfo ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
EntryEditActivity.launchForPasskeySelectionResult(
context = this@GroupActivity,
database = database,
activityResultLauncher = mCredentialActivityResultLauncher,
groupId = currentGroup.nodeId,
searchInfo = searchInfo,
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
passkeyRegistrationAction = { registerInfo ->
EntryEditActivity.launchToCreateForRegistration(
context = this@GroupActivity,
database = database,
activityResultLauncher = mCredentialActivityResultLauncher,
groupId = currentGroup.nodeId,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
onLaunchActivitySpecialMode()
}
@@ -679,30 +708,40 @@ class GroupActivity : DatabaseLockActivity(),
when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess) {
EntrySelectionHelper.doSpecialAction(intent,
{
EntrySelectionHelper.doSpecialAction(
intent = intent,
defaultAction = {
// Standard not used after task
},
{
searchAction = {
// Search not used
},
{
saveAction = {
// Save not used
},
{
keyboardSelectionAction = {
// Keyboard selection
entry?.let {
entrySelectedForKeyboardSelection(database, it)
}
},
{ _, _ ->
autofillSelectionAction = { _, _ ->
// Autofill selection
entry?.let {
entrySelectedForAutofillSelection(database, it)
}
},
{
autofillRegistrationAction = {
// Not use
},
passkeySelectionAction = {
// Passkey selection
entry?.let {
entrySelectedForPasskeySelection(database, it)
}
},
passkeyRegistrationAction = {
// TODO Passkey Registration
}
)
}
@@ -846,27 +885,28 @@ class GroupActivity : DatabaseLockActivity(),
Type.ENTRY -> try {
val entryVersioned = node as Entry
EntrySelectionHelper.doSpecialAction(intent,
{
EntrySelectionHelper.doSpecialAction(
intent = intent,
defaultAction = {
EntryActivity.launch(
this@GroupActivity,
database,
entryVersioned.nodeId,
mEntryActivityResultLauncher
activity = this@GroupActivity,
database = database,
entryId = entryVersioned.nodeId,
activityResultLauncher = mEntryActivityResultLauncher
)
// Do not reload group here
},
{
searchAction = {
// Nothing here, a search is simply performed
},
{ searchInfo ->
saveAction = { searchInfo ->
if (!database.isReadOnly) {
entrySelectedForSave(database, entryVersioned, searchInfo)
loadGroup()
} else
finish()
},
{ searchInfo ->
keyboardSelectionAction = { searchInfo ->
if (!database.isReadOnly
&& searchInfo != null
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
@@ -876,7 +916,7 @@ class GroupActivity : DatabaseLockActivity(),
entrySelectedForKeyboardSelection(database, entryVersioned)
loadGroup()
},
{ searchInfo, _ ->
autofillSelectionAction = { searchInfo, _ ->
if (!database.isReadOnly
&& searchInfo != null
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
@@ -886,9 +926,39 @@ class GroupActivity : DatabaseLockActivity(),
entrySelectedForAutofillSelection(database, entryVersioned)
loadGroup()
},
{ registerInfo ->
autofillRegistrationAction = { registerInfo ->
if (!database.isReadOnly) {
entrySelectedForRegistration(database, entryVersioned, registerInfo)
entrySelectedForRegistration(
database = database,
entry = entryVersioned,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL,
activityResultLauncher = null // TODO Result launcher autofill #765
)
loadGroup()
} else
finish()
},
passkeySelectionAction = { searchInfo ->
if (!database.isReadOnly
&& searchInfo != null
// TODO Passkey setting && PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
) {
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
}
entrySelectedForPasskeySelection(database, entryVersioned)
loadGroup()
},
passkeyRegistrationAction = { registerInfo ->
if (!database.isReadOnly) {
// TODO Passkey setting && PreferencesUtil.isAutofillOverwriteEnable(this@GroupActivity)
entrySelectedForRegistration(
database = database,
entry = entryVersioned,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY,
activityResultLauncher = mCredentialActivityResultLauncher
)
loadGroup()
} else
finish()
@@ -934,18 +1004,33 @@ class GroupActivity : DatabaseLockActivity(),
onValidateSpecialMode()
}
private fun entrySelectedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
removeSearch()
// Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
buildPasskeyResponseAndSetResult(
entryInfo = entry.getEntryInfo(database)
)
}
onValidateSpecialMode()
}
private fun entrySelectedForRegistration(
database: ContextualDatabase,
entry: Entry,
registerInfo: RegisterInfo?
activityResultLauncher: ActivityResultLauncher<Intent>?,
registerInfo: RegisterInfo?,
typeMode: TypeMode
) {
removeSearch()
// Registration to update the entry
EntryEditActivity.launchToUpdateForRegistration(
this@GroupActivity,
database,
entry.nodeId,
registerInfo
context = this@GroupActivity,
database = database,
activityResultLauncher = activityResultLauncher,
entryId = entry.nodeId,
registerInfo = registerInfo,
typeMode = typeMode
)
onLaunchActivitySpecialMode()
}
@@ -961,11 +1046,10 @@ class GroupActivity : DatabaseLockActivity(),
raw = true,
removeTemplateConfiguration = false
)
val modification = entryInfo.saveSearchInfo(database, searchInfo)
// TODO Transform SearchInfo in RegisterInfo
entryInfo.saveSearchInfo(database, searchInfo)
newEntry.setEntryInfo(database, entryInfo)
if (modification) {
updateEntry(entry, newEntry)
}
updateEntry(entry, newEntry)
}
private fun finishNodeAction() {
@@ -1373,7 +1457,8 @@ class GroupActivity : DatabaseLockActivity(),
}
else -> {
// Load the previous group
loadMainGroup(mPreviousGroupsIds.removeLast())
loadMainGroup(mPreviousGroupsIds
.removeAt(mPreviousGroupsIds.lastIndex))
}
}
}
@@ -1569,19 +1654,19 @@ class GroupActivity : DatabaseLockActivity(),
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: AppCompatActivity,
database: ContextualDatabase,
activityResultLaunch: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null,
autoSearch: Boolean = false) {
fun launchForAutofillSelectionResult(activity: AppCompatActivity,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null,
autoSearch: Boolean = false) {
if (database.loaded) {
checkTimeAndBuildIntent(activity, null) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
AutofillHelper.startActivityForAutofillResult(
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
activity,
intent,
activityResultLaunch,
activityResultLauncher,
autofillComponent,
searchInfo
)
@@ -1589,21 +1674,49 @@ class GroupActivity : DatabaseLockActivity(),
}
}
/*
* -------------------------
* Passkey Launch
* -------------------------
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun launchForPasskeySelectionResult(context: Context,
database: ContextualDatabase,
activityResultLauncher: ActivityResultLauncher<Intent>?,
searchInfo: SearchInfo? = null,
autoSearch: Boolean = false) {
if (database.loaded) {
checkTimeAndBuildIntent(context, null) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
context,
intent,
activityResultLauncher,
searchInfo
)
}
}
}
/*
* -------------------------
* Registration Launch
* -------------------------
*/
fun launchForRegistration(context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>?,
database: ContextualDatabase,
registerInfo: RegisterInfo? = null) {
registerInfo: RegisterInfo? = null,
typeMode: TypeMode) {
if (database.loaded && !database.isReadOnly) {
checkTimeAndBuildIntent(context, null) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, false)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
activityResultLauncher,
intent,
registerInfo
registerInfo,
typeMode
)
}
}
@@ -1619,153 +1732,232 @@ class GroupActivity : DatabaseLockActivity(),
onValidateSpecialMode: () -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
EntrySelectionHelper.doSpecialAction(activity.intent,
{
// Default action
launch(
activity,
activityResultLauncher: ActivityResultLauncher<Intent>?) {
EntrySelectionHelper.doSpecialAction(
intent = activity.intent,
defaultAction = {
// Default action
launch(
activity,
database,
true
)
},
searchAction = { searchInfo ->
// Search action
if (database.loaded) {
launchForSearchResult(activity,
database,
true
)
},
{ searchInfo ->
// Search action
if (database.loaded) {
launchForSearchResult(activity,
searchInfo,
true)
onLaunchActivitySpecialMode()
} else {
// Simply close if database not opened
onCancelSpecialMode()
}
},
saveAction = { searchInfo ->
// Save info
if (database.loaded) {
if (!database.isReadOnly) {
launchForSaveResult(
activity,
database,
searchInfo,
true)
false
)
onLaunchActivitySpecialMode()
} else {
// Simply close if database not opened
Toast.makeText(
activity.applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG
)
.show()
onCancelSpecialMode()
}
},
{ searchInfo ->
// Save info
if (database.loaded) {
if (!database.isReadOnly) {
launchForSaveResult(
activity,
database,
searchInfo,
false
)
onLaunchActivitySpecialMode()
} else {
Toast.makeText(
activity.applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG
)
.show()
onCancelSpecialMode()
}
}
},
{ searchInfo ->
// Keyboard selection
SearchHelper.checkAutoSearchInfo(activity,
database,
searchInfo,
{ _, items ->
MagikeyboardService.performSelection(
items,
{ entryInfo ->
// Keyboard populated
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
activity,
entryInfo
)
onValidateSpecialMode()
},
{ autoSearch ->
launchForKeyboardSelectionResult(activity,
database,
searchInfo,
autoSearch)
onLaunchActivitySpecialMode()
}
}
},
keyboardSelectionAction = { searchInfo ->
// Keyboard selection
SearchHelper.checkAutoSearchInfo(
context = activity,
database = database,
searchInfo = searchInfo,
onItemsFound = { _, items ->
MagikeyboardService.performSelection(
items,
{ entryInfo ->
// Keyboard populated
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
activity,
entryInfo
)
onValidateSpecialMode()
},
{
// Here no search info found, disable auto search
{ autoSearch ->
launchForKeyboardSelectionResult(activity,
database,
searchInfo,
false)
autoSearch)
onLaunchActivitySpecialMode()
},
{
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
},
onItemNotFound = {
// Here no search info found, disable auto search
launchForKeyboardSelectionResult(activity,
database,
searchInfo,
false)
onLaunchActivitySpecialMode()
},
onDatabaseClosed = {
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
},
autofillSelectionAction = { searchInfo, autofillComponent ->
// Autofill selection
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SearchHelper.checkAutoSearchInfo(
context = activity,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Response is build
AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
onValidateSpecialMode()
},
onItemNotFound = {
// Here no search info found, disable auto search
launchForAutofillSelectionResult(
activity = activity,
database = database,
autofillComponent = autofillComponent,
searchInfo = searchInfo,
autoSearch = false,
activityResultLauncher = activityResultLauncher)
onLaunchActivitySpecialMode()
},
onDatabaseClosed = {
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
},
{ searchInfo, autofillComponent ->
// Autofill selection
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SearchHelper.checkAutoSearchInfo(activity,
database,
searchInfo,
{ openedDatabase, items ->
// Response is build
AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
} else {
onCancelSpecialMode()
}
},
autofillRegistrationAction = { registerInfo ->
// Autofill registration
if (!database.isReadOnly) {
SearchHelper.checkAutoSearchInfo(
context = activity,
database = database,
searchInfo = registerInfo?.searchInfo,
onItemsFound = { _, _ ->
// No auto search, it's a registration
launchForRegistration(
context = activity,
activityResultLauncher = null, // TODO Autofill result Launcher #765
database = database,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
onLaunchActivitySpecialMode()
},
onItemNotFound = {
// Here no search info found, disable auto search
launchForRegistration(
context = activity,
activityResultLauncher = null, // TODO Autofill result Launcher #765
database = database,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
onLaunchActivitySpecialMode()
},
onDatabaseClosed = {
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
} else {
Toast.makeText(activity.applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
onCancelSpecialMode()
}
},
passkeySelectionAction = { searchInfo ->
// Passkey selection
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
SearchHelper.checkAutoSearchInfo(
context = activity,
database = database,
searchInfo = searchInfo,
onItemsFound = { _, items ->
// Response is build
EntrySelectionHelper.performSelection(
items = items,
actionPopulateCredentialProvider = { entryInfo ->
activity.buildPasskeyResponseAndSetResult(entryInfo)
onValidateSpecialMode()
},
{
// Here no search info found, disable auto search
launchForAutofillResult(activity,
database,
autofillActivityResultLauncher,
autofillComponent,
searchInfo,
false)
actionEntrySelection = {
launchForPasskeySelectionResult(
context = activity,
database = database,
searchInfo = searchInfo,
activityResultLauncher = activityResultLauncher,
autoSearch = true
)
onLaunchActivitySpecialMode()
},
{
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
} else {
onCancelSpecialMode()
}
},
{ registerInfo ->
// Autofill registration
if (!database.isReadOnly) {
SearchHelper.checkAutoSearchInfo(activity,
database,
registerInfo?.searchInfo,
{ _, _ ->
// No auto search, it's a registration
launchForRegistration(activity,
database,
registerInfo)
onLaunchActivitySpecialMode()
},
{
// Here no search info found, disable auto search
launchForRegistration(activity,
database,
registerInfo)
onLaunchActivitySpecialMode()
},
{
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
} else {
Toast.makeText(activity.applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
onCancelSpecialMode()
}
})
)
},
onItemNotFound = {
// Here no search info found, disable auto search
launchForPasskeySelectionResult(
context = activity,
database = database,
searchInfo = searchInfo,
activityResultLauncher = activityResultLauncher
)
onLaunchActivitySpecialMode()
},
onDatabaseClosed = {
// Simply close if database not opened, normally not happened
onCancelSpecialMode()
}
)
} else {
onCancelSpecialMode()
}
},
passkeyRegistrationAction = { registerInfo ->
// Passkey registration
if (!database.isReadOnly) {
launchForRegistration(
context = activity,
activityResultLauncher = activityResultLauncher,
database = database,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
onLaunchActivitySpecialMode()
} else {
Toast.makeText(activity.applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
onCancelSpecialMode()
}
}
)
}
}
}

View File

@@ -32,7 +32,6 @@ 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
@@ -43,32 +42,42 @@ 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.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.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
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 +88,27 @@ 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 viewModels()
private val mPasswordActivityEducation = PasswordActivityEducation(this)
@@ -113,10 +123,8 @@ 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
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher()
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)
@@ -166,21 +174,15 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
// Listen password checkbox to init advanced unlock and confirmation button
mainCredentialView?.onPasswordChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
mainCredentialView?.onKeyFileChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
mainCredentialView?.onHardwareKeyChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel.checkConditionToStoreCredential(
condition = verified
)
}
// TODO Async by ViewModel
enableConfirmationButton()
}
// Observe if default database
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
@@ -202,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
@@ -228,20 +237,53 @@ 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.uiState.collect { uiState ->
// New value received
uiState.credentialRequiredCipher?.let { cipher ->
mDeviceUnlockViewModel.encryptCredential(
credential = getCredentialForEncryption(),
cipher = cipher
)
}
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
onCredentialEncrypted(cipherEncryptDatabase)
mDeviceUnlockViewModel.consumeCredentialEncrypted()
}
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
onCredentialDecrypted(cipherDecryptDatabase)
mDeviceUnlockViewModel.consumeCredentialDecrypted()
}
uiState.exception?.let { error ->
Snackbar.make(
coordinatorLayout,
deviceUnlockError(error, this@MainCredentialActivity),
Snackbar.LENGTH_LONG
).asError().show()
mDeviceUnlockViewModel.exceptionShown()
}
}
}
}
}
}
override fun onResume() {
super.onResume()
// Init Biometric elements only if allowed
if (PreferencesUtil.isAdvancedUnlockEnable(this)) {
advancedUnlockFragment = supportFragmentManager
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
if (advancedUnlockFragment == null) {
advancedUnlockFragment = AdvancedUnlockFragment().also {
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_advanced_unlock_container_view,
R.id.fragment_device_unlock_container_view,
it,
UNLOCK_FRAGMENT_TAG
)
@@ -258,11 +300,6 @@ 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)
}
@@ -296,9 +333,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 {
@@ -395,28 +429,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()
@@ -433,7 +450,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()
@@ -485,7 +515,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
loadDatabase()
} else {
// Init Biometric elements
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel.connect(databaseFileUri)
}
}
enableConfirmationButton()
@@ -513,13 +545,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)
@@ -645,17 +670,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
)
)
},
@@ -685,6 +710,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)
}
@@ -692,6 +723,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
@@ -806,14 +844,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class)
fun launchForAutofillResult(activity: AppCompatActivity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
AutofillHelper.startActivityForAutofillResult(
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
activity,
intent,
activityResultLauncher,
@@ -822,21 +860,51 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
}
/*
* -------------------------
* Passkey Launch
* -------------------------
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Throws(FileNotFoundException::class)
fun launchForPasskeyResult(activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
activity,
intent,
activityResultLauncher,
searchInfo
)
}
}
/*
* -------------------------
* Registration Launch
* -------------------------
*/
fun launchForRegistration(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
registerInfo: RegisterInfo?) {
fun launchForRegistration(
activity: Activity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
typeMode: TypeMode,
registerInfo: RegisterInfo?
) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
activity,
intent,
registerInfo)
context = activity,
activityResultLauncher = activityResultLauncher,
intent = intent,
typeMode = typeMode,
registerInfo = registerInfo
)
}
}
@@ -852,74 +920,104 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
activityResultLauncher: ActivityResultLauncher<Intent>?) {
try {
EntrySelectionHelper.doSpecialAction(activity.intent,
{
launch(
activity,
databaseUri,
keyFile,
hardwareKey
)
},
{ 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
EntrySelectionHelper.doSpecialAction(
intent = activity.intent,
defaultAction = {
launch(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey
)
},
searchAction = { searchInfo ->
launchForSearchResult(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
saveAction = { searchInfo ->
launchForSaveResult(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
keyboardSelectionAction = { searchInfo ->
launchForKeyboardResult(
activity = activity,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
autofillSelectionAction = { searchInfo, autofillComponent ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
launchForAutofillResult(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
autofillComponent = autofillComponent,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
autofillRegistrationAction = { registerInfo ->
launchForRegistration(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = TypeMode.AUTOFILL,
registerInfo = registerInfo
)
onLaunchActivitySpecialMode()
},
passkeySelectionAction = { searchInfo ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
launchForPasskeyResult(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
passkeyRegistrationAction = { registerInfo ->
launchForRegistration(
activity = activity,
activityResultLauncher = activityResultLauncher,
databaseFile = databaseUri,
keyFile = keyFile,
hardwareKey = hardwareKey,
typeMode = TypeMode.PASSKEY,
registerInfo = registerInfo
)
onLaunchActivitySpecialMode()
}
)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)

View File

@@ -45,6 +45,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
}
@Suppress("DEPRECATION")
@Deprecated(message = "")
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

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
@@ -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

@@ -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,15 +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_TOTP_PERIOD
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() {

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
@@ -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,8 +36,8 @@ 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.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.adapters.NodesAdapter
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Group

View File

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

View File

@@ -5,14 +5,14 @@ import android.os.Bundle
import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getBinaryDir
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
@@ -41,6 +41,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
return true
}
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
@@ -48,6 +49,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
@@ -77,7 +79,13 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
mDatabaseTaskProvider?.startDatabaseLoad(
databaseUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
fixDuplicateUuid
)
}
protected fun closeDatabase() {
@@ -88,7 +96,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
super.onResume()
mDatabaseTaskProvider?.registerProgressTask()
}
override fun onPause() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()

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
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 {
@@ -184,8 +188,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
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()
@@ -413,8 +415,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
invalidateOptionsMenu()
LOCKING_ACTIVITY_UI_VISIBLE = true
}
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
@@ -429,8 +429,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
override fun onPause() {
LOCKING_ACTIVITY_UI_VISIBLE = false
super.onPause()
if (mTimeoutEnable) {
@@ -480,9 +478,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,11 @@ 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
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
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
@@ -42,14 +44,8 @@ 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() {
@@ -118,7 +114,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
val searchInfo: SearchInfo? = registerInfo?.searchInfo
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
// To show the selection mode
@@ -136,12 +133,13 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
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) {

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

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.adapters
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Build
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
@@ -418,6 +417,7 @@ class NodesAdapter (
}
}
// OTP
val otpElement = entry.getOtpElement()
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
if (otpElement != null
@@ -438,7 +438,11 @@ class NodesAdapter (
holder.otpContainer?.visibility = View.GONE
}
holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE
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)
@@ -451,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 {
@@ -459,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 {
@@ -467,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)
}
@@ -530,9 +537,8 @@ class NodesAdapter (
holder?.otpToken?.apply {
text = otpElement?.tokenString
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
textDirection = View.TEXT_DIRECTION_LTR
}
textDirection = View.TEXT_DIRECTION_LTR
}
holder?.otpContainer?.setOnClickListener {
otpElement?.token?.let { token ->
@@ -612,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

@@ -28,9 +28,10 @@ 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
@@ -43,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
@@ -70,15 +71,15 @@ class CipherDatabaseAction(context: Context) {
@Synchronized
private fun attachService(performedAction: () -> Unit) {
ContextCompat.registerReceiver(applicationContext, mAdvancedUnlockBroadcastReceiver,
ContextCompat.registerReceiver(applicationContext, mDeviceUnlockBroadcastReceiver,
IntentFilter().apply {
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
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()
}
@@ -87,7 +88,7 @@ class CipherDatabaseAction(context: Context) {
}
}
try {
AdvancedUnlockNotificationService.bindService(applicationContext,
DeviceUnlockNotificationService.bindService(applicationContext,
mServiceConnection!!,
Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
@@ -99,11 +100,11 @@ class CipherDatabaseAction(context: Context) {
@Synchronized
private fun detachService() {
try {
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
applicationContext.unregisterReceiver(mDeviceUnlockBroadcastReceiver)
} catch (_: Exception) {}
mServiceConnection?.let {
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
DeviceUnlockNotificationService.unbindService(applicationContext, it)
}
}
@@ -123,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
@@ -150,7 +155,11 @@ class CipherDatabaseAction(context: Context) {
)
}
}
cipherDatabaseResultListener.invoke(cipherDatabase)
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
}
}
}
} else {
IOActionTask(
@@ -158,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
@@ -170,21 +179,29 @@ 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) {
getCipherDatabase(databaseUri) {
contains.invoke(it != null)
if (databaseUri == null) {
contains.invoke(false)
} else {
getCipherDatabase(databaseUri) {
contains.invoke(it != null)
}
}
}
fun resetCipherParameters(databaseUri: Uri) {
fun resetCipherParameters(databaseUri: Uri?) {
containsCipherDatabase(databaseUri) { contains ->
if (contains) {
mBinder?.resetTimer()
@@ -206,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(
@@ -221,7 +242,11 @@ class CipherDatabaseAction(context: Context) {
}
},
{
cipherDatabaseResultListener?.invoke()
cipherDatabaseResultListener?.invoke() ?: run {
mDatabaseListeners.forEach { listener ->
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
}
}
}
).execute()
}
@@ -233,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(
@@ -241,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() {
@@ -259,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,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,678 +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
}
)
cipherDatabaseAction.resetCipherParameters(databaseUri)
}
}
@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

@@ -17,17 +17,32 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.helpers
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 com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import android.util.Log
import android.widget.RemoteViews
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.putEnumExtra
object EntrySelectionHelper {
@@ -37,6 +52,33 @@ object EntrySelectionHelper {
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
/**
* Utility method to build a registerForActivityResult,
* Used recursively, close each activity with return data
*/
fun AppCompatActivity.buildActivityResultLauncher(
lockDatabase: Boolean = false,
dataTransformation: (data: Intent?) -> Intent? = { it },
): ActivityResultLauncher<Intent> {
return this.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
val resultCode = it.resultCode
if (resultCode == Activity.RESULT_OK) {
this.setResult(resultCode, dataTransformation(it.data))
}
if (resultCode == Activity.RESULT_CANCELED) {
this.setResult(Activity.RESULT_CANCELED)
}
this.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
// Close the database
this.sendBroadcast(Intent(LOCK_ACTION))
}
}
}
fun startActivityForSearchModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo) {
@@ -66,15 +108,52 @@ object EntrySelectionHelper {
context.startActivity(intent)
}
fun startActivityForRegistrationModeResult(context: Context,
intent: Intent,
registerInfo: RegisterInfo?) {
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
// At the moment, only autofill for registration
/**
* Utility method to start an activity with an Autofill for result
*/
@RequiresApi(Build.VERSION_CODES.O)
fun startActivityForAutofillSelectionModeResult(
context: Context,
intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?
) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
intent.addAutofillComponent(context, autofillComponent)
addSearchInfoInIntent(intent, searchInfo)
activityResultLauncher?.launch(intent)
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun startActivityForPasskeySelectionModeResult(
context: Context,
intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
searchInfo: SearchInfo?
) {
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
addTypeModeInIntent(intent, TypeMode.PASSKEY)
addSearchInfoInIntent(intent, searchInfo)
activityResultLauncher?.launch(intent)
}
fun startActivityForRegistrationModeResult(
context: Context?,
activityResultLauncher: ActivityResultLauncher<Intent>?,
intent: Intent,
registerInfo: RegisterInfo?,
typeMode: TypeMode
) {
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
addTypeModeInIntent(intent, typeMode)
addRegisterInfoInIntent(intent, registerInfo)
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
if (activityResultLauncher == null) {
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
activityResultLauncher?.launch(intent) ?: context?.startActivity(intent) ?:
throw IllegalStateException("At least Context or ActivityResultLauncher must not be null")
}
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
@@ -103,8 +182,13 @@ object EntrySelectionHelper {
}
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
// TODO Replace by Intent.addSpecialMode
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
}
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
return this
}
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -115,8 +199,13 @@ object EntrySelectionHelper {
}
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
// TODO Replace by Intent.addTypeMode
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
}
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
return this
}
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -131,6 +220,17 @@ object EntrySelectionHelper {
intent.removeExtra(KEY_TYPE_MODE)
}
/**
* Intent sender uses special retains data in callback
*/
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
return (specialMode == SpecialMode.SELECTION
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|| (specialMode == SpecialMode.REGISTRATION
&& typeMode == TypeMode.PASSKEY)
}
fun doSpecialAction(intent: Intent,
defaultAction: () -> Unit,
searchAction: (searchInfo: SearchInfo) -> Unit,
@@ -138,7 +238,9 @@ object EntrySelectionHelper {
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
autofillSelectionAction: (searchInfo: SearchInfo?,
autofillComponent: AutofillComponent) -> Unit,
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit,
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit,
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
when (retrieveSpecialModeFromIntent(intent)) {
SpecialMode.DEFAULT -> {
@@ -186,6 +288,7 @@ object EntrySelectionHelper {
defaultAction.invoke()
}
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo)
else -> {
// In this case, error
removeModesFromIntent(intent)
@@ -202,10 +305,59 @@ object EntrySelectionHelper {
}
SpecialMode.REGISTRATION -> {
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
autofillRegistrationAction.invoke(registerInfo)
if (!isIntentSenderMode(
specialMode = retrieveSpecialModeFromIntent(intent),
typeMode = retrieveTypeModeFromIntent(intent))
) {
removeModesFromIntent(intent)
removeInfoFromIntent(intent)
}
when (retrieveTypeModeFromIntent(intent)) {
TypeMode.AUTOFILL -> {
autofillRegistrationAction.invoke(registerInfo)
}
TypeMode.PASSKEY -> {
passkeyRegistrationAction.invoke(registerInfo)
}
else -> {
// Do other registration type
}
}
}
}
}
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 Icon.createWithBitmap(bitmap)
}
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
return null
}
}

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.activities.helpers
package com.kunzisoft.keepass.credentialprovider
enum class SpecialMode {
DEFAULT,

View File

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

View File

@@ -17,9 +17,8 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
package com.kunzisoft.keepass.credentialprovider.activity
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
@@ -30,28 +29,30 @@ 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.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
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.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest
import com.kunzisoft.keepass.credentialprovider.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.utils.WebDomain
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
@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
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher(lockDatabase = true)
override fun applyCustomStyle(): Boolean {
return false
@@ -72,11 +73,13 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
// To pass extra inline request
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
compatInlineSuggestionsRequest = bundle.getParcelableCompat(
KEY_INLINE_SUGGESTION
)
}
// Build search param
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
WebDomain.getConcreteWebDomain(
AppUtil.getConcreteWebDomain(
this,
searchInfo.webDomain
) { concreteWebDomain ->
@@ -102,16 +105,18 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
SpecialMode.REGISTRATION -> {
// To register info
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(
KEY_REGISTER_INFO
)
val searchInfo = SearchInfo(registerInfo?.searchInfo)
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchRegistration(database, searchInfo, registerInfo)
}
}
else -> {
// Not an autofill call
setResult(Activity.RESULT_CANCELED)
setResult(RESULT_CANCELED)
finish()
}
}
@@ -122,43 +127,48 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo) {
if (autofillComponent == null) {
setResult(Activity.RESULT_CANCELED)
setResult(RESULT_CANCELED)
finish()
} else if (!KeeAutofillService.autofillAllowedFor(
} else if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = 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)
}
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, items ->
// Items found
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
finish()
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
GroupActivity.launchForAutofillSelectionResult(
this,
openedDatabase,
mCredentialActivityResultLauncher,
autofillComponent,
searchInfo,
false
)
},
onDatabaseClosed = {
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(
this,
mCredentialActivityResultLauncher,
autofillComponent,
searchInfo
)
}
)
} else {
showBlockRestartMessage()
setResult(RESULT_CANCELED)
finish()
}
}
@@ -169,40 +179,53 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
applicationId = searchInfo.applicationId,
webDomain = searchInfo.webDomain,
context = 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)
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, _ ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = null, // TODO Autofill result launcher #765
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
showReadOnlySaveMessage()
}
},
onItemNotFound = { openedDatabase ->
if (!readOnly) {
// Show the database UI to select the entry
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = null, // TODO Autofill result launcher #765
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
} else {
showReadOnlySaveMessage()
}
},
onDatabaseClosed = {
// If database not open
FileDatabaseSelectActivity.launchForRegistration(
context = this,
activityResultLauncher = null, // TODO Autofill result launcher #765
registerInfo = registerInfo,
typeMode = TypeMode.AUTOFILL
)
}
)
} else {
showBlockRestartMessage()
setResult(RESULT_CANCELED)
}
finish()
}

View File

@@ -17,23 +17,25 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.core.net.toUri
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.magikeyboard.MagikeyboardService
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.AppUtil
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,
@@ -73,7 +75,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
sharedWebDomain = Uri.parse(extra).host
sharedWebDomain = extra.toUri().host
}
}
launchSelection(database, sharedWebDomain, otpString)
@@ -107,7 +109,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
this.otpString = otpString
}
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launch(database, searchInfo)
}
@@ -121,87 +123,105 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
// 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(
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { 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,
false)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
autoSearch
)
}
} 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
)
}
},
onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
if (searchInfo.otpString != null) {
if (!readOnly) {
GroupActivity.launchForSaveResult(
this,
openedDatabase,
searchInfo,
false
)
} 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)
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
)
}
},
onDatabaseClosed = {
// If database not open
if (searchInfo.otpString != null) {
FileDatabaseSelectActivity.launchForSaveResult(
this,
searchInfo
)
} else if (searchShareForMagikeyboard) {
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(
this,
searchInfo
)
} else {
FileDatabaseSelectActivity.launchForSearchResult(
this,
searchInfo
)
}
}
)
}

View File

@@ -0,0 +1,460 @@
/*
* 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.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
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.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch
import java.io.IOException
import java.io.InvalidObjectException
import java.util.UUID
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class PasskeyLauncherActivity : DatabaseModeActivity() {
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
private var mPasskey: Passkey? = null
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher(
lockDatabase = true,
dataTransformation = { intent ->
// Build a new formatted response from the selection response
val responseIntent = Intent()
try {
Log.d(TAG, "Passkey selection result")
if (intent == null)
throw IOException("Intent is null")
val passkey = intent.retrievePasskey()
?: throw IOException("Passkey is null")
val appOrigin = intent.retrieveAppOrigin()
?: throw IOException("App origin is null")
intent.removePasskey()
intent.removeAppOrigin()
mUsageParameters?.let { usageParameters ->
// Check verified origin
PendingIntentHandler.setGetCredentialResponse(
responseIntent,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
clientDataResponse = getVerifiedGETClientDataResponse(
usageParameters = usageParameters,
appOrigin = appOrigin
),
passkey = passkey
)
)
)
} ?: run {
throw IOException("Usage parameters is null")
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create selection response for passkey", e)
showError(e)
}
// Return the response
responseIntent
}
)
private var mPasskeyRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher(
lockDatabase = true,
dataTransformation = { intent ->
// Build a new formatted response from the creation response
val responseIntent = Intent()
try {
Log.d(TAG, "Passkey registration result")
val passkey = intent?.retrievePasskey()
intent?.removePasskey()
intent?.removeAppOrigin()
// If registered passkey is the same as the one we want to validate,
if (mPasskey == passkey) {
mCreationParameters?.let {
PendingIntentHandler.setCreateCredentialResponse(
intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters = it
)
)
}
} else {
throw SecurityException("Passkey was modified before registration")
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create registration response for passkey", e)
showError(e)
}
responseIntent
}
)
override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return false
}
private fun cancelRequest() {
setResult(RESULT_CANCELED)
finish()
}
private fun cancelRequest(e: Throwable) {
Log.e(TAG, "Passkey launch error", e)
showError(e)
cancelRequest()
}
/**
* Launch the main action to manage Passkey
*/
private suspend fun launchPasskeyAction(database: ContextualDatabase?) {
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
val nodeId = intent.retrieveNodeId()
checkSecurity(intent, nodeId)
when (mSpecialMode) {
SpecialMode.SELECTION -> {
launchSelection(database, nodeId, searchInfo, appOrigin)
}
SpecialMode.REGISTRATION -> {
// TODO Registration in predefined group
// launchRegistration(database, nodeId, mSearchInfo)
launchRegistration(database, null, searchInfo)
}
else -> {
throw InvalidObjectException("Passkey launch mode not supported")
}
}
}
/**
* Display a dialog that asks the user to add an app to the list of privileged apps.
*/
private fun showAppPrivilegedDialog(e: PrivilegedAllowLists.PrivilegedException) {
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,
e.temptingApp.toString()
)
)
.append("\n\n")
.append(getString(R.string.passkeys_privileged_apps_explanation))
.toString()
)
setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(CoroutineExceptionHandler { _, e ->
cancelRequest(e)
}) {
saveCustomPrivilegedApps(
context = application,
privilegedApps = listOf(e.temptingApp)
)
launchPasskeyAction(mDatabase)
}
}
setNegativeButton(android.R.string.cancel) { _, _ ->
cancelRequest()
}
setOnCancelListener {
cancelRequest()
}
}.create().show()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
lifecycleScope.launch(CoroutineExceptionHandler { _, e ->
when (e) {
is PrivilegedAllowLists.PrivilegedException -> showAppPrivilegedDialog(e)
else -> cancelRequest(e)
}
}) {
launchPasskeyAction(database)
}
}
private fun autoSelectPasskeyAndSetResult(
database: ContextualDatabase?,
nodeId: UUID,
appOrigin: AppOrigin
) {
mUsageParameters?.let { usageParameters ->
// To get the passkey from the database
val passkey = database
?.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
?.passkey
?: throw GetCredentialUnknownException("No passkey with nodeId $nodeId found")
val result = Intent()
PendingIntentHandler.setGetCredentialResponse(
result,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
clientDataResponse = getVerifiedGETClientDataResponse(
usageParameters = usageParameters,
appOrigin = appOrigin
),
passkey = passkey
)
)
)
setResult(RESULT_OK, result)
finish()
} ?: run {
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
setResult(RESULT_CANCELED)
finish()
}
}
private suspend fun launchSelection(
database: ContextualDatabase?,
nodeId: UUID?,
searchInfo: SearchInfo,
appOrigin: AppOrigin
) {
Log.d(TAG, "Launch passkey selection")
retrievePasskeyUsageRequestParameters(
intent = intent,
context = applicationContext
) { usageParameters ->
// Save the requested parameters
mUsageParameters = usageParameters
// Manage the passkey to use
nodeId?.let { nodeId ->
autoSelectPasskeyAndSetResult(database, nodeId, appOrigin)
} ?: run {
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { _, _ ->
Log.w(
TAG, "Passkey found for auto selection, should not append," +
"use PasskeyProviderService instead"
)
finish()
},
onItemNotFound = { openedDatabase ->
Log.d(
TAG, "No Passkey found for selection," +
"launch manual selection in opened database"
)
GroupActivity.launchForPasskeySelectionResult(
context = this,
database = openedDatabase,
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = null,
autoSearch = false
)
},
onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database")
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
activity = this,
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = searchInfo,
)
}
)
}
}
}
private fun autoRegisterPasskeyAndSetResult(
database: ContextualDatabase?,
nodeId: UUID,
passkey: Passkey
) {
// TODO Overwrite and Register in a predefined group
mCreationParameters?.let { creationParameters ->
// To set the passkey to the database
setResult(RESULT_OK)
finish()
} ?: run {
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
setResult(RESULT_CANCELED)
finish()
}
}
private suspend fun launchRegistration(
database: ContextualDatabase?,
nodeId: UUID?,
searchInfo: SearchInfo
) {
Log.d(TAG, "Launch passkey registration")
retrievePasskeyCreationRequestParameters(
intent = intent,
context = applicationContext,
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
// Save the requested parameters
mPasskey = passkey
mCreationParameters = publicKeyCredentialParameters
// Manage the passkey and create a register info
val registerInfo = RegisterInfo(
searchInfo = searchInfo,
passkey = passkey,
appOrigin = appInfoToStore
)
// If nodeId already provided
nodeId?.let { nodeId ->
autoRegisterPasskeyAndSetResult(database, nodeId, passkey)
} ?: run {
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, _ ->
Log.w(TAG, "Passkey found for registration, " +
"but launch manual registration for a new entry")
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
},
onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database")
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
},
onDatabaseClosed = {
Log.d(TAG, "Manual passkey registration in closed database")
FileDatabaseSelectActivity.launchForRegistration(
context = this,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
}
)
}
}
)
}
private fun showError(e: Throwable) {
Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).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,
(Math.random() * Integer.MAX_VALUE).toInt(),
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

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.autofill
package com.kunzisoft.keepass.credentialprovider.autofill
import android.app.assist.AssistStructure

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.SuppressLint
import android.app.Activity
@@ -40,17 +40,13 @@ 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,7 +54,6 @@ 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.getParcelableExtraCompat
import kotlin.math.min
@@ -263,7 +258,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(
@@ -294,23 +289,6 @@ 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,
@@ -338,11 +316,7 @@ object AutofillHelper {
context,
0,
Intent(context, AutofillSettingsActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
PendingIntent.FLAG_IMMUTABLE
)
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
@@ -353,7 +327,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)
})
@@ -534,7 +508,9 @@ object AutofillHelper {
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)
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()
}
@@ -558,45 +534,14 @@ object AutofillHelper {
}
}
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>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
autofillComponent.compatInlineSuggestionsRequest?.let {
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
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

@@ -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.SuppressLint
import android.app.PendingIntent
@@ -43,8 +43,8 @@ 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.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
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
@@ -53,7 +53,7 @@ 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 com.kunzisoft.keepass.utils.AppUtil
import org.joda.time.DateTime
@@ -120,7 +120,7 @@ class KeeAutofillService : AutofillService() {
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
}
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
@@ -143,25 +143,28 @@ class KeeAutofillService : AutofillService() {
parseResult: StructureParser.Result,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this,
database,
searchInfo,
{ openedDatabase, items ->
callback.onSuccess(
AutofillHelper.buildResponse(this, openedDatabase,
items, parseResult, inlineSuggestionsRequest)
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { 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)
}
)
},
onItemNotFound = { openedDatabase ->
// Show UI if no search result
showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, inlineSuggestionsRequest, callback)
},
onDatabaseClosed = {
// Show UI if database not open
showUIForEntrySelection(parseResult, null,
searchInfo, inlineSuggestionsRequest, callback)
}
)
}
@@ -385,19 +388,21 @@ class KeeAutofillService : AutofillService() {
// 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(),
searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
},
username = parseResult.usernameValue?.textValue?.toString(),
password = parseResult.passwordValue?.textValue?.toString(),
creditCard =
CreditCard(
parseResult.creditCardHolder,
parseResult.creditCardNumber,
expiration,
parseResult.cardVerificationValue
))
)
)
// TODO Callback in each activity #765
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {

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

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,9 @@ 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.activity.EntrySelectionLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Field
@@ -324,9 +324,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 +341,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 +362,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
}
)
},
{
onItemNotFound = {
// Select if not found
launchEntrySelection(searchInfo)
},
{
onDatabaseClosed = {
// Select if database not opened
removeEntryInfo()
launchEntrySelection(searchInfo)
@@ -463,21 +464,18 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
fun performSelection(items: List<EntryInfo>,
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
if (items.size == 1) {
val itemFound = items[0]
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)
}
EntrySelectionHelper.performSelection(
items = items,
actionPopulateCredentialProvider = { itemFound ->
if (entryUUID != itemFound.id) {
actionPopulateKeyboard.invoke(itemFound)
} else {
// Force selection if magikeyboard already populated
actionEntrySelection.invoke(false)
}
},
actionEntrySelection = actionEntrySelection
)
}
fun populateKeyboardAndMoveAppToBackground(activity: Activity,

View File

@@ -0,0 +1,355 @@
/*
* 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.helper.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo
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
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)
}
}
override fun onDestroy() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onDestroy()
}
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
return SearchInfo().apply {
this.relyingParty = relyingParty
this.isAPasskeySearch = true
this.query = relyingParty
}
}
override fun onBeginGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
) {
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
try {
processGetCredentialsRequest(request)?.let { response ->
callback.onResult(response)
} ?: run {
callback.onError(GetCredentialUnknownException())
}
} catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
callback.onError(GetCredentialUnknownException())
}
}
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? {
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
for (option in request.beginGetCredentialOptions) {
when (option) {
is BeginGetPublicKeyCredentialOption -> {
credentialEntries.addAll(
populatePasskeyData(option)
)
return BeginGetCredentialResponse(credentialEntries)
}
}
}
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption")
return null
}
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
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 = true
)
)
}
}
},
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_locked_database_username),
displayName = getString(R.string.passkey_selection_description),
icon = defaultIcon,
pendingIntent = pendingIntent,
beginGetPublicKeyCredentialOption = option,
lastUsedTime = Instant.now(),
isAutoSelectAllowed = false
)
)
}
},
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_locked_database_username),
displayName = getString(R.string.passkey_locked_database_description),
icon = defaultIcon,
pendingIntent = pendingIntent,
beginGetPublicKeyCredentialOption = option,
lastUsedTime = Instant.now(),
isAutoSelectAllowed = true
)
)
}
}
)
return passkeyEntries
}
override fun onBeginCreateCredentialRequest(
request: BeginCreateCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
) {
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
try {
processCreateCredentialRequest(request)?.let { response ->
callback.onResult(response)
} ?: let {
callback.onError(CreateCredentialUnknownException())
}
} catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
callback.onError(CreateCredentialUnknownException())
}
}
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
// Request is passkey type
return handleCreatePasskeyQuery(request)
}
}
// request type not supported
Log.w(javaClass.simpleName, "unknown type of BeginCreateCredentialRequest")
return null
}
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): BeginCreateCredentialResponse {
val accountName = mDatabase?.name ?: getString(R.string.passkey_locked_database_username)
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 CreateCredentialUnknownException(
"Unable to register or overwrite a passkey in a database that is read only"
)
} 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
)
)
)
}
}*/
}
},
onItemNotFound = { database ->
// To create a new entry
if (database.isReadOnly) {
throw CreateCredentialUnknownException(
"Unable to register a new passkey in a database that is read only"
)
} else {
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
}
},
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)
)
)
}
}
)
return BeginCreateCredentialResponse(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,66 @@
/*
* 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 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 {
signature = Signature.sign(privateKey, dataToSign())
?: 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,
val 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.os.ParcelUuid
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
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.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.model.SearchInfo
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
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_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
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()
/**
* Build the Passkey response for one entry
*/
fun Activity.buildPasskeyResponseAndSetResult(
entryInfo: EntryInfo,
extras: Bundle? = null
) {
try {
entryInfo.passkey?.let {
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.putExtra(EXTRA_PASSKEY, entryInfo.passkey)
mReplyIntent.putExtra(EXTRA_APP_ORIGIN, entryInfo.appOrigin)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
Log.w(javaClass.name, "Failed Passkey manual selection")
setResult(Activity.RESULT_CANCELED)
}
} catch (e: Exception) {
Log.e(javaClass.name, "Cant add passkey entry as result", e)
setResult(Activity.RESULT_CANCELED)
}
}
/**
* 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()
)
})
}
/**
* 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 search info to the intent
*/
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
searchInfo?.let {
putExtra(EXTRA_SEARCH_INFO, searchInfo)
}
}
/**
* Retrieve the search info from the intent
*/
fun Intent.retrieveSearchInfo(): SearchInfo? {
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
}
/**
* 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)
}
/**
* Add the node id to the intent, useful for auto passkey selection
*/
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
}
/**
* 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: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginNotRetrieved: (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 = getOriginFromPrivilegedAllowLists(callingAppInfo, context)
// 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
* [passkeyCreated] is called asynchronously when the passkey has been created
*/
suspend fun retrievePasskeyCreationRequestParameters(
intent: Intent,
context: Context,
passkeyCreated: (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
)
// 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
): 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 = BACKUP_ELIGIBILITY,
backupState = false, // TODO Setting to add a backup manually #2135
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: (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
): PublicKeyCredential {
val getCredentialResponse = FidoPublicKeyCredential(
id = passkey.credentialId,
response = AuthenticatorAssertionResponse(
requestOptions = requestOptions,
userPresent = true,
userVerified = true,
backupEligibility = BACKUP_ELIGIBILITY,
backupState = false, // TODO Setting to add a backup manually #2135
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)
)
}
}
private const val BACKUP_ELIGIBILITY = true
}

View File

@@ -0,0 +1,227 @@
/*
* 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.content.Context
import android.util.Log
import androidx.credentials.provider.CallingAppInfo
import com.kunzisoft.encrypt.Signature.getAllFingerprints
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import org.json.JSONObject
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
object PrivilegedAllowLists {
private const val FILE_NAME_PRIVILEGED_APPS_CUSTOM = "passkeys_privileged_apps_custom.json"
private const val FILE_NAME_PRIVILEGED_APPS_COMMUNITY = "passkeys_privileged_apps_community.json"
private const val FILE_NAME_PRIVILEGED_APPS_GOOGLE = "passkeys_privileged_apps_google.json"
private fun retrieveContentFromStream(
inputStream: InputStream,
): String {
return inputStream.use { fileInputStream ->
fileInputStream.bufferedReader(Charsets.UTF_8).readText()
}
}
/**
* Get the origin from a predefined privileged allow list
*
* @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
* @param inputStream File input stream containing the origin list as JSON
*/
private fun getOriginFromPrivilegedAllowListStream(
callingAppInfo: CallingAppInfo,
inputStream: InputStream
): String? {
val privilegedAllowList = retrieveContentFromStream(inputStream)
return callingAppInfo.getOrigin(privilegedAllowList)?.removeSuffix("/")
}
/**
* Get the origin from the predefined privileged allow lists
*
* @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
* @param context Context for file operations.
*/
fun getOriginFromPrivilegedAllowLists(
callingAppInfo: CallingAppInfo,
context: Context
): String? {
return try {
// Check the custom apps first
getOriginFromPrivilegedAllowListStream(
callingAppInfo = callingAppInfo,
File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
.inputStream()
)
} catch (e: Exception) {
// Then the Google list if allowed
if (BuildConfig.CLOSED_STORE) {
try {
// Check the Google list if allowed
// http://www.gstatic.com/gpm-passkeys-privileged-apps/apps.json
getOriginFromPrivilegedAllowListStream(
callingAppInfo = callingAppInfo,
inputStream = context.assets.open(FILE_NAME_PRIVILEGED_APPS_GOOGLE)
)
} catch (_: Exception) {
// Then the community apps list
getOriginFromPrivilegedAllowListStream(
callingAppInfo = callingAppInfo,
inputStream = context.assets.open(FILE_NAME_PRIVILEGED_APPS_COMMUNITY)
)
}
} else {
when (e) {
is FileNotFoundException -> {
val attemptApp = AndroidPrivilegedApp(
packageName = callingAppInfo.packageName,
fingerprints = callingAppInfo.signingInfo
.getAllFingerprints() ?: emptySet()
)
throw PrivilegedException(
temptingApp = attemptApp,
message = "$attemptApp is not in the allow list"
)
}
else -> throw e
}
}
}
}
/**
* Retrieves a list of predefined AndroidPrivilegedApp objects from an asset JSON file.
*
* @param inputStream File input stream containing the origin list as JSON
*/
private fun retrievePrivilegedApps(
inputStream: InputStream
): List<AndroidPrivilegedApp> {
val jsonObject = JSONObject(retrieveContentFromStream(inputStream))
return AndroidPrivilegedApp.extractPrivilegedApps(jsonObject)
}
/**
* Retrieves a list of predefined AndroidPrivilegedApp objects from a context
*
* @param context Context for file operations.
*/
fun retrievePredefinedPrivilegedApps(
context: Context
): List<AndroidPrivilegedApp> {
return try {
val predefinedApps = mutableListOf<AndroidPrivilegedApp>()
predefinedApps.addAll(retrievePrivilegedApps(context.assets.open(FILE_NAME_PRIVILEGED_APPS_COMMUNITY)))
if (BuildConfig.CLOSED_STORE) {
predefinedApps.addAll(retrievePrivilegedApps(context.assets.open(FILE_NAME_PRIVILEGED_APPS_GOOGLE)))
}
predefinedApps
} catch (e: Exception) {
Log.e(PrivilegedAllowLists::class.simpleName, "Error retrieving privileged apps", e)
emptyList()
}
}
/**
* Retrieves a list of AndroidPrivilegedApp objects from the custom JSON file.
*
* @param context Context for file operations.
*/
fun retrieveCustomPrivilegedApps(
context: Context
): List<AndroidPrivilegedApp> {
return try {
retrievePrivilegedApps(File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM).inputStream())
} catch (e: Exception) {
Log.i(PrivilegedAllowLists::class.simpleName, "No custom privileged apps", e)
emptyList()
}
}
/**
* Retrieves a list of all predefined and custom AndroidPrivilegedApp objects.
*/
fun retrieveAllPrivilegedApps(
context: Context
): List<AndroidPrivilegedApp> {
return retrievePredefinedPrivilegedApps(context) + retrieveCustomPrivilegedApps(context)
}
/**
* Saves a list of custom AndroidPrivilegedApp objects to a JSON file.
*
* @param context Context for file operations.
* @param privilegedApps The list of apps to save.
* @return True if saving was successful, false otherwise.
*/
fun saveCustomPrivilegedApps(context: Context, privilegedApps: List<AndroidPrivilegedApp>): Boolean {
return try {
val jsonToSave = AndroidPrivilegedApp.toJsonObject(privilegedApps)
val file = File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
// Delete existing file before writing to ensure atomicity if needed
if (file.exists()) {
file.delete()
}
file.outputStream().use { fileOutputStream ->
fileOutputStream.write(
jsonToSave
.toString(4) // toString(4) for pretty print
.toByteArray(Charsets.UTF_8)
)
}
true
} catch (e: Exception) {
Log.e(PrivilegedAllowLists::class.simpleName, "Error saving privileged apps", e)
false
}
}
/**
* Deletes the custom JSON file.
*
* @param context Context for file operations.
* @return True if deletion was successful or file didn't exist, false otherwise.
*/
fun deletePrivilegedAppsFile(context: Context): Boolean {
return try {
val file = File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
if (file.exists()) {
file.delete()
} else {
true // File didn't exist, so considered "successfully deleted"
}
} catch (e: SecurityException) {
Log.e(PrivilegedAllowLists::class.simpleName, "Error deleting privileged apps file", e)
false
}
}
class PrivilegedException(
val temptingApp: AndroidPrivilegedApp,
message: String
) : Exception(message)
}

View File

@@ -108,14 +108,19 @@ class DatabaseTaskProvider(
) {
// To show dialog only if context is an activity
private var activity: FragmentActivity? = try { context as? FragmentActivity? }
catch (_: Exception) { null }
private var activity: FragmentActivity? = try {
context as? FragmentActivity?
} catch (_: Exception) {
null
}
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
var onActionFinish: ((database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result) -> Unit)? = null
var onActionFinish: ((
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) -> Unit)? = null
private var intentDatabaseTask: Intent = Intent(
context.applicationContext,
@@ -141,7 +146,7 @@ class DatabaseTaskProvider(
this.databaseChangedDialogFragment = null
}
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
override fun onActionStarted(
database: ContextualDatabase,
progressMessage: ProgressMessage
@@ -175,13 +180,14 @@ class DatabaseTaskProvider(
}
}
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
override fun validateDatabaseChanged() {
mBinder?.getService()?.saveDatabaseInfo()
private val mActionDatabaseListener =
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
override fun validateDatabaseChanged() {
mBinder?.getService()?.saveDatabaseInfo()
}
}
}
private var databaseInfoListener = object:
private var databaseInfoListener = object :
DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(
previousDatabaseInfo: SnapFileDatabaseInfo,
@@ -214,7 +220,7 @@ class DatabaseTaskProvider(
}
}
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
onDatabaseRetrieved?.invoke(database)
}
@@ -265,12 +271,13 @@ class DatabaseTaskProvider(
}
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addServiceListeners(this)
getService().checkDatabase()
getService().checkDatabaseInfo()
getService().checkAction()
}
mBinder =
(serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addServiceListeners(this)
getService().checkDatabase()
getService().checkDatabaseInfo()
getService().checkAction()
}
}
override fun onServiceDisconnected(name: ComponentName?) {
@@ -296,7 +303,11 @@ class DatabaseTaskProvider(
private fun bindService() {
initServiceConnection()
serviceConnection?.let {
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
context.bindService(
intentDatabaseTask,
it,
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
)
}
}
@@ -324,6 +335,7 @@ class DatabaseTaskProvider(
// Bind to the service when is starting
bindService()
}
DATABASE_STOP_TASK_ACTION -> {
// Remove the progress task
unBindService()
@@ -331,7 +343,8 @@ class DatabaseTaskProvider(
}
}
}
ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
ContextCompat.registerReceiver(
context, databaseTaskBroadcastReceiver,
IntentFilter().apply {
addAction(DATABASE_START_TASK_ACTION)
addAction(DATABASE_STOP_TASK_ACTION)
@@ -416,47 +429,51 @@ class DatabaseTaskProvider(
----
*/
fun startDatabaseCreate(databaseUri: Uri,
mainCredential: MainCredential
fun startDatabaseCreate(
databaseUri: Uri,
mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_CREATE_TASK)
}, ACTION_DATABASE_CREATE_TASK)
}
fun startDatabaseLoad(databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean) {
fun startDatabaseLoad(
databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
putParcelable(
DatabaseTaskNotificationService.CIPHER_DATABASE_KEY,
cipherEncryptDatabase
)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
}
, ACTION_DATABASE_LOAD_TASK)
}, ACTION_DATABASE_LOAD_TASK)
}
fun startDatabaseMerge(save: Boolean,
fromDatabaseUri: Uri? = null,
mainCredential: MainCredential? = null) {
fun startDatabaseMerge(
save: Boolean,
fromDatabaseUri: Uri? = null,
mainCredential: MainCredential? = null
) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_MERGE_TASK)
}, ACTION_DATABASE_MERGE_TASK)
}
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
}
, ACTION_DATABASE_RELOAD_TASK)
}, ACTION_DATABASE_RELOAD_TASK)
}
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
@@ -472,15 +489,15 @@ class DatabaseTaskProvider(
}
}
fun startDatabaseAssignCredential(databaseUri: Uri,
mainCredential: MainCredential
fun startDatabaseAssignCredential(
databaseUri: Uri,
mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
}, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
}
/*
@@ -489,54 +506,60 @@ class DatabaseTaskProvider(
----
*/
fun startDatabaseCreateGroup(newGroup: Group,
parent: Group,
save: Boolean) {
fun startDatabaseCreateGroup(
newGroup: Group,
parent: Group,
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_CREATE_GROUP_TASK)
}, ACTION_DATABASE_CREATE_GROUP_TASK)
}
fun startDatabaseUpdateGroup(oldGroup: Group,
groupToUpdate: Group,
save: Boolean) {
fun startDatabaseUpdateGroup(
oldGroup: Group,
groupToUpdate: Group,
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_GROUP_TASK)
}, ACTION_DATABASE_UPDATE_GROUP_TASK)
}
fun startDatabaseCreateEntry(newEntry: Entry,
parent: Group,
save: Boolean) {
fun startDatabaseCreateEntry(
newEntry: Entry,
parent: Group,
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_CREATE_ENTRY_TASK)
}, ACTION_DATABASE_CREATE_ENTRY_TASK)
}
fun startDatabaseUpdateEntry(oldEntry: Entry,
entryToUpdate: Entry,
save: Boolean) {
fun startDatabaseUpdateEntry(
oldEntry: Entry,
entryToUpdate: Entry,
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
}, ACTION_DATABASE_UPDATE_ENTRY_TASK)
}
private fun startDatabaseActionListNodes(actionTask: String,
nodesPaste: List<Node>,
newParent: Group?,
save: Boolean) {
private fun startDatabaseActionListNodes(
actionTask: String,
nodesPaste: List<Node>,
newParent: Group?,
save: Boolean
) {
val groupsIdToCopy = ArrayList<NodeId<*>>()
val entriesIdToCopy = ArrayList<NodeId<UUID>>()
nodesPaste.forEach { nodeVersioned ->
@@ -544,6 +567,7 @@ class DatabaseTaskProvider(
Type.GROUP -> {
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
}
Type.ENTRY -> {
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
}
@@ -558,24 +582,29 @@ class DatabaseTaskProvider(
if (newParentId != null)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, actionTask)
}, actionTask)
}
fun startDatabaseCopyNodes(nodesToCopy: List<Node>,
newParent: Group,
save: Boolean) {
fun startDatabaseCopyNodes(
nodesToCopy: List<Node>,
newParent: Group,
save: Boolean
) {
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
}
fun startDatabaseMoveNodes(nodesToMove: List<Node>,
newParent: Group,
save: Boolean) {
fun startDatabaseMoveNodes(
nodesToMove: List<Node>,
newParent: Group,
save: Boolean
) {
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
}
fun startDatabaseDeleteNodes(nodesToDelete: List<Node>,
save: Boolean) {
fun startDatabaseDeleteNodes(
nodesToDelete: List<Node>,
save: Boolean
) {
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
}
@@ -585,26 +614,28 @@ class DatabaseTaskProvider(
-----------------
*/
fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int,
save: Boolean) {
fun startDatabaseRestoreEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int,
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
}, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
}
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int,
save: Boolean) {
fun startDatabaseDeleteEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int,
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
}, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
}
/*
@@ -613,110 +644,118 @@ class DatabaseTaskProvider(
-----------------
*/
fun startDatabaseSaveName(oldName: String,
newName: String,
save: Boolean) {
fun startDatabaseSaveName(
oldName: String,
newName: String,
save: Boolean
) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_NAME_TASK)
}, ACTION_DATABASE_UPDATE_NAME_TASK)
}
fun startDatabaseSaveDescription(oldDescription: String,
newDescription: String,
save: Boolean) {
fun startDatabaseSaveDescription(
oldDescription: String,
newDescription: String,
save: Boolean
) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
}, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
}
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
newDefaultUsername: String,
save: Boolean) {
fun startDatabaseSaveDefaultUsername(
oldDefaultUsername: String,
newDefaultUsername: String,
save: Boolean
) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
}, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
}
fun startDatabaseSaveColor(oldColor: String,
newColor: String,
save: Boolean) {
fun startDatabaseSaveColor(
oldColor: String,
newColor: String,
save: Boolean
) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_COLOR_TASK)
}, ACTION_DATABASE_UPDATE_COLOR_TASK)
}
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm,
save: Boolean) {
fun startDatabaseSaveCompression(
oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm,
save: Boolean
) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
}, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
}
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
}, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
}
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
newRecycleBin: Group?,
save: Boolean) {
fun startDatabaseSaveRecycleBin(
oldRecycleBin: Group?,
newRecycleBin: Group?,
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
}, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
}
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
newTemplatesGroup: Group?,
save: Boolean) {
fun startDatabaseSaveTemplatesGroup(
oldTemplatesGroup: Group?,
newTemplatesGroup: Group?,
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
}, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
}
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
newMaxHistoryItems: Int,
save: Boolean) {
fun startDatabaseSaveMaxHistoryItems(
oldMaxHistoryItems: Int,
newMaxHistoryItems: Int,
save: Boolean
) {
start(Bundle().apply {
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
}, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
}
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
newMaxHistorySize: Long,
save: Boolean) {
fun startDatabaseSaveMaxHistorySize(
oldMaxHistorySize: Long,
newMaxHistorySize: Long,
save: Boolean
) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
}, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
}
/*
@@ -725,59 +764,64 @@ class DatabaseTaskProvider(
-------------------
*/
fun startDatabaseSaveEncryption(oldEncryption: EncryptionAlgorithm,
newEncryption: EncryptionAlgorithm,
save: Boolean) {
fun startDatabaseSaveEncryption(
oldEncryption: EncryptionAlgorithm,
newEncryption: EncryptionAlgorithm,
save: Boolean
) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
}, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
}
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
newKeyDerivation: KdfEngine,
save: Boolean) {
fun startDatabaseSaveKeyDerivation(
oldKeyDerivation: KdfEngine,
newKeyDerivation: KdfEngine,
save: Boolean
) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
}, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
}
fun startDatabaseSaveIterations(oldIterations: Long,
newIterations: Long,
save: Boolean) {
fun startDatabaseSaveIterations(
oldIterations: Long,
newIterations: Long,
save: Boolean
) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
}, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
}
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
newMemoryUsage: Long,
save: Boolean) {
fun startDatabaseSaveMemoryUsage(
oldMemoryUsage: Long,
newMemoryUsage: Long,
save: Boolean
) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
}, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
}
fun startDatabaseSaveParallelism(oldParallelism: Long,
newParallelism: Long,
save: Boolean) {
fun startDatabaseSaveParallelism(
oldParallelism: Long,
newParallelism: Long,
save: Boolean
) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
}, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
}
/**
@@ -787,15 +831,13 @@ class DatabaseTaskProvider(
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
}
, ACTION_DATABASE_SAVE)
}, ACTION_DATABASE_SAVE)
}
fun startChallengeResponded(response: ByteArray?) {
start(Bundle().apply {
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
}
, ACTION_CHALLENGE_RESPONDED)
}, ACTION_CHALLENGE_RESPONDED)
}
companion object {

View File

@@ -24,7 +24,33 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.template.TemplateEngine
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
import com.kunzisoft.keepass.database.exception.CorruptedDatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
import com.kunzisoft.keepass.database.exception.InvalidAlgorithmDatabaseException
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
import com.kunzisoft.keepass.database.exception.KDFMemoryDatabaseException
import com.kunzisoft.keepass.database.exception.MergeDatabaseKDBException
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
import com.kunzisoft.keepass.database.exception.NoMemoryDatabaseException
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USER_HANDLE
import com.kunzisoft.keepass.model.PasskeyEntryFields.PASSKEY_FIELD
fun DatabaseException.getLocalizedMessage(resources: Resources): String? =
when (this) {
@@ -63,6 +89,11 @@ fun TemplateField.isStandardPasswordName(context: Context, name: String): Boolea
|| name == getLocalizedName(context, LABEL_PASSWORD)
}
fun TemplateField.isPasskeyLabel(context: Context, name: String): Boolean {
return name.equals(PASSKEY_FIELD, true)
|| name == getLocalizedName(context, PASSKEY_FIELD)
}
fun TemplateField.getLocalizedName(context: Context?, name: String): String {
if (context == null
|| TemplateEngine.containsTemplateDecorator(name)
@@ -107,6 +138,13 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
LABEL_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
LABEL_MEMBERSHIP.equals(name, true) -> context.getString(R.string.membership)
PASSKEY_FIELD.equals(name, true) -> context.getString(R.string.passkey)
FIELD_USERNAME.equals(name, true) -> context.getString(R.string.passkey_username)
FIELD_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.passkey_private_key)
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
else -> name
}
}

View File

@@ -43,13 +43,15 @@ object SearchHelper {
/**
* Utility method to perform actions if item is found or not after an auto search in [database]
*/
fun checkAutoSearchInfo(context: Context,
database: ContextualDatabase?,
searchInfo: SearchInfo?,
onItemsFound: (openedDatabase: ContextualDatabase,
items: List<EntryInfo>) -> Unit,
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
onDatabaseClosed: () -> Unit) {
fun checkAutoSearchInfo(
context: Context,
database: ContextualDatabase?,
searchInfo: SearchInfo?,
onItemsFound: (openedDatabase: ContextualDatabase,
items: List<EntryInfo>) -> Unit,
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
onDatabaseClosed: () -> Unit
) {
if (database == null || !database.loaded) {
onDatabaseClosed.invoke()
} else if (TimeoutHelper.checkTime(context)) {
@@ -59,8 +61,7 @@ object SearchHelper {
&& !searchInfo.containsOnlyNullValues()) {
// If search provide results
database.createVirtualGroupFromSearchInfo(
searchInfo.toString(),
searchInfo.isASearchByDomain(),
searchInfo,
MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {

View File

@@ -89,8 +89,8 @@ class PasswordActivityEducation(activity: Activity)
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(isEducationBiometricPerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_advanced_unlock_title),
activity.getString(R.string.education_advanced_unlock_summary))
activity.getString(R.string.education_device_unlock_title),
activity.getString(R.string.education_device_unlock_summary))
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_fingerprint_24dp))

View File

@@ -14,7 +14,7 @@ import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
/**
* Special activity to deal with hardware key drivers,

View File

@@ -1,144 +0,0 @@
package com.kunzisoft.keepass.hardware
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
import kotlinx.coroutines.launch
class HardwareKeyResponseHelper {
private var activity: FragmentActivity? = null
private var fragment: Fragment? = null
private var getChallengeResponseResultLauncher: ActivityResultLauncher<Intent>? = null
constructor(context: FragmentActivity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?,
extra: Bundle?) -> Unit) {
val resultCallback = ActivityResultCallback<ActivityResult> { result ->
if (result.resultCode == Activity.RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
onChallengeResponded.invoke(challengeResponse,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
} else {
Log.e(TAG, "Response from challenge error")
onChallengeResponded.invoke(null,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
}
}
getChallengeResponseResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
} else {
activity?.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
}
}
fun launchChallengeForResponse(hardwareKey: HardwareKey, seed: ByteArray?) {
when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
// Send to the driver
getChallengeResponseResultLauncher!!.launch(
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
)
Log.d(TAG, "Challenge sent")
}
}
}
companion object {
private val TAG = HardwareKeyResponseHelper::class.java.simpleName
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
private const val EXTRA_BUNDLE_KEY = "EXTRA_BUNDLE_KEY"
fun isHardwareKeyAvailable(
activity: FragmentActivity,
hardwareKey: HardwareKey,
showDialog: Boolean = true
): Boolean {
return when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
if (showDialog)
UnderDevelopmentFeatureDialogFragment()
.show(activity.supportFragmentManager, "underDevFeatureDialog")
false
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
val yubikeyDriverAvailable =
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(activity.packageManager) != null
if (showDialog && !yubikeyDriverAvailable)
showHardwareKeyDriverNeeded(activity, hardwareKey)
yubikeyDriverAvailable
}
}
}
private fun showHardwareKeyDriverNeeded(
activity: FragmentActivity,
hardwareKey: HardwareKey
) {
activity.lifecycleScope.launch {
val builder = AlertDialog.Builder(activity)
builder
.setMessage(
activity.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
activity.openExternalApp(activity.getString(R.string.key_driver_app_id))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
}
}

View File

@@ -1,13 +1,9 @@
package com.kunzisoft.keepass.receivers
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil
class DexModeReceiver : BroadcastReceiver() {

View File

@@ -274,10 +274,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
val containsPasswordToCopy = entry.password.isNotEmpty()
&& PreferencesUtil.allowCopyProtectedFields(context)
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
&& (entry.containsCustomFieldsNotProtected()
val customFields = entry.getCustomFieldsForFilling()
val containsExtraFieldToCopy = customFields.isNotEmpty()
&& (customFields.any { !it.protectedValue.isProtected }
||
(entry.containsCustomFieldsProtected() && PreferencesUtil.allowCopyProtectedFields(context))
(customFields.any { it.protectedValue.isProtected }
&& PreferencesUtil.allowCopyProtectedFields(context))
)
var startService = false
@@ -320,7 +322,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
if (containsExtraFieldToCopy) {
try {
var anonymousFieldNumber = 0
entry.customFields.forEach { field ->
entry.getCustomFieldsForFilling().forEach { field ->
//If value is not protected or allowed
if ((!field.protectedValue.isProtected
|| PreferencesUtil.allowCopyProtectedFields(context))

View File

@@ -218,7 +218,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
Log.i(TAG, "Database file modified " +
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
// Call listener to indicate a change in database info
if (!mSaveState && previousDatabaseInfo != null) {
if (!mSaveState) {
mDatabaseInfoListeners.forEach { listener ->
listener.onDatabaseInfoChanged(
previousDatabaseInfo,

View File

@@ -15,13 +15,13 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
class AdvancedUnlockNotificationService : NotificationService() {
class DeviceUnlockNotificationService : NotificationService() {
private lateinit var mTempCipherDao: ArrayList<CipherDatabaseEntity>
private var mActionTaskBinder = AdvancedUnlockBinder()
private var mActionTaskBinder = DeviceUnlockBinder()
inner class AdvancedUnlockBinder: Binder() {
inner class DeviceUnlockBinder: Binder() {
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
}
@@ -48,11 +48,11 @@ class AdvancedUnlockNotificationService : NotificationService() {
override val notificationId: Int = 593
override fun retrieveChannelId(): String {
return CHANNEL_ADVANCED_UNLOCK_ID
return CHANNEL_DEVICE_UNLOCK_ID
}
override fun retrieveChannelName(): String {
return getString(R.string.advanced_unlock)
return getString(R.string.device_unlock)
}
override fun onCreate() {
@@ -60,7 +60,7 @@ class AdvancedUnlockNotificationService : NotificationService() {
mTempCipherDao = ArrayList()
}
// It's simpler to use pendingIntent to perform REMOVE_ADVANCED_UNLOCK_KEY_ACTION
// It's simpler to use pendingIntent to perform REMOVE_DEVICE_UNLOCK_KEY_ACTION
// because can be directly broadcast to another module or app
@SuppressLint("LaunchActivityFromNotification")
override fun onBind(intent: Intent): IBinder {
@@ -68,7 +68,7 @@ class AdvancedUnlockNotificationService : NotificationService() {
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
4577,
Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION),
Intent(REMOVE_DEVICE_UNLOCK_KEY_ACTION),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
@@ -81,28 +81,28 @@ class AdvancedUnlockNotificationService : NotificationService() {
} else {
R.drawable.notification_ic_device_unlock_24dp
})
setContentTitle(getString(R.string.advanced_unlock))
setContentText(getString(R.string.advanced_unlock_tap_delete))
setContentTitle(getString(R.string.device_unlock))
setContentText(getString(R.string.device_unlock_tap_delete))
setContentIntent(pendingDeleteIntent)
// Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent)
}
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
val notificationTimeoutMilliSecs = PreferencesUtil.getDeviceUnlockTimeout(this)
// Not necessarily a foreground service
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
defineTimerJob(
notificationBuilder,
NotificationServiceType.ADVANCED_UNLOCK,
NotificationServiceType.DEVICE_UNLOCK,
notificationTimeoutMilliSecs
) {
sendBroadcast(Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION))
sendBroadcast(Intent(REMOVE_DEVICE_UNLOCK_KEY_ACTION))
}
} else {
startForegroundCompat(
notificationId,
notificationBuilder,
NotificationServiceType.ADVANCED_UNLOCK
NotificationServiceType.DEVICE_UNLOCK
)
}
@@ -119,11 +119,11 @@ class AdvancedUnlockNotificationService : NotificationService() {
super.onDestroy()
}
class AdvancedUnlockReceiver(var removeKeyAction: () -> Unit): BroadcastReceiver() {
class DeviceUnlockReceiver(var removeKeyAction: () -> Unit): BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
intent.action?.let {
when (it) {
REMOVE_ADVANCED_UNLOCK_KEY_ACTION -> {
REMOVE_DEVICE_UNLOCK_KEY_ACTION -> {
removeKeyAction.invoke()
}
}
@@ -132,13 +132,13 @@ class AdvancedUnlockNotificationService : NotificationService() {
}
companion object {
private const val CHANNEL_ADVANCED_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
const val REMOVE_ADVANCED_UNLOCK_KEY_ACTION = "com.kunzisoft.keepass.REMOVE_ADVANCED_UNLOCK_KEY"
private const val CHANNEL_DEVICE_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
const val REMOVE_DEVICE_UNLOCK_KEY_ACTION = "com.kunzisoft.keepass.REMOVE_DEVICE_UNLOCK_KEY"
// Only one service connection
fun bindService(context: Context, serviceConnection: ServiceConnection, flags: Int) {
context.bindService(Intent(context,
AdvancedUnlockNotificationService::class.java),
DeviceUnlockNotificationService::class.java),
serviceConnection,
flags)
}

View File

@@ -27,7 +27,7 @@ import android.util.Log
import android.widget.Toast
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper

View File

@@ -12,6 +12,7 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.util.TypedValue
import android.widget.Toast
import androidx.core.app.NotificationCompat
@@ -105,7 +106,7 @@ abstract class NotificationService : Service() {
NotificationServiceType.ATTACHMENT -> FOREGROUND_SERVICE_TYPE_DATA_SYNC
NotificationServiceType.CLIPBOARD -> foregroundServiceTimer
NotificationServiceType.KEYBOARD -> foregroundServiceTimer
NotificationServiceType.ADVANCED_UNLOCK -> foregroundServiceTimer
NotificationServiceType.DEVICE_UNLOCK -> foregroundServiceTimer
}
startForeground(notificationId, builder.build(), foregroundType)
} else {
@@ -156,11 +157,21 @@ abstract class NotificationService : Service() {
mReset = true
}
override fun onDestroy() {
override fun onTimeout(startId: Int, fgsType: Int) {
super.onTimeout(startId, fgsType)
Log.e(javaClass::class.simpleName, "The service took too long to execute")
cancelNotification()
stopSelf()
}
protected fun cancelNotification() {
mTimerJob?.cancel()
mTimerJob = null
notificationManager?.cancel(notificationId)
}
override fun onDestroy() {
cancelNotification()
super.onDestroy()
}

View File

@@ -5,5 +5,5 @@ enum class NotificationServiceType {
ATTACHMENT,
CLIPBOARD,
KEYBOARD,
ADVANCED_UNLOCK
DEVICE_UNLOCK
}

View File

@@ -19,9 +19,12 @@
*/
package com.kunzisoft.keepass.settings
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R
@RequiresApi(Build.VERSION_CODES.O)
class AutofillSettingsActivity : ExternalSettingsActivity() {
override fun retrieveTitle(): Int {

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.settings
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@@ -29,6 +30,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
@RequiresApi(Build.VERSION_CODES.O)
class AutofillSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -42,8 +44,6 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
}
override fun onDisplayPreferenceDialog(preference: Preference) {
var otherDialogFragment = false
var dialogFragment: DialogFragment? = null
when (preference.key) {
@@ -53,7 +53,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
getString(R.string.autofill_web_domain_blocklist_key) -> {
dialogFragment = AutofillBlocklistWebDomainPreferenceDialogFragmentCompat.newInstance(preference.key)
}
else -> otherDialogFragment = true
else -> {}
}
if (dialogFragment != null) {
@@ -62,7 +62,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT)
}
// Could not be handled here. Try with the super method.
else if (otherDialogFragment) {
else {
super.onDisplayPreferenceDialog(preference)
}
}

View File

@@ -23,15 +23,15 @@ import android.os.Bundle
import androidx.fragment.app.Fragment
class AdvancedUnlockSettingsActivity : SettingsActivity() {
class DeviceUnlockSettingsActivity : SettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mTimeoutEnable = false
setTitle(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
setTitle(NestedSettingsFragment.Screen.DEVICE_UNLOCK)
}
override fun retrieveMainFragment(): Fragment {
return NestedSettingsFragment.newInstance(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
return NestedSettingsFragment.newInstance(NestedSettingsFragment.Screen.DEVICE_UNLOCK)
}
}

View File

@@ -84,9 +84,9 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
}
}
findPreference<Preference>(getString(R.string.settings_advanced_unlock_key))?.apply {
findPreference<Preference>(getString(R.string.settings_device_unlock_key))?.apply {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DEVICE_UNLOCK)
false
}
}

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.settings
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
@@ -30,6 +29,7 @@ import android.view.autofill.AutofillManager
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import androidx.preference.ListPreference
@@ -41,13 +41,13 @@ import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
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.UriUtil.releaseAllUnnecessaryPermissionUris
@@ -66,8 +66,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
Screen.FORM_FILLING -> {
onCreateFormFillingPreference(rootKey)
}
Screen.ADVANCED_UNLOCK -> {
onCreateAdvancedUnlockPreferences(rootKey)
Screen.DEVICE_UNLOCK -> {
onCreateDeviceUnlockPreferences(rootKey)
}
Screen.APPEARANCE -> {
onCreateAppearancePreferences(rootKey)
@@ -119,7 +119,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val autoFillEnablePreference: TwoStatePreference? = findPreference(getString(R.string.settings_autofill_enable_key))
// Hide Passkeys settings if needed
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
findPreference<Preference>(getString(R.string.passkeys_explanation_key))
?.isVisible = false
findPreference<Preference>(getString(R.string.settings_passkeys_key))
?.isVisible = false
}
val autoFillEnablePreference: TwoStatePreference? = findPreference(getString(R.string.settings_credential_provider_enable_key))
activity.getSystemService(AutofillManager::class.java)?.let { autofillManager ->
if (autofillManager.hasEnabledAutofillServices())
autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices()
@@ -161,7 +170,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val intent =
Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
intent.data =
Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService")
"package:com.kunzisoft.keepass.autofill.KeeAutofillService".toUri()
Log.d(javaClass.name, "Autofill enable service: intent=$intent")
startActivity(intent)
} else {
@@ -171,7 +180,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
} else {
findPreference<Preference>(getString(R.string.autofill_key))?.isVisible = false
findPreference<Preference>(getString(R.string.credential_provider_key))?.isVisible = false
}
}
@@ -192,14 +201,28 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
false
}
findPreference<Preference>(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener {
context?.openUrl(R.string.autofill_explanation_url)
false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
findPreference<Preference>(getString(R.string.passkeys_explanation_key))?.setOnPreferenceClickListener {
context?.openUrl(R.string.passkeys_explanation_url)
false
}
findPreference<Preference>(getString(R.string.settings_passkeys_key))?.setOnPreferenceClickListener {
startActivity(Intent(context, PasskeysSettingsActivity::class.java))
false
}
}
findPreference<Preference>(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener {
startActivity(Intent(context, AutofillSettingsActivity::class.java))
false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
findPreference<Preference>(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener {
context?.openUrl(R.string.autofill_explanation_url)
false
}
findPreference<Preference>(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener {
startActivity(Intent(context, AutofillSettingsActivity::class.java))
false
}
}
findPreference<Preference>(getString(R.string.clipboard_notifications_key))?.setOnPreferenceChangeListener { _, newValue ->
@@ -240,18 +263,18 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
private fun onCreateAdvancedUnlockPreferences(rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_advanced_unlock, rootKey)
private fun onCreateDeviceUnlockPreferences(rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_device_unlock, rootKey)
activity?.let { activity ->
val biometricUnlockEnablePreference: TwoStatePreference? = findPreference(getString(R.string.biometric_unlock_enable_key))
val deviceCredentialUnlockEnablePreference: TwoStatePreference? = findPreference(getString(R.string.device_credential_unlock_enable_key))
val autoOpenPromptPreference: TwoStatePreference? = findPreference(getString(R.string.biometric_auto_open_prompt_key))
val tempAdvancedUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
val tempDeviceUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_device_unlock_enable_key))
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AdvancedUnlockManager.biometricUnlockSupported(activity)
DeviceUnlockManager.biometricUnlockSupported(activity)
} else false
biometricUnlockEnablePreference?.apply {
// False if under Marshmallow
@@ -272,7 +295,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
biometricUnlockEnablePreference.isChecked = false
autoOpenPromptPreference?.isEnabled = deviceCredentialChecked
tempAdvancedUnlockPreference?.isEnabled = deviceCredentialChecked
tempDeviceUnlockPreference?.isEnabled = deviceCredentialChecked
}
} else {
if (deviceCredentialChecked) {
@@ -286,7 +309,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
biometricUnlockEnablePreference.isChecked = true
autoOpenPromptPreference?.isEnabled = true
tempAdvancedUnlockPreference?.isEnabled = true
tempDeviceUnlockPreference?.isEnabled = true
}
}
}
@@ -296,7 +319,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AdvancedUnlockManager.deviceCredentialUnlockSupported(activity)
DeviceUnlockManager.deviceCredentialUnlockSupported(activity)
} else false
deviceCredentialUnlockEnablePreference?.apply {
// Biometric unlock already checked
@@ -319,7 +342,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
deviceCredentialUnlockEnablePreference.isChecked = false
autoOpenPromptPreference?.isEnabled = biometricChecked
tempAdvancedUnlockPreference?.isEnabled = biometricChecked
tempDeviceUnlockPreference?.isEnabled = biometricChecked
}
} else {
if (biometricChecked) {
@@ -333,7 +356,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
deviceCredentialUnlockEnablePreference.isChecked = true
autoOpenPromptPreference?.isEnabled = true
tempAdvancedUnlockPreference?.isEnabled = true
tempDeviceUnlockPreference?.isEnabled = true
}
}
}
@@ -344,13 +367,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
autoOpenPromptPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|| deviceCredentialUnlockEnablePreference?.isChecked == true
tempAdvancedUnlockPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
tempDeviceUnlockPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|| deviceCredentialUnlockEnablePreference?.isChecked == true
tempAdvancedUnlockPreference?.setOnPreferenceClickListener {
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
tempDeviceUnlockPreference?.setOnPreferenceClickListener {
tempDeviceUnlockPreference.isChecked = !tempDeviceUnlockPreference.isChecked
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
tempDeviceUnlockPreference.isChecked = !tempDeviceUnlockPreference.isChecked
}
true
}
@@ -366,8 +389,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
findPreference<Preference>(getString(R.string.advanced_unlock_explanation_key))?.setOnPreferenceClickListener {
context?.openUrl(R.string.advanced_unlock_explanation_url)
findPreference<Preference>(getString(R.string.device_unlock_explanation_key))?.setOnPreferenceClickListener {
context?.openUrl(R.string.device_unlock_explanation_url)
false
}
}
@@ -378,14 +401,14 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
validate: (()->Unit)? = null) {
var message = ""
if (keystoreWarning) {
message += resources.getString(R.string.advanced_unlock_prompt_store_credential_message)
message += "\n\n" + resources.getString(R.string.advanced_unlock_keystore_warning)
message += resources.getString(R.string.device_unlock_prompt_store_credential_message)
message += "\n\n" + resources.getString(R.string.device_unlock_keystore_warning)
}
if (keystoreWarning && deleteKeys) {
message += "\n\n"
}
if (deleteKeys) {
message += resources.getString(R.string.advanced_unlock_delete_all_key_warning)
message += resources.getString(R.string.device_unlock_delete_all_key_warning)
}
warningAlertDialog = AlertDialog.Builder(activity)
.setMessage(message)
@@ -395,7 +418,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
validate?.invoke()
warningAlertDialog?.setOnDismissListener(null)
if (deleteKeys && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
DeviceUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
}
}
.setNegativeButton(resources.getString(android.R.string.cancel)
@@ -509,7 +532,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
when (preference.key) {
getString(R.string.app_timeout_key),
getString(R.string.clipboard_timeout_key),
getString(R.string.temp_advanced_unlock_timeout_key) -> {
getString(R.string.temp_device_unlock_timeout_key) -> {
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
}
else -> otherDialogFragment = true
@@ -530,7 +553,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
super.onResume()
activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
findPreference<TwoStatePreference?>(getString(R.string.settings_autofill_enable_key))?.let { autoFillEnablePreference ->
findPreference<TwoStatePreference?>(getString(R.string.settings_credential_provider_enable_key))?.let { autoFillEnablePreference ->
val autofillManager = activity.getSystemService(AutofillManager::class.java)
autoFillEnablePreference.isChecked = autofillManager != null
&& autofillManager.hasEnabledAutofillServices()

View File

@@ -30,7 +30,7 @@ import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFra
abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
enum class Screen {
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
APPLICATION, FORM_FILLING, DEVICE_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
}
fun getScreen(): Screen {
@@ -66,7 +66,7 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
val fragment: NestedSettingsFragment = when (key) {
Screen.APPLICATION,
Screen.FORM_FILLING,
Screen.ADVANCED_UNLOCK,
Screen.DEVICE_UNLOCK,
Screen.APPEARANCE -> NestedAppSettingsFragment()
Screen.DATABASE,
Screen.DATABASE_SECURITY,
@@ -83,7 +83,7 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
return when (key) {
Screen.APPLICATION -> resources.getString(R.string.menu_app_settings)
Screen.FORM_FILLING -> resources.getString(R.string.menu_form_filling_settings)
Screen.ADVANCED_UNLOCK -> resources.getString(R.string.menu_advanced_unlock_settings)
Screen.DEVICE_UNLOCK -> resources.getString(R.string.menu_device_unlock_settings)
Screen.APPEARANCE -> resources.getString(R.string.menu_appearance_settings)
Screen.DATABASE -> resources.getString(R.string.menu_database_settings)
Screen.DATABASE_SECURITY -> resources.getString(R.string.menu_security_settings)

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.settings
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class PasskeysSettingsActivity : ExternalSettingsActivity() {
override fun retrieveTitle(): Int {
return R.string.passkeys_preference_title
}
override fun retrievePreferenceFragment(): PreferenceFragmentCompat {
return PasskeysSettingsFragment()
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.settings
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.preferencedialogfragment.PasskeysPrivilegedAppsPreferenceDialogFragmentCompat
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class PasskeysSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// Load the preferences from an XML resource
setPreferencesFromResource(R.xml.preferences_passkeys, rootKey)
}
@Suppress("DEPRECATION")
override fun onDisplayPreferenceDialog(preference: Preference) {
var dialogFragment: DialogFragment? = null
when (preference.key) {
getString(R.string.passkeys_privileged_apps_key) -> {
dialogFragment = PasskeysPrivilegedAppsPreferenceDialogFragmentCompat.newInstance(preference.key)
}
else -> {}
}
if (dialogFragment != null) {
dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PASSKEYS_PREF_FRAGMENT)
} else {
super.onDisplayPreferenceDialog(preference)
}
}
companion object {
private const val TAG_PASSKEYS_PREF_FRAGMENT = "TAG_PASSKEYS_PREF_FRAGMENT"
}
}

View File

@@ -29,14 +29,14 @@ import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
import java.util.Properties
object PreferencesUtil {
@@ -460,10 +460,10 @@ object PreferencesUtil {
?: TimeoutHelper.DEFAULT_TIMEOUT
}
fun getAdvancedUnlockTimeout(context: Context): Long {
fun getDeviceUnlockTimeout(context: Context): Long {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.temp_advanced_unlock_timeout_key),
context.getString(R.string.temp_advanced_unlock_timeout_default))?.toLong()
return prefs.getString(context.getString(R.string.temp_device_unlock_timeout_key),
context.getString(R.string.temp_device_unlock_timeout_default))?.toLong()
?: TimeoutHelper.DEFAULT_TIMEOUT
}
@@ -503,7 +503,7 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.enable_screenshot_mode_key_default))
}
fun isAdvancedUnlockEnable(context: Context): Boolean {
fun isDeviceUnlockEnable(context: Context): Boolean {
return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context)
}
@@ -512,7 +512,7 @@ object PreferencesUtil {
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
&& (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
AdvancedUnlockManager.biometricUnlockSupported(context)
DeviceUnlockManager.biometricUnlockSupported(context)
} else {
false
})
@@ -526,13 +526,13 @@ object PreferencesUtil {
&& !isBiometricUnlockEnable(context)
}
fun isTempAdvancedUnlockEnable(context: Context): Boolean {
fun isTempDeviceUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.temp_advanced_unlock_enable_key),
context.resources.getBoolean(R.bool.temp_advanced_unlock_enable_default))
return prefs.getBoolean(context.getString(R.string.temp_device_unlock_enable_key),
context.resources.getBoolean(R.bool.temp_device_unlock_enable_default))
}
fun isAdvancedUnlockPromptAutoOpenEnable(context: Context): Boolean {
fun isDeviceUnlockPromptAutoOpenEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.biometric_auto_open_prompt_key),
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
@@ -618,12 +618,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.allow_no_password_default))
}
fun enableReadOnlyDatabase(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_read_only_key),
context.resources.getBoolean(R.bool.enable_read_only_default))
}
fun deletePasswordAfterConnexionAttempt(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.delete_entered_password_key),
@@ -804,7 +798,6 @@ object PreferencesUtil {
when (name) {
context.getString(R.string.allow_no_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.delete_entered_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_read_only_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_auto_save_database_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_keep_screen_on_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.auto_focus_search_key) -> editor.putBoolean(name, value.toBoolean())
@@ -821,14 +814,14 @@ object PreferencesUtil {
context.getString(R.string.biometric_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.device_credential_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.biometric_auto_open_prompt_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.temp_advanced_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.temp_advanced_unlock_timeout_key) -> editor.putString(name, value.toLong().toString())
context.getString(R.string.temp_device_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.temp_device_unlock_timeout_key) -> editor.putString(name, value.toLong().toString())
context.getString(R.string.magic_keyboard_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.clipboard_notifications_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.clear_clipboard_notification_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.clipboard_timeout_key) -> editor.putString(name, value.toLong().toString())
context.getString(R.string.settings_autofill_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.settings_credential_provider_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_notification_entry_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString())

View File

@@ -52,6 +52,7 @@ class DurationDialogPreference @JvmOverloads constructor(context: Context,
notifyChanged()
}
@Deprecated(message = "")
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
if (restorePersistedValue) {
mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration

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