Compare commits

...

232 Commits

Author SHA1 Message Date
J-Jamet
e6b33d60c3 Merge branch 'release/2.9.12' 2021-01-23 13:44:33 +01:00
J-Jamet
3fd06890d7 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-01-23 13:22:56 +01:00
J-Jamet
4af4ad7663 Fix show UUID key 2021-01-23 13:18:03 +01:00
J-Jamet
6ca8501e28 Keep current screen after theme change 2021-01-23 13:13:01 +01:00
J-Jamet
432b385f60 Fix orientation change in settings #872 2021-01-23 12:56:47 +01:00
Hosted Weblate
6cebdefa4a Merge branch 'origin/develop' into Weblate. 2021-01-23 12:48:39 +01:00
J-Jamet
bc665eb83d Fix orientation change in settings #872 2021-01-23 12:16:18 +01:00
J-Jamet
cb187300fe Deactivate GiB #851 2021-01-23 11:21:38 +01:00
J-Jamet
f34e007ecd Add kibibyte and gibibyte #851 2021-01-22 16:07:50 +01:00
J-Jamet
3b6ad080b4 Update CHANGELOG 2021-01-22 15:46:30 +01:00
J-Jamet
9919e90ba5 Change memory unit to MiB 2021-01-22 15:37:50 +01:00
WaldiS
f4af44925b Translated using Weblate (Polish)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-01-21 21:45:29 +01:00
J-Jamet
4bb366b568 Capture exception in IO action task 2021-01-21 14:38:34 +01:00
J-Jamet
7e7ab4ce19 Catch exception when check database info 2021-01-21 14:32:11 +01:00
J-Jamet
4d833d25ce Add IME_FLAG_NO_PERSONALIZED_LEARNING options #642 2021-01-21 14:16:13 +01:00
J-Jamet
a9c508ecd9 Fix back appearance setting #865 2021-01-21 13:36:50 +01:00
J-Jamet
ef4dbb8fdb Fix auto open biometric prompt #862 2021-01-21 12:46:43 +01:00
J-Jamet
3fd13f3e3b Try to fix decodeHex method conflict in some devices 2021-01-20 11:42:09 +01:00
Kornelijus Tvarijanavičius
319c9cad4b Translated using Weblate (Lithuanian)
Currently translated at 15.1% (77 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2021-01-20 11:32:13 +01:00
nautilusx
c12297c98d Translated using Weblate (German)
Currently translated at 98.8% (501 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-01-20 11:32:13 +01:00
J-Jamet
7c38361844 Fix OTP token type #863 2021-01-18 16:48:57 +01:00
J-Jamet
559554a975 Upgrade to 2.9.12 2021-01-18 16:47:48 +01:00
Darin Avdeyeva
7e2ffa2124 Translated using Weblate (Russian)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-01-17 19:40:00 +01:00
Milo Ivir
66dbac4bb2 Translated using Weblate (Croatian)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-01-17 15:28:44 +01:00
Carlos Pinto
8b6a843a85 Translated using Weblate (Portuguese (Portugal))
Currently translated at 90.7% (460 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2021-01-17 15:28:43 +01:00
HARADA Hiroyuki
976cff2751 Translated using Weblate (Japanese)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-01-17 15:28:43 +01:00
zeritti
f7c30fa8eb Translated using Weblate (Czech)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-01-17 15:28:42 +01:00
J-Jamet
7757c8218b Merge tag '2.9.11' into develop
2.9.11
2021-01-16 19:09:33 +01:00
J-Jamet
2928b7daa3 Merge branch 'release/2.9.11' 2021-01-16 19:09:22 +01:00
J-Jamet
3a55dea276 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2021-01-16 18:54:47 +01:00
J-Jamet
2a25213d66 Update CHANGELOG 2021-01-16 18:49:11 +01:00
J-Jamet
035ffd8135 Fix hexadecimal keyfile #861 2021-01-16 18:46:17 +01:00
J-Jamet
b040487f1f Use decodeHex method 2021-01-15 18:16:46 +01:00
J-Jamet
6fc821aecf Fix keyx file version 2 #844 2021-01-15 17:00:29 +01:00
J-Jamet
cdceb1fb6f Fix keyx file version 2 #844 2021-01-15 16:57:29 +01:00
J-Jamet
07d185913d Upgrade to 2.9.11 2021-01-15 16:35:25 +01:00
Eric
f2a245a9c8 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-01-15 14:19:14 +01:00
Oliver Cervera
33338f4759 Translated using Weblate (Italian)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-01-15 14:19:13 +01:00
J-Jamet
f7a4370b29 Merge tag '2.9.10' into develop
2.9.10
2021-01-15 01:53:07 +01:00
J-Jamet
77b7afedda Merge branch 'release/2.9.10' 2021-01-15 01:52:59 +01:00
J-Jamet
caa13039e5 Update CHANGELOG 2021-01-15 01:52:32 +01:00
J-Jamet
02845d93ed Change order keyfile recognition 2021-01-15 01:44:40 +01:00
J-Jamet
9ef4695cc7 Change order keyfile recognition 2021-01-15 01:00:13 +01:00
Oğuz Ersen
d619e089c0 Translated using Weblate (Turkish)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-01-14 23:11:57 +01:00
Ihor Hordiichuk
3c50348a79 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-01-14 23:11:56 +01:00
solokot
167ea3b82b Translated using Weblate (Russian)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-01-14 23:11:56 +01:00
WaldiS
9eda3e62f7 Translated using Weblate (Polish)
Currently translated at 99.0% (502 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-01-14 23:11:56 +01:00
Kunzisoft
99c4319b51 Translated using Weblate (French)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-01-14 23:11:55 +01:00
Retrial
790b25db65 Translated using Weblate (Greek)
Currently translated at 100.0% (507 of 507 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-01-14 23:11:55 +01:00
J-Jamet
97d4972f9a Try to fix crash with autofill #852 2021-01-14 21:42:40 +01:00
J-Jamet
8e6853756f Upgrade to version 2.9.10 2021-01-14 14:58:07 +01:00
J-Jamet
6d3aae187b Merge tag '2.9.9' into develop
2.9.9
2021-01-14 14:46:53 +01:00
J-Jamet
b8c7acf7ce Merge branch 'release/2.9.9' 2021-01-14 14:46:46 +01:00
J-Jamet
17a356ae76 Replace strong tag 2021-01-14 13:50:41 +01:00
J-Jamet
bd847e632d Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-01-14 13:46:56 +01:00
J-Jamet
2bfb9b048d Better entry visualisation 2021-01-14 13:23:35 +01:00
J-Jamet
dc40b50b65 Parse new TOTP fields from KeePass 2.47 #850 2021-01-14 11:48:44 +01:00
J-Jamet
3e2271e596 Update CHANGELOG 2021-01-13 16:48:36 +01:00
J-Jamet
4b4fd2a11d Fix OTP generation for long secret key #848 2021-01-13 16:46:19 +01:00
J-Jamet
23468290df Fix small visual element 2021-01-12 19:43:16 +01:00
J-Jamet
a276f6aa06 Remove keyfile icon 2021-01-12 19:11:23 +01:00
J-Jamet
f2a58361a1 Update CHANGELOG 2021-01-12 18:09:40 +01:00
J-Jamet
271023b528 Fix Toggling custom field protection #849 2021-01-12 18:08:05 +01:00
J-Jamet
e1771ca249 Upgrade CHANGELOG 2021-01-12 15:11:09 +01:00
J-Jamet
ca4f4bd151 Add priority to OTP button in notification #845 2021-01-12 15:08:59 +01:00
J-Jamet
d81454d618 FEATURE_SECURE_PROCESSING Error to Warning log 2021-01-12 13:13:08 +01:00
J-Jamet
fb43c1c624 Special search in title fields #830 2021-01-12 09:28:05 +01:00
J-Jamet
9e060f878d Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2021-01-12 09:24:09 +01:00
Milo Ivir
bd9c21ee8a Translated using Weblate (Croatian)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-01-12 00:32:09 +01:00
J-Jamet
3e6d40e8da Better autofill suggestion toast 2021-01-11 21:33:25 +01:00
J-Jamet
79683cb3fc Encapsulate autofillInlineSuggestionsEnabled preference 2021-01-11 21:17:34 +01:00
J-Jamet
52a2090a31 Upgrade CHANGELOG 2021-01-11 20:53:52 +01:00
J-Jamet
3dfe4ace7b Fix binary keyfiles of 64 bytes #835 2021-01-11 20:42:14 +01:00
J-Jamet
bd0d17b134 Remove unused log 2021-01-11 17:58:22 +01:00
J-Jamet
6b0ccc1780 Update CHANGELOG 2021-01-11 15:02:18 +01:00
J-Jamet
d75ac4b825 Remove unused log 2021-01-11 14:55:37 +01:00
J-Jamet
b60d610d02 Remove loadXmlKeyFile method in KDB database 2021-01-11 14:30:53 +01:00
J-Jamet
f7a5c5d0ea Fix hash in keyfile XML version 1 2021-01-11 14:17:43 +01:00
J-Jamet
28f79aec11 Check keyfile XML hash 2021-01-11 14:11:30 +01:00
J-Jamet
778d963fbf Encapsulate String util and fix Key File recognition #844 2021-01-11 11:31:22 +01:00
J-Jamet
a765bc84e7 Upgrade buildToolsVersion to 30.0.3 2021-01-11 11:29:31 +01:00
J-Jamet
804ecc1baa Merge branch 'feature/Autofill_Inline' into develop #827 2021-01-09 16:01:04 +01:00
J-Jamet
d331c3dc03 Fix title in autofill activity 2021-01-09 16:00:44 +01:00
J-Jamet
7010d2f86a Refresh preferences during connection 2021-01-09 15:50:50 +01:00
J-Jamet
b1d6117eb2 Fix autofill longpress 2021-01-09 15:44:47 +01:00
J-Jamet
f3b814388d Add toast to inform the user of inline suggestions in the keyboard 2021-01-09 15:02:29 +01:00
J-Jamet
b62996a57c Setting to allow or not inline suggestions 2021-01-09 14:29:59 +01:00
J-Jamet
a49e056f02 Autofill component to select entry with inline response 2021-01-09 14:02:01 +01:00
J-Jamet
a6dece16bf Merge branch 'develop' into feature/Autofill_Inline 2021-01-09 12:33:26 +01:00
J-Jamet
8e3ddd64d2 Better exception catching #794 2021-01-09 12:17:43 +01:00
J-Jamet
45a847fa3e Remove unused code 2021-01-09 11:26:19 +01:00
J-Jamet
6b6f03b143 Remove small warning 2021-01-09 11:20:41 +01:00
J-Jamet
5446efca4a Output header refactor 2021-01-09 11:09:31 +01:00
J-Jamet
8d04a7f90b Merge branch 'develop' into feature/Autofill_Inline 2021-01-08 16:36:34 +01:00
J-Jamet
626495c19e Upgrade CHANGELOG 2021-01-07 23:35:06 +01:00
J-Jamet
e5a198f524 Remove unused variable 2021-01-07 23:22:30 +01:00
J-Jamet
161524843f Merge branch 'feature/Detect_File_Changes' into develop #794 2021-01-07 22:50:05 +01:00
J-Jamet
5550e7dea3 Remove unused duplicateUUID exception during reloading 2021-01-07 22:44:46 +01:00
J-Jamet
64f66c290c Prevent reloading from special mode 2021-01-07 22:34:11 +01:00
J-Jamet
e8925b3c0b Fix exception 2021-01-07 22:12:02 +01:00
J-Jamet
cf67ce04a8 Better animation and reload setting screen 2021-01-07 22:03:17 +01:00
J-Jamet
84ee4ca2c7 Change dialog 2021-01-07 21:07:32 +01:00
J-Jamet
27eb095720 Add database reloading #794 2021-01-07 16:25:05 +01:00
J-Jamet
d273f21819 Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2021-01-05 15:50:34 +01:00
J-Jamet
455fd0cd6d Remove unused log 2021-01-05 13:20:12 +01:00
J-Jamet
c5a8650c81 Show a dialog when a database file info changes #794 2021-01-05 12:48:06 +01:00
J-Jamet
b5f9bbed5e Detect Database File Info Changes #794 2021-01-04 19:26:20 +01:00
George
e789741090 Translated using Weblate (Bulgarian)
Currently translated at 2.2% (11 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bg/
2021-01-04 03:29:34 +01:00
George
5c6d93bc57 Added translation using Weblate (Bulgarian) 2021-01-03 02:39:46 +01:00
WaldiS
697b672038 Translated using Weblate (Polish)
Currently translated at 98.8% (494 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-01-02 22:49:58 +01:00
Y. Sakamoto
2d9e9c24a8 Translated using Weblate (Japanese)
Currently translated at 99.8% (499 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-01-02 22:49:58 +01:00
J-Jamet
5fb281c800 Upgrade to 2.9.9 2021-01-02 19:13:08 +01:00
J-Jamet
96896c1c42 Merge tag '2.9.8' into develop
2.9.8
2021-01-02 18:28:48 +01:00
J-Jamet
d7052bd9e6 Merge branch 'release/2.9.8' 2021-01-02 18:28:26 +01:00
J-Jamet
8b23932788 Fix IllegalStateException 2021-01-02 18:28:03 +01:00
J-Jamet
50912c6966 Update CHANGELOG 2021-01-02 18:25:45 +01:00
J-Jamet
53b51934b9 Upgrade kotlin version 2021-01-02 18:14:15 +01:00
J-Jamet
a8a3685965 Upgrade Room to 2.2.6 2021-01-02 13:50:38 +01:00
J-Jamet
149b67e28b Better exception StreamCipherFactory.getInstance management 2021-01-02 11:04:45 +01:00
J-Jamet
a83032bffa Fix binary in a single entry #828 2021-01-02 09:44:03 +01:00
Oğuz Ersen
a5d3a153bf Translated using Weblate (Turkish)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-12-31 13:30:01 +01:00
Eric
4210c155eb Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-12-31 13:30:01 +01:00
Ihor Hordiichuk
4bf110a9b1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-12-31 13:30:00 +01:00
solokot
50f2684500 Translated using Weblate (Russian)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-31 13:30:00 +01:00
J. Lavoie
e95424b8f9 Translated using Weblate (Italian)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-31 13:30:00 +01:00
J. Lavoie
8462882707 Translated using Weblate (French)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-12-31 13:30:00 +01:00
Óscar Fernández Díaz
a5c8d25f64 Translated using Weblate (Spanish)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-31 13:29:59 +01:00
Retrial
689ce2f9b3 Translated using Weblate (Greek)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-12-31 13:29:59 +01:00
J. Lavoie
54246533ac Translated using Weblate (German)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-31 13:29:59 +01:00
zeritti
66e4b0fe47 Translated using Weblate (Czech)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-12-31 13:29:16 +01:00
J-Jamet
3e8ae3e2e3 Upgrade KeePassDX Pro description 2020-12-30 17:23:17 +01:00
J-Jamet
d856ef3772 Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2020-12-30 17:20:05 +01:00
x
5727880ac7 Translated using Weblate (Italian)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-30 03:21:37 +01:00
Oliver Cervera
ec4302a780 Translated using Weblate (Italian)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-30 03:20:48 +01:00
x
d4203598a1 Translated using Weblate (Italian)
Currently translated at 100.0% (500 of 500 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-30 03:20:46 +01:00
Hosted Weblate
a278c8c718 Merge branch 'origin/develop' into Weblate. 2020-12-28 21:11:52 +01:00
WaldiS
faf5f4b51a Translated using Weblate (Polish)
Currently translated at 98.7% (492 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-12-28 21:11:52 +01:00
J-Jamet
b2f503b326 Upgrade to 2.9.8 2020-12-28 21:11:20 +01:00
J-Jamet
beb5484bf6 Merge tag '2.9.7' into develop
2.9.7
2020-12-28 21:00:32 +01:00
J-Jamet
ec63d75349 Merge branch 'release/2.9.7' 2020-12-28 21:00:27 +01:00
J-Jamet
4c0e79b245 Update CHANGELOG 2020-12-28 20:46:14 +01:00
J-Jamet
50a77684c1 Replace strong tag 2020-12-28 20:35:20 +01:00
J-Jamet
8bb84b486d Fix small translation 2020-12-28 20:33:41 +01:00
J-Jamet
4b05f2536f Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2020-12-28 20:33:05 +01:00
J-Jamet
d6f968fe7e Write permission until Android 10 #823 2020-12-28 12:06:48 +01:00
J-Jamet
ed758edd44 Fix small warning 2020-12-28 11:57:29 +01:00
J-Jamet
94b7fce2e5 Merge branch 'tibequadorian-patch-1' into develop 2020-12-28 11:54:56 +01:00
J-Jamet
dbd9c6cbb7 Fix rebuiltList crash 2020-12-28 11:39:16 +01:00
J-Jamet
0f6376fb80 Fix illegalstate when managing views 2020-12-28 11:17:38 +01:00
J-Jamet
9522328238 Fix crash when creating new field 2020-12-28 11:05:20 +01:00
J-Jamet
e6ad716119 Fix crash 2020-12-28 10:45:27 +01:00
Éfrit
440006bb08 Translated using Weblate (French)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-12-26 23:29:31 +01:00
tibequadorian
ea289ef7cf fix typo 2020-12-26 05:42:57 +01:00
solokot
352b171484 Translated using Weblate (Russian)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-25 17:10:11 +01:00
Óscar Fernández Díaz
969ab56bf8 Translated using Weblate (Spanish)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-25 00:29:12 +01:00
J-Jamet
062a9852e5 Fix small warning 2020-12-24 15:20:13 +01:00
Óscar Fernández Díaz
dd77d7a5e6 Translated using Weblate (Spanish)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 23:22:51 +01:00
SeerLite
f60e32522a Translated using Weblate (Spanish)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 23:22:51 +01:00
Óscar Fernández Díaz
070a91f19c Translated using Weblate (Spanish)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 23:20:44 +01:00
SeerLite
0790e80670 Translated using Weblate (Spanish)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 23:20:44 +01:00
Óscar Fernández Díaz
20841e3d7b Translated using Weblate (Spanish)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 23:13:36 +01:00
SeerLite
0caae233c3 Translated using Weblate (Spanish)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 23:13:36 +01:00
Óscar Fernández Díaz
5497d8fafb Translated using Weblate (Spanish)
Currently translated at 99.5% (496 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 17:41:42 +01:00
SeerLite
f69b43249c Translated using Weblate (Spanish)
Currently translated at 99.5% (496 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 17:41:41 +01:00
Óscar Fernández Díaz
b606fd98f6 Translated using Weblate (Spanish)
Currently translated at 88.5% (441 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 17:20:52 +01:00
SeerLite
ba3b7b0f1f Translated using Weblate (Spanish)
Currently translated at 88.5% (441 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 17:20:52 +01:00
Óscar Fernández Díaz
058d82dc36 Translated using Weblate (Spanish)
Currently translated at 86.3% (430 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 17:17:07 +01:00
SeerLite
6f0b0ac4fa Translated using Weblate (Spanish)
Currently translated at 86.3% (430 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 17:17:06 +01:00
Óscar Fernández Díaz
35f87b0f94 Translated using Weblate (Spanish)
Currently translated at 85.5% (426 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 17:15:19 +01:00
SeerLite
0ead9ce9b4 Translated using Weblate (Spanish)
Currently translated at 85.5% (426 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-12-23 17:15:18 +01:00
vachan-maker
80479a6a7c Translated using Weblate (Malayalam)
Currently translated at 76.9% (383 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2020-12-22 00:37:38 +01:00
J-Jamet
a7cea8201e Try to fix biometric crash 2020-12-20 11:41:58 +01:00
solokot
081a7fa798 Translated using Weblate (Russian)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-20 10:29:12 +01:00
WaldiS
85782c4f93 Translated using Weblate (Polish)
Currently translated at 97.9% (488 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-12-20 10:29:11 +01:00
uniprivscy
d7b7df26d7 Translated using Weblate (German)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-18 18:29:10 +01:00
J-Jamet
b6b1c8e31d Upgrade to version 2.9.7 2020-12-18 10:51:42 +01:00
J-Jamet
17156f7ca2 Merge tag '2.9.6' into develop
2.9.6
2020-12-18 10:15:51 +01:00
J-Jamet
0761d356b8 Merge branch 'release/2.9.6' 2020-12-18 10:15:41 +01:00
J-Jamet
6da747ce6f Fix keyfile bug #820 2020-12-18 10:06:38 +01:00
uniprivscy
87b1a1f527 Translated using Weblate (German)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-17 17:44:09 +01:00
Paul
72a8a55faf Translated using Weblate (German)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-17 17:44:09 +01:00
J-Jamet
9a6a709746 Inline presentation when sign in 2020-12-17 15:16:06 +01:00
J-Jamet
428b53cc56 Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2020-12-17 12:32:37 +01:00
J-Jamet
e688859e32 Fix exception when UI not fully loaded and click performed 2020-12-17 12:32:30 +01:00
Milo Ivir
98336da116 Translated using Weblate (Croatian)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-12-17 09:56:23 +01:00
Oğuz Ersen
c037e443b0 Translated using Weblate (Turkish)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-12-17 09:56:23 +01:00
Eric
d339a50e0a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-12-17 09:56:23 +01:00
Ihor Hordiichuk
7d836f2633 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2020-12-17 09:56:22 +01:00
solokot
45d8470b4c Translated using Weblate (Russian)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-17 09:56:22 +01:00
Oliver Cervera
1ca3bfe472 Translated using Weblate (Italian)
Currently translated at 99.7% (497 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-17 09:56:22 +01:00
Retrial
066da83d70 Translated using Weblate (Greek)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-12-17 09:56:22 +01:00
zeritti
44ab881751 Translated using Weblate (Czech)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-12-17 09:56:21 +01:00
J-Jamet
5ab3cf985a Merge branch 'develop' into feature/Autofill_Inline 2020-12-16 17:54:53 +01:00
J-Jamet
f271f2b181 Update to 2.9.6 2020-12-16 17:54:33 +01:00
J-Jamet
91d75be0ea Merge tag '2.9.5' into develop
2.9.5
2020-12-16 17:06:02 +01:00
J-Jamet
774dddca54 Merge branch 'release/2.9.5' 2020-12-16 17:05:47 +01:00
J-Jamet
e18b3436c9 Add inline autofill right icon 2020-12-16 15:37:23 +01:00
J-Jamet
fcb1b5ae6b First inline code 2020-12-16 15:25:04 +01:00
J-Jamet
de980d030a Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-12-16 11:13:48 +01:00
J-Jamet
0e859646fe Fix timeout reset #817 2020-12-15 19:40:27 +01:00
christopher robert
059c7b7713 Translated using Weblate (German)
Currently translated at 98.3% (490 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-15 13:29:10 +01:00
J-Jamet
5fb7bf71c8 Prevent auto switch back to previous keyboard if otp field exists #814 2020-12-15 11:47:57 +01:00
J-Jamet
8b0133ff7f Update CHANGELOG 2020-12-14 19:25:34 +01:00
J-Jamet
8d834946b8 Fix view flickering 2020-12-14 19:12:45 +01:00
J-Jamet
2f646395d4 Merge branch 'feature/Device_Unlock' into develop 2020-12-14 18:31:28 +01:00
J-Jamet
f6e79ba37b Add advanced unlock education hint 2020-12-14 18:23:41 +01:00
J-Jamet
e633c7a861 Biometric unlock in priority and device unlock when biometric not available 2020-12-14 17:51:18 +01:00
J-Jamet
dc02a8d78c Rollback to fix bug after orientation change 2020-12-14 16:59:38 +01:00
J-Jamet
baa9b88512 Check if current database is the same after activity result 2020-12-14 16:56:15 +01:00
J-Jamet
c522e87da8 Fix multiple methods in settings 2020-12-14 16:41:14 +01:00
Paul
ef5ebf2c15 Translated using Weblate (German)
Currently translated at 98.3% (490 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-14 13:23:34 +01:00
christopher robert
4b147e770c Translated using Weblate (German)
Currently translated at 98.3% (490 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-12-14 13:23:34 +01:00
J-Jamet
157a5c0b05 Refactor view visibility method 2020-12-13 23:39:17 +01:00
J-Jamet
f2288b0c64 Fix biometric prompt during orientation change 2020-12-13 23:25:44 +01:00
J-Jamet
d8506450aa Fix biometric orientation change 2020-12-13 23:19:04 +01:00
J-Jamet
f9b085e73f Fix min setting version and condition in Android R 2020-12-13 22:44:30 +01:00
J-Jamet
388cf6a91b Fix device credential condition and keep connexion if ActivityForResult requested 2020-12-13 22:33:58 +01:00
Stephan Paternotte
9e6e77b363 Translated using Weblate (Dutch)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-12-13 18:48:46 +01:00
J-Jamet
ec33ca8173 Refactoring Advanced unlock fragment and manager 2020-12-13 17:09:41 +01:00
J-Jamet
6be0457947 Add fragment 2020-12-13 14:18:21 +01:00
WaldiS
f3b84aa845 Translated using Weblate (Polish)
Currently translated at 98.1% (489 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-12-12 17:29:08 +01:00
J-Jamet
bd0b5b0954 Fix exception message 2020-12-12 14:53:23 +01:00
J-Jamet
7dc93604ad Refactoring AdvancedUnlockManager.kt 2020-12-12 14:13:13 +01:00
J-Jamet
0ab22698a6 Refactoring classes 2020-12-11 15:15:52 +01:00
J-Jamet
c885ce7aaf Refactoring of advanced unlock 2020-12-11 13:36:51 +01:00
J-Jamet
92d1a7b901 Fix string 2020-12-11 11:11:48 +01:00
J-Jamet
6119054b45 Upgrade to version 2.9.5 2020-12-11 11:03:00 +01:00
Eric
e7aed72398 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-12-11 01:55:56 +01:00
solokot
cee7fa50f5 Translated using Weblate (Russian)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-12-11 01:55:55 +01:00
Stephan Paternotte
39a38bb223 Translated using Weblate (Dutch)
Currently translated at 100.0% (498 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2020-12-11 01:55:55 +01:00
Oliver Cervera
7159a993db Translated using Weblate (Italian)
Currently translated at 99.7% (497 of 498 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-12-11 01:55:54 +01:00
J-Jamet
23933e80e3 Merge tag '2.9.4' into develop
2.9.4
2020-12-10 22:27:38 +01:00
148 changed files with 3745 additions and 1836 deletions

View File

@@ -1,3 +1,45 @@
KeePassDX(2.9.12)
* Fix OTP token type #863
* Fix auto open biometric prompt #862
* Fix back appearance setting #865
* Fix orientation change in settings #872
* Change memory unit to MiB #851
* Small changes #642
KeePassDX(2.9.11)
* Add Keyfile XML version 2 (fix hex) #844
* Fix hex Keyfile #861
KeePassDX(2.9.10)
* Try to fix autofill #852
* Fix database change dialog displayed too often #853
KeePassDX(2.9.9)
* Detect file changes and reload database #794
* Inline suggestions autofill with compatible keyboard (Android R) #827
* Add Keyfile XML version 2 #844
* Fix binaries of 64 bytes #835
* Special search in title fields #830
* Priority to OTP button in notifications #845
* Fix OTP generation for long secret key #848
* Fix small bugs #849
KeePassDX(2.9.8)
* Fix specific attachments with kdbx3.1 databases #828
* Fix small bugs
KeePassDX(2.9.7)
* Remove write permission since Android 10 #823
* Fix small bugs
KeePassDX(2.9.6)
* Fix KeyFile bug #820
KeePassDX(2.9.5)
* Unlock database by device credentials (PIN/Password/Pattern) with Android M+ #102 #152 #811
* Prevent auto switch back to previous keyboard if otp field exists #814
* Fix timeout reset #817
KeePassDX(2.9.4) KeePassDX(2.9.4)
* Fix small bugs #812 * Fix small bugs #812
* Argon2ID implementation #791 * Argon2ID implementation #791

View File

@@ -5,15 +5,15 @@ apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 30 compileSdkVersion 30
buildToolsVersion '30.0.2' buildToolsVersion '30.0.3'
ndkVersion '21.3.6528147' ndkVersion '21.3.6528147'
defaultConfig { defaultConfig {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 30 targetSdkVersion 30
versionCode = 48 versionCode = 56
versionName = "2.9.4" versionName = "2.9.12"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -92,7 +92,7 @@ android {
} }
} }
def room_version = "2.2.5" def room_version = "2.2.6"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@@ -110,6 +110,8 @@ dependencies {
// Database // Database
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
// Autofill
implementation "androidx.autofill:autofill:1.1.0-rc01"
// Crypto // Crypto
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01' implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
// Time // Time
@@ -121,7 +123,7 @@ dependencies {
// Apache Commons Collections // Apache Commons Collections
implementation 'commons-collections:commons-collections:3.2.2' implementation 'commons-collections:commons-collections:3.2.2'
// Apache Commons Codec // Apache Commons Codec
implementation 'commons-codec:commons-codec:1.14' implementation 'commons-codec:commons-codec:1.15'
// Icon pack // Icon pack
implementation project(path: ':icon-pack-classic') implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material') implementation project(path: ':icon-pack-material')

View File

@@ -14,10 +14,15 @@
android:name="android.permission.USE_BIOMETRIC" /> android:name="android.permission.USE_BIOMETRIC" />
<uses-permission <uses-permission
android:name="android.permission.VIBRATE"/> android:name="android.permission.VIBRATE"/>
<!-- Write permission until Android 10 -->
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<!-- Open apps from links -->
<uses-permission <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"/> android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application <application
android:label="@string/app_name" android:label="@string/app_name"

View File

@@ -26,6 +26,7 @@ import android.content.Intent
import android.content.IntentSender import android.content.IntentSender
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -33,6 +34,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
import com.kunzisoft.keepass.autofill.KeeAutofillService import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
@@ -40,7 +42,6 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.UriUtil
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : AppCompatActivity() { class AutofillLauncherActivity : AppCompatActivity() {
@@ -84,9 +85,9 @@ class AutofillLauncherActivity : AppCompatActivity() {
private fun launchSelection(searchInfo: SearchInfo) { private fun launchSelection(searchInfo: SearchInfo) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper.retrieveAssistStructure(intent) val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
if (assistStructure == null) { if (autofillComponent == null) {
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
finish() finish()
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId, } else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
@@ -105,21 +106,21 @@ class AutofillLauncherActivity : AppCompatActivity() {
searchInfo, searchInfo,
{ items -> { items ->
// Items found // Items found
AutofillHelper.buildResponse(this, items) AutofillHelper.buildResponseAndSetResult(this, items)
finish() finish()
}, },
{ {
// Show the database UI to select the entry // Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this, GroupActivity.launchForAutofillResult(this,
readOnly, readOnly,
assistStructure, autofillComponent,
searchInfo, searchInfo,
false) false)
}, },
{ {
// If database not open // If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this, FileDatabaseSelectActivity.launchForAutofillResult(this,
assistStructure, autofillComponent,
searchInfo) searchInfo)
} }
) )
@@ -196,7 +197,8 @@ class AutofillLauncherActivity : AppCompatActivity() {
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getAuthIntentSenderForSelection(context: Context, fun getAuthIntentSenderForSelection(context: Context,
searchInfo: SearchInfo? = null): IntentSender { searchInfo: SearchInfo? = null,
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
// Doesn't work with Parcelable (don't know why?) // Doesn't work with Parcelable (don't know why?)
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
@@ -205,6 +207,11 @@ class AutofillLauncherActivity : AppCompatActivity() {
putExtra(KEY_SEARCH_DOMAIN, it.webDomain) putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
putExtra(KEY_SEARCH_SCHEME, it.webScheme) putExtra(KEY_SEARCH_SCHEME, it.webScheme)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let {
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
}, },
PendingIntent.FLAG_CANCEL_CURRENT).intentSender PendingIntent.FLAG_CANCEL_CURRENT).intentSender
} }

View File

@@ -39,7 +39,9 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
@@ -52,7 +54,9 @@ import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.ClipboardHelper
@@ -133,7 +137,7 @@ class EntryActivity : LockingActivity() {
} }
// Focus view to reinitialize timeout // Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout) coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
// Init the clipboard helper // Init the clipboard helper
clipboardHelper = ClipboardHelper(this) clipboardHelper = ClipboardHelper(this)
@@ -150,6 +154,10 @@ class EntryActivity : LockingActivity() {
if (result.isSuccess) if (result.isSuccess)
finish() finish()
} }
ACTION_DATABASE_RELOAD_TASK -> {
// Close the current activity
finish()
}
} }
coordinatorLayout?.showActionError(result) coordinatorLayout?.showActionError(result)
} }
@@ -198,8 +206,7 @@ class EntryActivity : LockingActivity() {
// Refresh Menu // Refresh Menu
invalidateOptionsMenu() invalidateOptionsMenu()
val entryInfo = entry.getEntryInfo(Database.getInstance()) val entryInfo = entry.getEntryInfo(mDatabase)
// Manage entry copy to start notification if allowed // Manage entry copy to start notification if allowed
if (mFirstLaunchOfActivity) { if (mFirstLaunchOfActivity) {
// Manage entry to launch copying notification if allowed // Manage entry to launch copying notification if allowed
@@ -231,23 +238,21 @@ class EntryActivity : LockingActivity() {
private fun fillEntryDataInContentsView(entry: Entry) { private fun fillEntryDataInContentsView(entry: Entry) {
val database = Database.getInstance() val entryInfo = entry.getEntryInfo(mDatabase)
database.startManageEntry(entry)
// Assign title icon // Assign title icon
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor) titleIconView?.assignDatabaseIcon(mDatabase!!.drawFactory, entryInfo.icon, iconColor)
// Assign title text // Assign title text
val entryTitle = entry.title val entryTitle = entryInfo.title
collapsingToolbarLayout?.title = entryTitle collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle toolbar?.title = entryTitle
// Assign basic fields // Assign basic fields
entryContentsView?.assignUserName(entry.username) { entryContentsView?.assignUserName(entryInfo.username) {
database.startManageEntry(entry) clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
clipboardHelper?.timeoutCopyToClipboard(entry.username,
getString(R.string.copy_field, getString(R.string.copy_field,
getString(R.string.entry_user_name))) getString(R.string.entry_user_name)))
database.stopManageEntry(entry)
} }
val isFirstTimeAskAllowCopyPasswordAndProtectedFields = val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
@@ -277,11 +282,9 @@ class EntryActivity : LockingActivity() {
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) { val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
View.OnClickListener { View.OnClickListener {
database.startManageEntry(entry) clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
clipboardHelper?.timeoutCopyToClipboard(entry.password,
getString(R.string.copy_field, getString(R.string.copy_field,
getString(R.string.entry_password))) getString(R.string.entry_password)))
database.stopManageEntry(entry)
} }
} else { } else {
// If dialog not already shown // If dialog not already shown
@@ -291,44 +294,46 @@ class EntryActivity : LockingActivity() {
null null
} }
} }
entryContentsView?.assignPassword(entry.password, entryContentsView?.assignPassword(entryInfo.password,
allowCopyPasswordAndProtectedFields, allowCopyPasswordAndProtectedFields,
onPasswordCopyClickListener) onPasswordCopyClickListener)
//Assign OTP field //Assign OTP field
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress, entry.getOtpElement()?.let { otpElement ->
View.OnClickListener { entryContentsView?.assignOtp(otpElement, entryProgress) {
entry.getOtpElement()?.let { otpElement -> clipboardHelper?.timeoutCopyToClipboard(
clipboardHelper?.timeoutCopyToClipboard( otpElement.token,
otpElement.token, getString(R.string.copy_field, getString(R.string.entry_otp))
getString(R.string.copy_field, getString(R.string.entry_otp)) )
) }
} }
})
entryContentsView?.assignURL(entry.url) entryContentsView?.assignURL(entryInfo.url)
entryContentsView?.assignNotes(entry.notes) entryContentsView?.assignNotes(entryInfo.notes)
// Assign custom fields // Assign custom fields
if (mDatabase?.allowEntryCustomFields() == true) { if (mDatabase?.allowEntryCustomFields() == true) {
entryContentsView?.clearExtraFields() entryContentsView?.clearExtraFields()
entry.getExtraFields().forEach { field -> entryInfo.customFields.forEach { field ->
val label = field.name val label = field.name
val value = field.protectedValue // OTP field is already managed in dedicated view
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
if (allowCopyProtectedField) { val value = field.protectedValue
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) { val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
clipboardHelper?.timeoutCopyToClipboard( if (allowCopyProtectedField) {
value.toString(), entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
getString(R.string.copy_field, label) clipboardHelper?.timeoutCopyToClipboard(
) value.toString(),
} getString(R.string.copy_field, label)
} else { )
// If dialog not already shown }
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
} else { } else {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null) // If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
} else {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
}
} }
} }
} }
@@ -336,24 +341,16 @@ class EntryActivity : LockingActivity() {
entryContentsView?.setHiddenProtectedValue(!mShowPassword) entryContentsView?.setHiddenProtectedValue(!mShowPassword)
// Manage attachments // Manage attachments
mDatabase?.binaryPool?.let { binaryPool -> entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem -> createDocument(this, attachmentItem.name)?.let { requestCode ->
createDocument(this, attachmentItem.name)?.let { requestCode -> mAttachmentsToDownload[requestCode] = attachmentItem
mAttachmentsToDownload[requestCode] = attachmentItem
}
} }
} }
// Assign dates // Assign dates
entryContentsView?.assignCreationDate(entry.creationTime) entryContentsView?.assignCreationDate(entryInfo.creationTime)
entryContentsView?.assignModificationDate(entry.lastModificationTime) entryContentsView?.assignModificationDate(entryInfo.modificationTime)
entryContentsView?.assignLastAccessDate(entry.lastAccessTime) entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
entryContentsView?.setExpires(entry.isCurrentlyExpires)
if (entry.expires) {
entryContentsView?.assignExpiresDate(entry.expiryTime)
} else {
entryContentsView?.assignExpiresDate(getString(R.string.never))
}
// Manage history // Manage history
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
@@ -368,8 +365,6 @@ class EntryActivity : LockingActivity() {
// Assign special data // Assign special data
entryContentsView?.assignUUID(entry.nodeId.id) entryContentsView?.assignUUID(entry.nodeId.id)
database.stopManageEntry(entry)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -407,6 +402,9 @@ class EntryActivity : LockingActivity() {
menu.findItem(R.id.menu_save_database)?.isVisible = false menu.findItem(R.id.menu_save_database)?.isVisible = false
menu.findItem(R.id.menu_edit)?.isVisible = false menu.findItem(R.id.menu_edit)?.isVisible = false
} }
if (mSpecialMode != SpecialMode.DEFAULT) {
menu.findItem(R.id.menu_reload_database)?.isVisible = false
}
val gotoUrl = menu.findItem(R.id.menu_goto_url) val gotoUrl = menu.findItem(R.id.menu_goto_url)
gotoUrl?.apply { gotoUrl?.apply {
@@ -500,6 +498,9 @@ class EntryActivity : LockingActivity() {
R.id.menu_save_database -> { R.id.menu_save_database -> {
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
} }
R.id.menu_reload_database -> {
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
}
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any) android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.app.TimePickerDialog import android.app.TimePickerDialog
import android.app.assist.AssistStructure
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -48,6 +47,8 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
@@ -60,6 +61,7 @@ import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
@@ -134,7 +136,7 @@ class EntryEditActivity : LockingActivity(),
} }
// Focus view to reinitialize timeout // Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout) coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
stopService(Intent(this, ClipboardEntryNotificationService::class.java)) stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::class.java)) stopService(Intent(this, KeyboardEntryNotificationService::class.java))
@@ -334,6 +336,10 @@ class EntryEditActivity : LockingActivity(),
Log.e(TAG, "Unable to retrieve entry after database action", e) Log.e(TAG, "Unable to retrieve entry after database action", e)
} }
} }
ACTION_DATABASE_RELOAD_TASK -> {
// Close the current activity
finish()
}
} }
coordinatorLayout?.showActionError(result) coordinatorLayout?.showActionError(result)
} }
@@ -360,7 +366,7 @@ class EntryEditActivity : LockingActivity(),
// Build Autofill response with the entry selected // Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mDatabase?.let { database -> mDatabase?.let { database ->
AutofillHelper.buildResponse(this@EntryEditActivity, AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
entry.getEntryInfo(database)) entry.getEntryInfo(database))
} }
} }
@@ -478,8 +484,12 @@ class EntryEditActivity : LockingActivity(),
} }
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) { override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
verifyNameField(newField) { if (oldField.name.equals(newField.name, true)) {
entryEditFragment?.replaceExtraField(oldField, newField) entryEditFragment?.replaceExtraField(oldField, newField)
} else {
verifyNameField(newField) {
entryEditFragment?.replaceExtraField(oldField, newField)
}
} }
} }
@@ -609,13 +619,7 @@ class EntryEditActivity : LockingActivity(),
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
MenuUtil.contributionMenuInflater(menuInflater, menu)
val inflater = menuInflater
inflater.inflate(R.menu.database, menu)
// Save database not needed here
menu.findItem(R.id.menu_save_database)?.isVisible = false
MenuUtil.contributionMenuInflater(inflater, menu)
return true return true
} }
@@ -672,9 +676,6 @@ class EntryEditActivity : LockingActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_save_database -> {
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
}
R.id.menu_contribute -> { R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this) MenuUtil.onContributionItemSelected(this)
return true return true
@@ -908,7 +909,7 @@ class EntryEditActivity : LockingActivity(),
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: Activity,
assistStructure: AssistStructure, autofillComponent: AutofillComponent,
group: Group, group: Group,
searchInfo: SearchInfo? = null) { searchInfo: SearchInfo? = null) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
@@ -916,7 +917,7 @@ class EntryEditActivity : LockingActivity(),
intent.putExtra(KEY_PARENT, group.nodeId) intent.putExtra(KEY_PARENT, group.nodeId)
AutofillHelper.startActivityForAutofillResult(activity, AutofillHelper.startActivityForAutofillResult(activity,
intent, intent,
assistStructure, autofillComponent,
searchInfo) searchInfo)
} }
} }

View File

@@ -37,6 +37,7 @@ import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
@@ -148,6 +149,8 @@ class EntryEditFragment: StylishFragment() {
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
taIconColor?.recycle() taIconColor?.recycle()
rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext())
// Retrieve the new entry after an orientation change // Retrieve the new entry after an orientation change
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true) if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -48,6 +47,7 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
@@ -434,8 +434,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
when (item.itemId) { when (item.itemId) {
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url) android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
} }
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
companion object { companion object {
@@ -501,11 +501,11 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: Activity,
assistStructure: AssistStructure, autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) { searchInfo: SearchInfo? = null) {
AutofillHelper.startActivityForAutofillResult(activity, AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java), Intent(activity, FileDatabaseSelectActivity::class.java),
assistStructure, autofillComponent,
searchInfo) searchInfo)
} }

View File

@@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.app.SearchManager import android.app.SearchManager
import android.app.assist.AssistStructure
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -50,7 +49,9 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
@@ -69,6 +70,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
@@ -153,7 +155,7 @@ class GroupActivity : LockingActivity(),
taTextColor.recycle() taTextColor.recycle()
// Focus view to reinitialize timeout // Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(rootContainerView) rootContainerView?.resetAppTimeoutWhenViewFocusedOrChanged(this)
// Retrieve elements after an orientation change // Retrieve elements after an orientation change
if (savedInstanceState != null) { if (savedInstanceState != null) {
@@ -227,10 +229,10 @@ class GroupActivity : LockingActivity(),
currentGroup, searchInfo) currentGroup, searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo, assistStructure -> { searchInfo, autofillComponent ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
EntryEditActivity.launchForAutofillResult(this@GroupActivity, EntryEditActivity.launchForAutofillResult(this@GroupActivity,
assistStructure, autofillComponent,
currentGroup, searchInfo) currentGroup, searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
} else { } else {
@@ -316,7 +318,12 @@ class GroupActivity : LockingActivity(),
if (result.isSuccess) { if (result.isSuccess) {
// Rebuild all the list to avoid bug when delete node from sort // Rebuild all the list to avoid bug when delete node from sort
mListNodesFragment?.rebuildList() try {
mListNodesFragment?.rebuildList()
} catch (e: Exception) {
Log.e(TAG, "Unable to rebuild the list after deletion")
e.printStackTrace()
}
// Add trash in views list if it doesn't exists // Add trash in views list if it doesn't exists
if (database.isRecycleBinEnabled) { if (database.isRecycleBinEnabled) {
@@ -336,6 +343,12 @@ class GroupActivity : LockingActivity(),
} }
} }
} }
ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity
startActivity(intent)
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
} }
coordinatorLayout?.showActionError(result) coordinatorLayout?.showActionError(result)
@@ -659,7 +672,7 @@ class GroupActivity : LockingActivity(),
// Build response with the entry selected // Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
mDatabase?.let { database -> mDatabase?.let { database ->
AutofillHelper.buildResponse(this, AutofillHelper.buildResponseAndSetResult(this,
entry.getEntryInfo(database)) entry.getEntryInfo(database))
} }
} }
@@ -872,6 +885,8 @@ class GroupActivity : LockingActivity(),
} }
if (mSpecialMode == SpecialMode.DEFAULT) { if (mSpecialMode == SpecialMode.DEFAULT) {
MenuUtil.defaultMenuInflater(inflater, menu) MenuUtil.defaultMenuInflater(inflater, menu)
} else {
menu.findItem(R.id.menu_reload_database)?.isVisible = false
} }
// Menu for recycle bin // Menu for recycle bin
@@ -997,6 +1012,10 @@ class GroupActivity : LockingActivity(),
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
return true return true
} }
R.id.menu_reload_database -> {
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
return true
}
R.id.menu_empty_recycle_bin -> { R.id.menu_empty_recycle_bin -> {
mCurrentGroup?.getChildren()?.let { listChildren -> mCurrentGroup?.getChildren()?.let { listChildren ->
// Automatically delete all elements // Automatically delete all elements
@@ -1124,7 +1143,16 @@ class GroupActivity : LockingActivity(),
private fun rebuildListNodes() { private fun rebuildListNodes() {
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment? mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment?
// to refresh fragment // to refresh fragment
mListNodesFragment?.rebuildList() try {
mListNodesFragment?.rebuildList()
} catch (e: Exception) {
e.printStackTrace()
coordinatorLayout?.let { coordinatorLayout ->
Snackbar.make(coordinatorLayout,
R.string.error_rebuild_list,
Snackbar.LENGTH_LONG).asError().show()
}
}
mCurrentGroup = mListNodesFragment?.mainGroup mCurrentGroup = mListNodesFragment?.mainGroup
// Remove search in intent // Remove search in intent
deletePreviousSearchGroup() deletePreviousSearchGroup()
@@ -1295,14 +1323,14 @@ class GroupActivity : LockingActivity(),
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: Activity,
readOnly: Boolean, readOnly: Boolean,
assistStructure: AssistStructure, autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
autoSearch: Boolean = false) { autoSearch: Boolean = false) {
checkTimeAndBuildIntent(activity, null, readOnly) { intent -> checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch) intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
AutofillHelper.startActivityForAutofillResult(activity, AutofillHelper.startActivityForAutofillResult(activity,
intent, intent,
assistStructure, autofillComponent,
searchInfo) searchInfo)
} }
} }
@@ -1419,21 +1447,21 @@ class GroupActivity : LockingActivity(),
} }
) )
}, },
{ searchInfo, assistStructure -> { searchInfo, autofillComponent ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SearchHelper.checkAutoSearchInfo(activity, SearchHelper.checkAutoSearchInfo(activity,
Database.getInstance(), Database.getInstance(),
searchInfo, searchInfo,
{ items -> { items ->
// Response is build // Response is build
AutofillHelper.buildResponse(activity, items) AutofillHelper.buildResponseAndSetResult(activity, items)
onValidateSpecialMode() onValidateSpecialMode()
}, },
{ {
// Here no search info found, disable auto search // Here no search info found, disable auto search
GroupActivity.launchForAutofillResult(activity, GroupActivity.launchForAutofillResult(activity,
readOnly, readOnly,
assistStructure, autofillComponent,
searchInfo, searchInfo,
false) false)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()

View File

@@ -22,30 +22,24 @@ package com.kunzisoft.keepass.activities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.*
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
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.appcompat.view.ActionMode
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.* import java.util.*
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener { class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
@@ -197,7 +191,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
} }
// Refresh data // Refresh data
rebuildList() try {
rebuildList()
} catch (e: Exception) {
Log.e(TAG, "Unable to rebuild the list during resume")
e.printStackTrace()
}
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) { if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found " // To show the " no search entry found "
@@ -209,10 +208,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
} }
} }
@Throws(IllegalArgumentException::class)
fun rebuildList() { fun rebuildList() {
// Add elements to the list // Add elements to the list
mainGroup?.let { mainGroup -> mainGroup?.let { mainGroup ->
mAdapter?.apply { mAdapter?.apply {
// Thrown an exception when sort cannot be performed
rebuildList(mainGroup) rebuildList(mainGroup)
// To visually change the elements // To visually change the elements
if (PreferencesUtil.APPEARANCE_CHANGED) { if (PreferencesUtil.APPEARANCE_CHANGED) {
@@ -231,8 +232,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
} }
// Tell the adapter to refresh it's list // Tell the adapter to refresh it's list
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters) try {
rebuildList() mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
rebuildList()
} catch (e:Exception) {
Log.e(TAG, "Unable to rebuild the list with the sort")
e.printStackTrace()
}
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
@@ -37,8 +36,8 @@ import android.widget.*
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
@@ -49,9 +48,9 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -69,14 +68,13 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import kotlinx.android.synthetic.main.activity_password.* import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException import java.io.FileNotFoundException
open class PasswordActivity : SpecialModeActivity() { open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views // Views
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
@@ -86,9 +84,8 @@ open class PasswordActivity : SpecialModeActivity() {
private var confirmButtonView: Button? = null private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null private var checkboxKeyFileView: CompoundButton? = null
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private var infoContainerView: ViewGroup? = null private var infoContainerView: ViewGroup? = null
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels() private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
@@ -114,7 +111,6 @@ open class PasswordActivity : SpecialModeActivity() {
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
private var mAllowAutoOpenBiometricPrompt: Boolean = true private var mAllowAutoOpenBiometricPrompt: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -134,7 +130,6 @@ open class PasswordActivity : SpecialModeActivity() {
keyFileSelectionView = findViewById(R.id.keyfile_selection) keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox) checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox) checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
advancedUnlockInfoView = findViewById(R.id.biometric_info)
infoContainerView = findViewById(R.id.activity_password_info_container) infoContainerView = findViewById(R.id.activity_password_info_container)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
@@ -161,10 +156,6 @@ open class PasswordActivity : SpecialModeActivity() {
} }
}) })
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton()
}
// If is a view intent // If is a view intent
getUriFromIntent(intent) getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) { if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
@@ -174,6 +165,24 @@ open class PasswordActivity : SpecialModeActivity() {
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
} }
// Init Biometric elements
advancedUnlockFragment = supportFragmentManager
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
if (advancedUnlockFragment == null) {
advancedUnlockFragment = AdvancedUnlockFragment()
supportFragmentManager.commit {
replace(R.id.fragment_advanced_unlock_container_view,
advancedUnlockFragment!!,
UNLOCK_FRAGMENT_TAG)
}
}
// Listen password checkbox to init advanced unlock and confirmation button
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
advancedUnlockFragment?.checkUnlockAvailability()
enableOrNotTheConfirmationButton()
}
// Observe if default database // Observe if default database
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
mDefaultDatabase = isDefaultDatabase mDefaultDatabase = isDefaultDatabase
@@ -207,12 +216,7 @@ open class PasswordActivity : SpecialModeActivity() {
when (actionTask) { when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> { ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error // Recheck advanced unlock if error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { advancedUnlockFragment?.initAdvancedUnlockMode()
if (PreferencesUtil.isAdvancedUnlockEnable(this@PasswordActivity)) {
// Stay with the same mode and init it
advancedUnlockedManager?.initAdvancedUnlockMode()
}
}
if (result.isSuccess) { if (result.isSuccess) {
mDatabaseKeyFileUri = null mDatabaseKeyFileUri = null
@@ -320,6 +324,33 @@ open class PasswordActivity : SpecialModeActivity() {
finish() finish()
} }
override fun retrieveCredentialForEncryption(): String {
return passwordView?.text?.toString() ?: ""
}
override fun conditionToStoreCredential(): Boolean {
return checkboxPasswordView?.isChecked == true
}
override fun onCredentialEncrypted(databaseUri: Uri,
encryptedCredential: String,
ivSpec: String) {
// Load the database if password is registered with biometric
verifyCheckboxesAndLoadDatabase(
CipherDatabaseEntity(
databaseUri.toString(),
encryptedCredential,
ivSpec)
)
}
override fun onCredentialDecrypted(databaseUri: Uri,
decryptedCredential: String) {
// Load the database if password is retrieve from biometric
// Retrieve from biometric
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
}
private val onEditorActionListener = object : TextView.OnEditorActionListener { private val onEditorActionListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == IME_ACTION_DONE) { if (actionId == IME_ACTION_DONE) {
@@ -386,48 +417,9 @@ open class PasswordActivity : SpecialModeActivity() {
verifyCheckboxesAndLoadDatabase(password, keyFileUri) verifyCheckboxesAndLoadDatabase(password, keyFileUri)
} else { } else {
// Init Biometric elements // Init Biometric elements
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { advancedUnlockFragment?.loadDatabase(databaseFileUri,
if (PreferencesUtil.isAdvancedUnlockEnable(this)) { mAllowAutoOpenBiometricPrompt
if (advancedUnlockedManager == null && mProgressDatabaseTaskProvider?.isBinded() != true)
&& databaseFileUri != null) {
advancedUnlockedManager = AdvancedUnlockedManager(this,
databaseFileUri,
advancedUnlockInfoView,
checkboxPasswordView,
enableButtonOnCheckedChangeListener,
passwordView,
{ passwordEncrypted, ivSpec ->
// Load the database if password is registered with biometric
if (passwordEncrypted != null && ivSpec != null) {
verifyCheckboxesAndLoadDatabase(
CipherDatabaseEntity(
databaseFileUri.toString(),
passwordEncrypted,
ivSpec)
)
}
},
{ passwordDecrypted ->
// Load the database if password is retrieve from biometric
passwordDecrypted?.let {
// Retrieve from biometric
verifyKeyFileCheckboxAndLoadDatabase(it)
}
})
}
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable =
mAllowAutoOpenBiometricPrompt && mProgressDatabaseTaskProvider?.isBinded() != true
advancedUnlockedManager?.checkBiometricAvailability()
} else {
advancedUnlockInfoView?.visibility = View.GONE
advancedUnlockedManager?.destroy()
advancedUnlockedManager = null
}
}
if (advancedUnlockedManager == null) {
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
}
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
} }
enableOrNotTheConfirmationButton() enableOrNotTheConfirmationButton()
@@ -479,11 +471,6 @@ open class PasswordActivity : SpecialModeActivity() {
override fun onPause() { override fun onPause() {
mProgressDatabaseTaskProvider?.unregisterProgressTask() mProgressDatabaseTaskProvider?.unregisterProgressTask()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.destroy()
advancedUnlockedManager = null
}
// Reinit locking activity UI variable // Reinit locking activity UI variable
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true mAllowAutoOpenBiometricPrompt = true
@@ -592,11 +579,6 @@ open class PasswordActivity : SpecialModeActivity() {
MenuUtil.defaultMenuInflater(inflater, menu) MenuUtil.defaultMenuInflater(inflater, menu)
} }
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// biometric menu
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
}
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
launchEducation(menu) launchEducation(menu)
@@ -606,13 +588,13 @@ open class PasswordActivity : SpecialModeActivity() {
// Check permission // Check permission
private fun checkPermission() { private fun checkPermission() {
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE if (Build.VERSION.SDK_INT in 23..28
val permissions = arrayOf(writePermission)
if (Build.VERSION.SDK_INT >= 23
&& !readOnly && !readOnly
&& !mPermissionAsked) { && !mPermissionAsked) {
mPermissionAsked = true mPermissionAsked = true
// Check self permission to show or not the dialog // Check self permission to show or not the dialog
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
val permissions = arrayOf(writePermission)
if (toolbar != null if (toolbar != null
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) { && ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST) ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
@@ -672,21 +654,14 @@ open class PasswordActivity : SpecialModeActivity() {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(passwordActivityEducation, menu)
}) })
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M advancedUnlockFragment?.performEducation(passwordActivityEducation,
&& !readOnlyEducationPerformed) { readOnlyEducationPerformed,
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(this) {
PreferencesUtil.isAdvancedUnlockEnable(applicationContext) performedNextEducation(passwordActivityEducation, menu)
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) },
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.visibility == View.VISIBLE {
&& advancedUnlockInfoView?.unlockIconImageView != null performedNextEducation(passwordActivityEducation, menu)
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!, })
{
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
})
}
} }
} }
@@ -708,10 +683,7 @@ open class PasswordActivity : SpecialModeActivity() {
readOnly = !readOnly readOnly = !readOnly
changeOpenFileReadIcon(item) changeOpenFileReadIcon(item)
} }
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
advancedUnlockedManager?.deleteEncryptedDatabaseKey()
}
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
@@ -725,6 +697,9 @@ open class PasswordActivity : SpecialModeActivity() {
mAllowAutoOpenBiometricPrompt = false mAllowAutoOpenBiometricPrompt = false
// To get device credential unlock result
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
// To get entry in result // To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
@@ -745,7 +720,7 @@ open class PasswordActivity : SpecialModeActivity() {
when (resultCode) { when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK -> { LockingActivity.RESULT_EXIT_LOCK -> {
clearCredentialsViews() clearCredentialsViews()
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this)) Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
} }
Activity.RESULT_CANCELED -> { Activity.RESULT_CANCELED -> {
clearCredentialsViews() clearCredentialsViews()
@@ -758,6 +733,8 @@ open class PasswordActivity : SpecialModeActivity() {
private val TAG = PasswordActivity::class.java.name private val TAG = PasswordActivity::class.java.name
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
private const val KEY_FILENAME = "fileName" private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile" private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW" private const val VIEW_INTENT = "android.intent.action.VIEW"
@@ -861,13 +838,13 @@ open class PasswordActivity : SpecialModeActivity() {
fun launchForAutofillResult(activity: Activity, fun launchForAutofillResult(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
assistStructure: AssistStructure, autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
assistStructure, autofillComponent,
searchInfo) searchInfo)
} }
} }
@@ -925,11 +902,11 @@ open class PasswordActivity : SpecialModeActivity() {
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo, assistStructure -> // Autofill Selection Action { searchInfo, autofillComponent -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PasswordActivity.launchForAutofillResult(activity, PasswordActivity.launchForAutofillResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
assistStructure, autofillComponent,
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
} else { } else {

View File

@@ -0,0 +1,92 @@
/*
* 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.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
class DatabaseChangedDialogFragment : DialogFragment() {
var actionDatabaseListener: ActionDatabaseChangedListener? = null
override fun onPause() {
super.onPause()
actionDatabaseListener = null
this.dismiss()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(OLD_FILE_DATABASE_INFO)
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(NEW_FILE_DATABASE_INFO)
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder()
if (newSnapFileDatabaseInfo.exists) {
stringBuilder.append(getString(R.string.warning_database_info_changed))
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
+ "\n\n" +
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
} else {
stringBuilder.append(getString(R.string.warning_database_revoked))
}
builder.setMessage(stringBuilder)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
actionDatabaseListener?.validateDatabaseChanged()
}
return builder.create()
}
}
return super.onCreateDialog(savedInstanceState)
}
interface ActionDatabaseChangedListener {
fun validateDatabaseChanged()
}
companion object {
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
newSnapFileDatabaseInfo: SnapFileDatabaseInfo)
: DatabaseChangedDialogFragment {
val fragment = DatabaseChangedDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
}
return fragment
}
}
}

View File

@@ -29,10 +29,7 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.AdapterView import android.widget.*
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@@ -57,6 +54,7 @@ class SetOTPDialogFragment : DialogFragment() {
private var mOtpElement: OtpElement = OtpElement() private var mOtpElement: OtpElement = OtpElement()
private var otpTypeMessage: TextView? = null
private var otpTypeSpinner: Spinner? = null private var otpTypeSpinner: Spinner? = null
private var otpTokenTypeSpinner: Spinner? = null private var otpTokenTypeSpinner: Spinner? = null
private var otpSecretContainer: TextInputLayout? = null private var otpSecretContainer: TextInputLayout? = null
@@ -74,6 +72,8 @@ class SetOTPDialogFragment : DialogFragment() {
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
private var mHotpTokenTypeArray: Array<OtpTokenType>? = null
private var mTotpTokenTypeArray: Array<OtpTokenType>? = null
private var mManualEvent = false private var mManualEvent = false
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus -> private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
@@ -134,6 +134,7 @@ class SetOTPDialogFragment : DialogFragment() {
activity?.let { activity -> activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup? val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
otpTypeMessage = root?.findViewById(R.id.setup_otp_type_message)
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type) otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type) otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label) otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
@@ -183,23 +184,23 @@ class SetOTPDialogFragment : DialogFragment() {
// HOTP / TOTP Type selection // HOTP / TOTP Type selection
val otpTypeArray = OtpType.values() val otpTypeArray = OtpType.values()
otpTypeAdapter = ArrayAdapter<OtpType>(activity, otpTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, otpTypeArray).apply { android.R.layout.simple_spinner_item, otpTypeArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
otpTypeSpinner?.adapter = otpTypeAdapter otpTypeSpinner?.adapter = otpTypeAdapter
// Otp Token type selection // Otp Token type selection
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues() mHotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
hotpTokenTypeAdapter = ArrayAdapter(activity, hotpTokenTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply { android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
// Proprietary only on closed and full version // Proprietary only on closed and full version
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues( mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION) BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
totpTokenTypeAdapter = ArrayAdapter(activity, totpTokenTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply { android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
otpTokenTypeAdapter = hotpTokenTypeAdapter otpTokenTypeAdapter = hotpTokenTypeAdapter
@@ -207,7 +208,7 @@ class SetOTPDialogFragment : DialogFragment() {
// OTP Algorithm // OTP Algorithm
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values() val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity, otpAlgorithmAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply { android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
} }
@@ -372,24 +373,40 @@ class SetOTPDialogFragment : DialogFragment() {
} }
private fun upgradeTokenType() { private fun upgradeTokenType() {
val tokenType = mOtpElement.tokenType
when (mOtpElement.type) { when (mOtpElement.type) {
OtpType.HOTP -> { OtpType.HOTP -> {
otpPeriodContainer?.visibility = View.GONE otpPeriodContainer?.visibility = View.GONE
otpCounterContainer?.visibility = View.VISIBLE otpCounterContainer?.visibility = View.VISIBLE
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
otpTokenTypeSpinner?.setSelection(OtpTokenType mHotpTokenTypeArray?.let { otpTokenTypeArray ->
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType)) defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC4226)
}
} }
OtpType.TOTP -> { OtpType.TOTP -> {
otpPeriodContainer?.visibility = View.VISIBLE otpPeriodContainer?.visibility = View.VISIBLE
otpCounterContainer?.visibility = View.GONE otpCounterContainer?.visibility = View.GONE
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
otpTokenTypeSpinner?.setSelection(OtpTokenType mTotpTokenTypeArray?.let { otpTokenTypeArray ->
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType)) defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC6238)
}
} }
} }
} }
private fun defineOtpTokenTypeSpinner(otpTokenTypeArray: Array<OtpTokenType>,
tokenType: OtpTokenType,
defaultTokenType: OtpTokenType) {
val formTokenType = if (otpTokenTypeArray.contains(tokenType)) {
otpTypeMessage?.visibility = View.GONE
tokenType
} else {
otpTypeMessage?.visibility = View.VISIBLE
defaultTokenType
}
otpTokenTypeSpinner?.setSelection(otpTokenTypeArray.indexOf(formTokenType))
}
private fun upgradeParameters() { private fun upgradeParameters() {
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values() otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
.indexOf(mOtpElement.algorithm)) .indexOf(mOtpElement.algorithm))

View File

@@ -19,10 +19,10 @@
*/ */
package com.kunzisoft.keepass.activities.helpers package com.kunzisoft.keepass.activities.helpers
import android.app.assist.AssistStructure
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
@@ -106,7 +106,7 @@ object EntrySelectionHelper {
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode { fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (AutofillHelper.retrieveAssistStructure(intent) != null) if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return SpecialMode.SELECTION return SpecialMode.SELECTION
} }
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode? return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
@@ -119,7 +119,7 @@ object EntrySelectionHelper {
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode { fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (AutofillHelper.retrieveAssistStructure(intent) != null) if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return TypeMode.AUTOFILL return TypeMode.AUTOFILL
} }
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
@@ -136,7 +136,7 @@ object EntrySelectionHelper {
saveAction: (searchInfo: SearchInfo) -> Unit, saveAction: (searchInfo: SearchInfo) -> Unit,
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit, keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
autofillSelectionAction: (searchInfo: SearchInfo?, autofillSelectionAction: (searchInfo: SearchInfo?,
assistStructure: AssistStructure) -> Unit, autofillComponent: AutofillComponent) -> Unit,
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) { autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
when (retrieveSpecialModeFromIntent(intent)) { when (retrieveSpecialModeFromIntent(intent)) {
@@ -167,14 +167,14 @@ object EntrySelectionHelper {
} }
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent) val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
var assistStructureInit = false var autofillComponentInit = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure -> AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
autofillSelectionAction.invoke(searchInfo, assistStructure) autofillSelectionAction.invoke(searchInfo, autofillComponent)
assistStructureInit = true autofillComponentInit = true
} }
} }
if (!assistStructureInit) { if (!autofillComponentInit) {
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) { if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
when (retrieveTypeModeFromIntent(intent)) { when (retrieveTypeModeFromIntent(intent)) {
TypeMode.DEFAULT -> { TypeMode.DEFAULT -> {

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.activities.lock package com.kunzisoft.keepass.activities.lock
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MotionEvent import android.view.MotionEvent
@@ -59,6 +60,9 @@ abstract class LockingActivity : SpecialModeActivity() {
private set private set
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (savedInstanceState != null if (savedInstanceState != null
@@ -83,8 +87,6 @@ abstract class LockingActivity : SpecialModeActivity() {
} }
mExitLock = false mExitLock = false
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -163,35 +165,6 @@ abstract class LockingActivity : SpecialModeActivity() {
sendBroadcast(Intent(LOCK_ACTION)) sendBroadcast(Intent(LOCK_ACTION))
} }
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
views.forEach {
it?.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Log.d(TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
}
}
false
}
it?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
// Log.d(TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
}
}
if (it is ViewGroup) {
for (i in 0..it.childCount) {
resetAppTimeoutWhenViewFocusedOrChanged(it.getChildAt(i))
}
}
}
}
override fun onBackPressed() { override fun onBackPressed() {
if (mTimeoutEnable) { if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) { TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
@@ -204,7 +177,7 @@ abstract class LockingActivity : SpecialModeActivity() {
companion object { companion object {
private const val TAG = "LockingActivity" const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450 const val RESULT_EXIT_LOCK = 1450
@@ -215,3 +188,28 @@ abstract class LockingActivity : SpecialModeActivity() {
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
} }
} }
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) {
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
}
}
false
}
setOnFocusChangeListener { _, _ ->
//Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
}
if (this is ViewGroup) {
for (i in 0..childCount) {
getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context)
}
}
}

View File

@@ -18,7 +18,7 @@ import com.kunzisoft.keepass.view.SpecialModeView
abstract class SpecialModeActivity : StylishActivity() { abstract class SpecialModeActivity : StylishActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
protected var mTypeMode: TypeMode = TypeMode.DEFAULT private var mTypeMode: TypeMode = TypeMode.DEFAULT
private var mSpecialModeView: SpecialModeView? = null private var mSpecialModeView: SpecialModeView? = null

View File

@@ -34,7 +34,7 @@ class App : MultiDexApplication() {
} }
override fun onTerminate() { override fun onTerminate() {
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this)) Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
super.onTerminate() super.onTerminate()
} }
} }

View File

@@ -47,7 +47,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri), UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
fileDatabaseInfo.exists, fileDatabaseInfo.exists,
fileDatabaseInfo.getModificationString(), fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString() fileDatabaseInfo.getSizeString()
) )
}, },
@@ -90,7 +90,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri), UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.exists, fileDatabaseInfo.exists,
fileDatabaseInfo.getModificationString(), fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString() fileDatabaseInfo.getSizeString()
) )
) )
@@ -152,7 +152,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
UriUtil.decode(fileDatabaseHistory.databaseUri), UriUtil.decode(fileDatabaseHistory.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.exists, fileDatabaseInfo.exists,
fileDatabaseInfo.getModificationString(), fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString() fileDatabaseInfo.getSizeString()
) )
} }

View File

@@ -34,7 +34,12 @@ class IOActionTask<T>(
mainScope.launch { mainScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val asyncResult: Deferred<T?> = async { val asyncResult: Deferred<T?> = async {
action.invoke() try {
action.invoke()
} catch (e: Exception) {
e.printStackTrace()
null
}
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
afterActionDatabaseListener?.invoke(asyncResult.await()) afterActionDatabaseListener?.invoke(asyncResult.await())

View File

@@ -0,0 +1,7 @@
package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
import android.view.inputmethod.InlineSuggestionsRequest
data class AutofillComponent(val assistStructure: AssistStructure,
val inlineSuggestionsRequest: InlineSuggestionsRequest?)

View File

@@ -19,18 +19,27 @@
*/ */
package com.kunzisoft.keepass.autofill package com.kunzisoft.keepass.autofill
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.PendingIntent
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.service.autofill.Dataset import android.service.autofill.Dataset
import android.service.autofill.FillResponse import android.service.autofill.FillResponse
import android.service.autofill.InlinePresentation
import android.util.Log import android.util.Log
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
@@ -38,8 +47,11 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.createIconFromDatabaseIcon
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
@@ -47,11 +59,17 @@ object AutofillHelper {
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165 private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
fun retrieveAssistStructure(intent: Intent?): AssistStructure? { fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
intent?.let { intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
return it.getParcelableExtra(ASSIST_STRUCTURE) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AutofillComponent(assistStructure,
intent.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST))
} else {
AutofillComponent(assistStructure, null)
}
} }
return null return null
} }
@@ -68,26 +86,10 @@ object AutofillHelper {
return "" return ""
} }
internal fun addHeader(responseBuilder: FillResponse.Builder, private fun buildDataset(context: Context,
packageName: String,
webDomain: String?,
applicationId: String?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (webDomain != null) {
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
setTextViewText(R.id.autofill_web_domain_text, webDomain)
})
} else if (applicationId != null) {
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
setTextViewText(R.id.autofill_app_id_text, applicationId)
})
}
}
}
internal fun buildDataset(context: Context,
entryInfo: EntryInfo, entryInfo: EntryInfo,
struct: StructureParser.Result): Dataset? { struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? {
val title = makeEntryTitle(entryInfo) val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, title, entryInfo.icon) val views = newRemoteViews(context, title, entryInfo.icon)
val builder = Dataset.Builder(views) val builder = Dataset.Builder(views)
@@ -100,6 +102,12 @@ object AutofillHelper {
builder.setValue(password, AutofillValue.forText(entryInfo.password)) builder.setValue(password, AutofillValue.forText(entryInfo.password))
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
return try { return try {
builder.build() builder.build()
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
@@ -108,44 +116,120 @@ object AutofillHelper {
} }
} }
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context,
inlineSuggestionsRequest: InlineSuggestionsRequest,
positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion-1
&& inlinePresentationSpecs.size > positionItem) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null
// Build the content for IME UI
val pendingIntent = PendingIntent.getActivity(context,
0,
Intent(context, AutofillSettingsActivity::class.java),
0)
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
setTitle(entryInfo.title)
setSubtitle(entryInfo.username)
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
buildIconFromEntry(context, entryInfo)?.let { icon ->
setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST)
})
}
}.build().slice, inlinePresentationSpec, false)
}
return null
}
fun buildResponse(context: Context,
entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse {
val responseBuilder = FillResponse.Builder()
// Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val packageName = context.packageName
parseResult.webDomain?.let { webDomain ->
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
setTextViewText(R.id.autofill_web_domain_text, webDomain)
})
} ?: kotlin.run {
parseResult.applicationId?.let { applicationId ->
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
setTextViewText(R.id.autofill_app_id_text, applicationId)
})
}
}
}
// Add inline suggestion for new IME and dataset
entriesInfo.forEachIndexed { index, entryInfo ->
val inlinePresentation = inlineSuggestionsRequest?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
buildInlinePresentationForEntry(context, inlineSuggestionsRequest, index, entryInfo)
} else {
null
}
}
responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation))
}
return responseBuilder.build()
}
/** /**
* Build the Autofill response for one entry * Build the Autofill response for one entry
*/ */
fun buildResponse(activity: Activity, entryInfo: EntryInfo) { fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) {
buildResponse(activity, ArrayList<EntryInfo>().apply { add(entryInfo) }) buildResponseAndSetResult(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
} }
/** /**
* Build the Autofill response for many entry * Build the Autofill response for many entry
*/ */
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) { fun buildResponseAndSetResult(activity: Activity, entriesInfo: List<EntryInfo>) {
if (entriesInfo.isEmpty()) { if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED) activity.setResult(Activity.RESULT_CANCELED)
} else { } else {
var setResultOk = false var setResultOk = false
activity.intent?.extras?.let { extras -> activity.intent?.getParcelableExtra<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
if (extras.containsKey(ASSIST_STRUCTURE)) { StructureParser(structure).parse()?.let { result ->
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure -> // New Response
StructureParser(structure).parse()?.let { result -> val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// New Response val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
val responseBuilder = FillResponse.Builder() if (inlineSuggestionsRequest != null) {
entriesInfo.forEach { Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
responseBuilder.addDataset(buildDataset(activity, it, result))
}
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
responseBuilder.build())
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
} }
buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
} else {
buildResponse(activity, entriesInfo, result, null)
} }
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
response)
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
} }
if (!setResultOk) { }
Log.w(activity.javaClass.name, "Failed Autofill auth.") if (!setResultOk) {
activity.setResult(Activity.RESULT_CANCELED) Log.w(activity.javaClass.name, "Failed Autofill auth.")
} activity.setResult(Activity.RESULT_CANCELED)
} }
} }
} }
@@ -155,10 +239,16 @@ object AutofillHelper {
*/ */
fun startActivityForAutofillResult(activity: Activity, fun startActivityForAutofillResult(activity: Activity,
intent: Intent, intent: Intent,
assistStructure: AssistStructure, autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION) EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
intent.putExtra(ASSIST_STRUCTURE, assistStructure) intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
autofillComponent.inlineSuggestionsRequest?.let {
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo) EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE) activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
} }
@@ -192,4 +282,11 @@ object AutofillHelper {
} }
return presentation return presentation
} }
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
return createIconFromDatabaseIcon(context,
Database.getInstance().drawFactory,
entryInfo.icon,
ContextCompat.getColor(context, R.color.green))
}
} }

View File

@@ -19,37 +19,50 @@
*/ */
package com.kunzisoft.keepass.autofill package com.kunzisoft.keepass.autofill
import android.app.PendingIntent
import android.content.Intent
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.os.CancellationSignal import android.os.CancellationSignal
import android.service.autofill.* import android.service.autofill.*
import android.util.Log import android.util.Log
import android.view.autofill.AutofillId import android.view.autofill.AutofillId
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() { class KeeAutofillService : AutofillService() {
var applicationIdBlocklist: Set<String>? = null var applicationIdBlocklist: Set<String>? = null
var webDomainBlocklist: Set<String>? = null var webDomainBlocklist: Set<String>? = null
var askToSaveData: Boolean = false var askToSaveData: Boolean = false
var autofillInlineSuggestionsEnabled: Boolean = false
private var mLock = AtomicBoolean() private var mLock = AtomicBoolean()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
getPreferences()
}
private fun getPreferences() {
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this) applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this) webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
askToSaveData = PreferencesUtil.askToSaveAutofillData(this) // TODO apply when changed askToSaveData = PreferencesUtil.askToSaveAutofillData(this)
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
} }
override fun onFillRequest(request: FillRequest, override fun onFillRequest(request: FillRequest,
@@ -75,7 +88,16 @@ class KeeAutofillService : AutofillService() {
} }
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain -> SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain searchInfo.webDomain = webDomainWithoutSubDomain
launchSelection(searchInfo, parseResult, callback) val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
request.inlineSuggestionsRequest
} else {
null
}
launchSelection(searchInfo,
parseResult,
inlineSuggestionsRequest,
callback)
} }
} }
} }
@@ -84,39 +106,40 @@ class KeeAutofillService : AutofillService() {
private fun launchSelection(searchInfo: SearchInfo, private fun launchSelection(searchInfo: SearchInfo,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(), Database.getInstance(),
searchInfo, searchInfo,
{ items -> { items ->
val responseBuilder = FillResponse.Builder() callback.onSuccess(
AutofillHelper.addHeader(responseBuilder, packageName, AutofillHelper.buildResponse(this,
parseResult.webDomain, parseResult.applicationId) items, parseResult, inlineSuggestionsRequest)
items.forEach { )
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
}
callback.onSuccess(responseBuilder.build())
}, },
{ {
// Show UI if no search result // Show UI if no search result
showUIForEntrySelection(parseResult, searchInfo, callback) showUIForEntrySelection(parseResult,
searchInfo, inlineSuggestionsRequest, callback)
}, },
{ {
// Show UI if database not open // Show UI if database not open
showUIForEntrySelection(parseResult, searchInfo, callback) showUIForEntrySelection(parseResult,
searchInfo, inlineSuggestionsRequest, callback)
} }
) )
} }
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(parseResult: StructureParser.Result,
searchInfo: SearchInfo, searchInfo: SearchInfo,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
parseResult.allAutofillIds().let { autofillIds -> parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) { if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used // If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response. // to generate Response.
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this, val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
searchInfo) searchInfo, inlineSuggestionsRequest)
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) { val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply { RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
@@ -149,7 +172,40 @@ class KeeAutofillService : AutofillService() {
) )
} }
} }
// Build response
// Build inline presentation
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.let {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0) {
val inlinePresentationSpec = inlinePresentationSpecs[0]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) {
// Build the content for IME UI
inlinePresentation = InlinePresentation(
InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(this,
0,
Intent(this, AutofillSettingsActivity::class.java),
0)
).apply {
setContentDescription(getString(R.string.autofill_sign_in_prompt))
setTitle(getString(R.string.autofill_sign_in_prompt))
setStartIcon(Icon.createWithResource(this@KeeAutofillService, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false)
}
}
}
// Build response
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
}
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock) responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build()) callback.onSuccess(responseBuilder.build())
} }
@@ -190,6 +246,7 @@ class KeeAutofillService : AutofillService() {
override fun onConnected() { override fun onConnected() {
Log.d(TAG, "onConnected") Log.d(TAG, "onConnected")
getPreferences()
} }
override fun onDisconnected() { override fun onDisconnected() {

View File

@@ -33,7 +33,7 @@ import java.util.*
* Parse AssistStructure and guess username and password fields. * Parse AssistStructure and guess username and password fields.
*/ */
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
internal class StructureParser(private val structure: AssistStructure) { class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null private var result: Result? = null
private var usernameNeeded = true private var usernameNeeded = true
@@ -274,7 +274,7 @@ internal class StructureParser(private val structure: AssistStructure) {
} }
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
internal class Result { class Result {
var applicationId: String? = null var applicationId: String? = null
var webDomain: String? = null var webDomain: String? = null

View File

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,628 @@
/*
* 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.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.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
class AdvancedUnlockFragment: StylishFragment(), 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
/**
* Manage setting to auto open biometric prompt
*/
private var mAutoOpenPrompt: Boolean = false
get() {
return field && mAutoOpenPromptEnabled
}
// 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.DatabaseListener? = null
// 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
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)
retainInstance = true
setHasOptionsMenu(true)
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_advanced_unlock, container, false)
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
return rootView
}
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
private var activityResult: ActivityResult? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// To wait resume
if (keepConnection) {
activityResult = ActivityResult(requestCode, resultCode, data)
}
keepConnection = false
}
override fun onResume() {
super.onResume()
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext())
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())
keepConnection = false
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// biometric menu
if (mAllowAdvancedUnlockMenu)
inflater.inflate(R.menu.advanced_unlock, menu)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEncryptedDatabaseKey()
}
}
return super.onOptionsItemSelected(item)
}
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// To get device credential unlock result, only if same database uri
if (databaseUri != null
&& mAdvancedUnlockEnabled) {
activityResult?.let {
if (databaseUri == databaseFileUri) {
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
} else {
disconnect()
}
} ?: run {
this.mAutoOpenPrompt = autoOpenPrompt
connect(databaseUri)
}
} else {
disconnect()
}
activityResult = null
}
}
/**
* Check unlock availability and change the current mode depending of device's state
*/
fun checkUnlockAvailability() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true
if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|| 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(requireContext())) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
if (AdvancedUnlockManager.isDeviceSecure(requireContext())) {
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(false, null)
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openBiometricSetting() {
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
requireContext().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.no_credentials_stored)
setAdvancedUnlockedMessageView("")
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
requireContext().getString(R.string.credential_before_click_advanced_unlock_button))
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
activity?.runOnUiThread {
if (allowOpenBiometricPrompt) {
if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true
try {
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
} 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.open_advanced_unlock_prompt_store_credential)
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.open_advanced_unlock_prompt_unlock_database)
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 (mAutoOpenPrompt) {
mAutoOpenPrompt = false
openAdvancedUnlockPrompt(cryptoPrompt)
}
}
} ?: deleteEncryptedDatabaseKey()
}
} ?: throw IODatabaseException()
} ?: throw Exception("AdvancedUnlockManager not initialized")
}
@Synchronized
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.DatabaseListener {
override fun onDatabaseCleared() {
deleteEncryptedDatabaseKey()
}
}
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() {
allowOpenBiometricPrompt = false
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
checkUnlockAvailability()
}
}
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
activity?.runOnUiThread {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationFailed() {
activity?.runOnUiThread {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationSucceeded() {
activity?.runOnUiThread {
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)
}
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
}
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: String, ivSpec: String) {
databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec)
}
}
override fun handleDecryptedResult(decryptedValue: String) {
// Load database directly with password retrieve
databaseFileUri?.let {
mBuilderListener?.onCredentialDecrypted(it, decryptedValue)
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
override fun onGenericException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage)
}
private fun showViews(show: Boolean) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.visibility = if (show)
View.VISIBLE
else {
View.GONE
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedTitleView(textId: Int) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.setTitle(textId)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(textId: Int) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.setMessage(textId)
}
}
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.message = text
}
}
fun performEducation(passwordActivityEducation: PasswordActivityEducation,
readOnlyEducationPerformed: Boolean,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !readOnlyEducationPerformed) {
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
PreferencesUtil.isAdvancedUnlockEnable(requireContext())
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& mAdvancedUnlockInfoView != null && mAdvancedUnlockInfoView?.visibility == View.VISIBLE
&& mAdvancedUnlockInfoView?.unlockIconImageView != null
&& passwordActivityEducation.checkAndPerformedBiometricEducation(mAdvancedUnlockInfoView!!.unlockIconImageView!!,
onEducationViewClick,
onOuterViewClick)
}
} catch (ignored: Exception) {}
}
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(): String
fun conditionToStoreCredential(): Boolean
fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String)
fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String)
}
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,5 +1,5 @@
/* /*
* Copyright 2019 Jeremy Jamet / Kunzisoft. * Copyright 2020 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePassDX. * This file is part of KeePassDX.
* *
@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.biometric package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
@@ -31,6 +32,7 @@ import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.* import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -44,48 +46,74 @@ import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
@RequiresApi(api = Build.VERSION_CODES.M) @RequiresApi(api = Build.VERSION_CODES.M)
class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) { class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) {
private var biometricPrompt: BiometricPrompt? = null
private var keyStore: KeyStore? = null private var keyStore: KeyStore? = null
private var keyGenerator: KeyGenerator? = null private var keyGenerator: KeyGenerator? = null
private var cipher: Cipher? = null private var cipher: Cipher? = null
private var keyguardManager: KeyguardManager? = null
private var cryptoObject: BiometricPrompt.CryptoObject? = 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 var isKeyManagerInit = false
var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
var biometricUnlockCallback: BiometricUnlockCallback? = null
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(context) private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext())
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext())
val isKeyManagerInitialized: Boolean val isKeyManagerInitialized: Boolean
get() { get() {
if (!isKeyManagerInit) { if (!isKeyManagerInit) {
biometricUnlockCallback?.onBiometricException(Exception("Biometric not initialized")) advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized"))
} }
return isKeyManagerInit 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 { init {
if (allowInitKeyStore(context)) { if (isDeviceSecure(retrieveContext())
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager? && (biometricUnlockEnable || deviceCredentialUnlockEnable)) {
try { try {
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE) this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE) this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE)
this.cipher = Cipher.getInstance( this.cipher = Cipher.getInstance(
BIOMETRIC_KEY_ALGORITHM + "/" ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
+ BIOMETRIC_BLOCKS_MODES + "/" + ADVANCED_UNLOCK_BLOCKS_MODES + "/"
+ BIOMETRIC_ENCRYPTION_PADDING) + ADVANCED_UNLOCK_ENCRYPTION_PADDING)
this.cryptoObject = BiometricPrompt.CryptoObject(cipher!!)
isKeyManagerInit = (keyStore != null isKeyManagerInit = (keyStore != null
&& keyGenerator != null && keyGenerator != null
&& cipher != null) && cipher != null)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to initialize the keystore", e) Log.e(TAG, "Unable to initialize the keystore", e)
isKeyManagerInit = false isKeyManagerInit = false
biometricUnlockCallback?.onBiometricException(e) advancedUnlockCallback?.onGenericException(e)
} }
} else { } else {
// really not much to do when no fingerprint support found // really not much to do when no fingerprint support found
@@ -103,22 +131,20 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
keyStore.load(null) keyStore.load(null)
try { try {
if (!keyStore.containsAlias(BIOMETRIC_KEYSTORE_KEY)) { if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
// Set the alias of the entry in Android KeyStore where the key will appear // Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder // and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init( keyGenerator?.init(
KeyGenParameterSpec.Builder( KeyGenParameterSpec.Builder(
BIOMETRIC_KEYSTORE_KEY, ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
// Require the user to authenticate with a fingerprint to authorize every use // Require the user to authenticate with a fingerprint to authorize every use
// of the key // of the key, don't use it for device credential because it's the user authentication
.setUserAuthenticationRequired(true)
.apply { .apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R if (biometricUnlockEnable) {
&& deviceCredentialUnlockEnable) { setUserAuthenticationRequired(true)
setUserAuthenticationParameters(0, KeyProperties.AUTH_DEVICE_CREDENTIAL)
} }
} }
.build()) .build())
@@ -126,56 +152,46 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to create a key in keystore", e) Log.e(TAG, "Unable to create a key in keystore", e)
biometricUnlockCallback?.onBiometricException(e) advancedUnlockCallback?.onGenericException(e)
} }
return keyStore.getKey(BIOMETRIC_KEYSTORE_KEY, null) as SecretKey? return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to retrieve the key in keystore", e) Log.e(TAG, "Unable to retrieve the key in keystore", e)
biometricUnlockCallback?.onBiometricException(e) advancedUnlockCallback?.onGenericException(e)
} }
return null return null
} }
fun initEncryptData(actionIfCypherInit fun initEncryptData(actionIfCypherInit
: (biometricPrompt: BiometricPrompt?, : (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
try { try {
// TODO if (keyguardManager?.isDeviceSecure == true) {
getSecretKey()?.let { secretKey -> getSecretKey()?.let { secretKey ->
cipher?.init(Cipher.ENCRYPT_MODE, secretKey) cipher?.let { cipher ->
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
initBiometricPrompt() actionIfCypherInit.invoke(
AdvancedUnlockCryptoPrompt(
val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply { cipher,
setTitle(context.getString(R.string.advanced_unlock_prompt_store_credential_title)) R.string.advanced_unlock_prompt_store_credential_title,
setDescription(context.getString(R.string.advanced_unlock_prompt_store_credential_message)) R.string.advanced_unlock_prompt_store_credential_message,
setConfirmationRequired(true) isDeviceCredentialOperation(), isBiometricOperation())
if (deviceCredentialUnlockEnable) { )
setAllowedAuthenticators(DEVICE_CREDENTIAL) }
} else {
setNegativeButtonText(context.getString(android.R.string.cancel))
}
}.build()
actionIfCypherInit.invoke(biometricPrompt,
cryptoObject,
promptInfoStoreCredential)
} }
} catch (unrecoverableKeyException: UnrecoverableKeyException) { } catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException) Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
biometricUnlockCallback?.onInvalidKeyException(unrecoverableKeyException) advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) { } catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException) Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException) advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e) Log.e(TAG, "Unable to initialize encrypt data", e)
biometricUnlockCallback?.onBiometricException(e) advancedUnlockCallback?.onGenericException(e)
} }
} }
@@ -190,57 +206,46 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
// passes updated iv spec on to callback so this can be stored for decryption // passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec -> cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP) val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
biometricUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue) advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
} }
} catch (e: Exception) { } catch (e: Exception) {
val exception = Exception(context.getString(R.string.keystore_not_accessible), e)
Log.e(TAG, "Unable to encrypt data", e) Log.e(TAG, "Unable to encrypt data", e)
biometricUnlockCallback?.onBiometricException(exception) advancedUnlockCallback?.onGenericException(e)
} }
} }
fun initDecryptData(ivSpecValue: String, actionIfCypherInit fun initDecryptData(ivSpecValue: String, actionIfCypherInit
: (biometricPrompt: BiometricPrompt?, : (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
try { try {
// TODO if (keyguardManager?.isDeviceSecure == true) {
// important to restore spec here that was used for decryption // important to restore spec here that was used for decryption
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP) val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
val spec = IvParameterSpec(iv) val spec = IvParameterSpec(iv)
getSecretKey()?.let { secretKey -> getSecretKey()?.let { secretKey ->
cipher?.init(Cipher.DECRYPT_MODE, secretKey, spec) cipher?.let { cipher ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
initBiometricPrompt() actionIfCypherInit.invoke(
AdvancedUnlockCryptoPrompt(
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply { cipher,
setTitle(context.getString(R.string.advanced_unlock_prompt_extract_credential_title)) R.string.advanced_unlock_prompt_extract_credential_title,
//setDescription(context.getString(R.string.biometric_prompt_extract_credential_message)) null,
setConfirmationRequired(false) isDeviceCredentialOperation(), isBiometricOperation())
if (deviceCredentialUnlockEnable) { )
setAllowedAuthenticators(DEVICE_CREDENTIAL) }
} else {
setNegativeButtonText(context.getString(android.R.string.cancel))
}
}.build()
actionIfCypherInit.invoke(biometricPrompt,
cryptoObject,
promptInfoExtractCredential)
} }
} catch (unrecoverableKeyException: UnrecoverableKeyException) { } catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException) Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
deleteKeystoreKey() deleteKeystoreKey()
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) { } catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException) Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException) advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e) Log.e(TAG, "Unable to initialize decrypt data", e)
biometricUnlockCallback?.onBiometricException(e) advancedUnlockCallback?.onGenericException(e)
} }
} }
@@ -252,33 +257,73 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
// actual decryption here // actual decryption here
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP) val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
cipher?.doFinal(encrypted)?.let { decrypted -> cipher?.doFinal(encrypted)?.let { decrypted ->
biometricUnlockCallback?.handleDecryptedResult(String(decrypted)) advancedUnlockCallback?.handleDecryptedResult(String(decrypted))
} }
} catch (badPaddingException: BadPaddingException) { } catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException) Log.e(TAG, "Unable to decrypt data", badPaddingException)
biometricUnlockCallback?.onInvalidKeyException(badPaddingException) advancedUnlockCallback?.onInvalidKeyException(badPaddingException)
} catch (e: Exception) { } catch (e: Exception) {
val exception = Exception(context.getString(R.string.keystore_not_accessible), e) Log.e(TAG, "Unable to decrypt data", e)
Log.e(TAG, "Unable to decrypt data", exception) advancedUnlockCallback?.onGenericException(e)
biometricUnlockCallback?.onBiometricException(exception)
} }
} }
fun deleteKeystoreKey() { fun deleteKeystoreKey() {
try { try {
keyStore?.load(null) keyStore?.load(null)
keyStore?.deleteEntry(BIOMETRIC_KEYSTORE_KEY) keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to delete entry key in keystore", e) Log.e(TAG, "Unable to delete entry key in keystore", e)
biometricUnlockCallback?.onBiometricException(e) advancedUnlockCallback?.onGenericException(e)
}
}
@Suppress("DEPRECATION")
@Synchronized
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
// 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)
retrieveContext().startActivityForResult(
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
REQUEST_DEVICE_CREDENTIAL)
} }
} }
@Synchronized @Synchronized
fun initBiometricPrompt() { fun onActivityResult(requestCode: Int, resultCode: Int) {
if (biometricPrompt == null) { if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
authenticationCallback?.let { if (resultCode == Activity.RESULT_OK) {
biometricPrompt = BiometricPrompt(context, Executors.newSingleThreadExecutor(), it) advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockCallback?.onAuthenticationFailed()
} }
} }
} }
@@ -287,25 +332,30 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
biometricPrompt?.cancelAuthentication() biometricPrompt?.cancelAuthentication()
} }
interface BiometricUnlockErrorCallback { interface AdvancedUnlockErrorCallback {
fun onInvalidKeyException(e: Exception) fun onInvalidKeyException(e: Exception)
fun onBiometricException(e: Exception) fun onGenericException(e: Exception)
} }
interface BiometricUnlockCallback : BiometricUnlockErrorCallback { interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback {
fun onAuthenticationSucceeded()
fun onAuthenticationFailed()
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
fun handleEncryptedResult(encryptedValue: String, ivSpec: String) fun handleEncryptedResult(encryptedValue: String, ivSpec: String)
fun handleDecryptedResult(decryptedValue: String) fun handleDecryptedResult(decryptedValue: String)
} }
companion object { companion object {
private val TAG = BiometricUnlockDatabaseHelper::class.java.name private val TAG = AdvancedUnlockManager::class.java.name
private const val BIOMETRIC_KEYSTORE = "AndroidKeyStore" private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
private const val BIOMETRIC_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key" private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
private const val BIOMETRIC_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val REQUEST_DEVICE_CREDENTIAL = 556
@RequiresApi(api = Build.VERSION_CODES.M) @RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int { fun canAuthenticate(context: Context): Int {
@@ -337,11 +387,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
} }
@RequiresApi(api = Build.VERSION_CODES.M) @RequiresApi(api = Build.VERSION_CODES.M)
fun allowInitKeyStore(context: Context): Boolean { fun isDeviceSecure(context: Context): Boolean {
val biometricCanAuthenticate = canAuthenticate(context) val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
return ( biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS return keyguardManager?.isDeviceSecure ?: false
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
)
} }
@RequiresApi(api = Build.VERSION_CODES.M) @RequiresApi(api = Build.VERSION_CODES.M)
@@ -365,36 +413,48 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
) )
} }
@RequiresApi(api = Build.VERSION_CODES.R) @RequiresApi(api = Build.VERSION_CODES.M)
fun deviceCredentialUnlockSupported(context: Context): Boolean { fun deviceCredentialUnlockSupported(context: Context): Boolean {
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE || biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
) || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ContextCompat.getSystemService(context, KeyguardManager::class.java)?.apply {
return isDeviceSecure
}
}
return false
} }
/** /**
* Remove entry key in keystore * Remove entry key in keystore
*/ */
@RequiresApi(api = Build.VERSION_CODES.M) @RequiresApi(api = Build.VERSION_CODES.M)
fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity, fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
biometricCallback: BiometricUnlockErrorCallback) { advancedCallback: AdvancedUnlockErrorCallback) {
BiometricUnlockDatabaseHelper(context).apply { AdvancedUnlockManager{ fragmentActivity }.apply {
biometricUnlockCallback = object : BiometricUnlockCallback { advancedUnlockCallback = object : AdvancedUnlockCallback {
override fun onAuthenticationSucceeded() {}
override fun onAuthenticationFailed() {}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {} override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
override fun handleDecryptedResult(decryptedValue: String) {} override fun handleDecryptedResult(decryptedValue: String) {}
override fun onInvalidKeyException(e: Exception) { override fun onInvalidKeyException(e: Exception) {
biometricCallback.onInvalidKeyException(e) advancedCallback.onInvalidKeyException(e)
} }
override fun onBiometricException(e: Exception) { override fun onGenericException(e: Exception) {
biometricCallback.onBiometricException(e) advancedCallback.onGenericException(e)
} }
} }
deleteKeystoreKey() deleteKeystoreKey()

View File

@@ -1,422 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.biometric
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
@RequiresApi(api = Build.VERSION_CODES.M)
class AdvancedUnlockedManager(var context: FragmentActivity,
var databaseFileUri: Uri,
private var advancedUnlockInfoView: AdvancedUnlockInfoView?,
private var checkboxPasswordView: CompoundButton?,
private var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
var passwordView: TextView?,
private var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
private var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
private var mAddBiometricMenuInProgress = false
/**
* Manage setting to auto open biometric prompt
*/
private var biometricPromptAutoOpenPreference = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
var isBiometricPromptAutoOpenEnable: Boolean = false
get() {
return field && biometricPromptAutoOpenPreference
}
// 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 var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
private val cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
override fun onDatabaseCleared() {
deleteEncryptedDatabaseKey()
}
}
init {
// Add a check listener to change fingerprint mode
checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
checkBiometricAvailability()
// Add old listener to enable the button, only be call here because of onCheckedChange bug
onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked)
}
cipherDatabaseAction.apply {
reloadPreferences()
registerDatabaseListener(cipherDatabaseListener)
}
}
/**
* Check biometric availability and change the current mode depending of device's state
*/
fun checkBiometricAvailability() {
if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
advancedUnlockInfoView?.setIconResource(R.drawable.bolt)
} else if (PreferencesUtil.isBiometricUnlockEnable(context)) {
advancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
}
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(context)
allowOpenBiometricPrompt = true
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.BIOMETRIC_NOT_CONFIGURED)
} else {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context)
// callback for fingerprint findings
biometricUnlockDatabaseHelper?.biometricUnlockCallback = this
biometricUnlockDatabaseHelper?.authenticationCallback = biometricAuthenticationCallback
}
// Recheck to change the mode
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
} else {
if (checkboxPasswordView?.isChecked == true) {
// listen for encryption
toggleMode(Mode.STORE_CREDENTIAL)
} else {
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { 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
})
}
}
}
}
}
}
private fun toggleMode(newBiometricMode: Mode) {
if (newBiometricMode != biometricMode) {
biometricMode = newBiometricMode
initAdvancedUnlockMode()
}
}
private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback () {
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence) {
context.runOnUiThread {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
}
override fun onAuthenticationFailed() {
context.runOnUiThread {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
}
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
context.runOnUiThread {
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> {
}
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
}
Mode.BIOMETRIC_NOT_CONFIGURED -> {
}
Mode.KEY_MANAGER_UNAVAILABLE -> {
}
Mode.WAIT_CREDENTIAL -> {
}
Mode.STORE_CREDENTIAL -> {
// newly store the entered password in encrypted way
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
AdvancedUnlockNotificationService.startServiceForTimeout(context)
}
Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences
cipherDatabaseAction.getCipherDatabase(databaseFileUri) { cipherDatabase ->
cipherDatabase?.encryptedValue?.let { value ->
biometricUnlockDatabaseHelper?.decryptData(value)
} ?: deleteEncryptedDatabaseKey()
}
}
}
}
}
}
private fun initNotAvailable() {
showFingerPrintViews(false)
advancedUnlockInfoView?.setIconViewClickListener(false, null)
}
@Suppress("DEPRECATION")
private fun openBiometricSetting() {
advancedUnlockInfoView?.setIconViewClickListener(false) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
context.startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
private fun initSecurityUpdateRequired() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
openBiometricSetting()
}
private fun initNotConfigured() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.configure_biometric)
setAdvancedUnlockedMessageView("")
openBiometricSetting()
}
private fun initKeyManagerNotAvailable() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
openBiometricSetting()
}
private fun initWaitData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(false) {
biometricAuthenticationCallback.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
context.getString(R.string.credential_before_click_advanced_unlock_button))
}
}
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo) {
context.runOnUiThread {
if (allowOpenBiometricPrompt) {
if (biometricPrompt != null) {
if (cryptoObject != null) {
biometricPrompt.authenticate(promptInfo, cryptoObject)
} else {
setAdvancedUnlockedTitleView(R.string.crypto_object_not_initialized)
}
} else {
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
}
}
}
}
private fun initEncryptData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
setAdvancedUnlockedMessageView("")
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
// Set listener to open the biometric dialog and save credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
}
}
}
private fun initDecryptData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
setAdvancedUnlockedMessageView("")
if (biometricUnlockDatabaseHelper != null) {
cipherDatabaseAction.getCipherDatabase(databaseFileUri) { cipherDatabase ->
cipherDatabase?.let {
biometricUnlockDatabaseHelper?.initDecryptData(it.specParameters) { biometricPrompt, cryptoObject, promptInfo ->
// Set listener to open the biometric dialog and check credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
}
// Auto open the biometric prompt
if (isBiometricPromptAutoOpenEnable) {
isBiometricPromptAutoOpenEnable = false
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
}
}
} ?: deleteEncryptedDatabaseKey()
}
}
}
@Synchronized
fun initAdvancedUnlockMode() {
mAllowAdvancedUnlockMenu = false
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
Mode.WAIT_CREDENTIAL -> initWaitData()
Mode.STORE_CREDENTIAL -> initEncryptData()
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
}
invalidateBiometricMenu()
}
private fun invalidateBiometricMenu() {
// Show fingerprint key deletion
if (!mAddBiometricMenuInProgress) {
mAddBiometricMenuInProgress = true
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
mAllowAdvancedUnlockMenu = containsCipher
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
mAddBiometricMenuInProgress = false
context.invalidateOptionsMenu()
}
}
}
fun destroy() {
// Close the biometric prompt
allowOpenBiometricPrompt = false
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
// Restore the checked listener
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
cipherDatabaseAction.unregisterDatabaseListener(cipherDatabaseListener)
}
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
if (mAllowAdvancedUnlockMenu)
menuInflater.inflate(R.menu.advanced_unlock, menu)
}
fun deleteEncryptedDatabaseKey() {
allowOpenBiometricPrompt = false
advancedUnlockInfoView?.setIconViewClickListener(false, null)
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri) {
checkBiometricAvailability()
}
}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
}
override fun handleDecryptedResult(decryptedValue: String) {
// Load database directly with password retrieve
loadDatabaseAfterRetrieveCredentials.invoke(decryptedValue)
}
override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
override fun onBiometricException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage)
}
private fun showFingerPrintViews(show: Boolean) {
context.runOnUiThread {
advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE
}
}
private fun setAdvancedUnlockedTitleView(textId: Int) {
context.runOnUiThread {
advancedUnlockInfoView?.setTitle(textId)
}
}
private fun setAdvancedUnlockedMessageView(textId: Int) {
context.runOnUiThread {
advancedUnlockInfoView?.setMessage(textId)
}
}
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
context.runOnUiThread {
advancedUnlockInfoView?.message = text
}
}
enum class Mode {
BIOMETRIC_UNAVAILABLE,
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
BIOMETRIC_NOT_CONFIGURED,
KEY_MANAGER_UNAVAILABLE,
WAIT_CREDENTIAL,
STORE_CREDENTIAL,
EXTRACT_CREDENTIAL
}
companion object {
private val TAG = AdvancedUnlockedManager::class.java.name
}
}

View File

@@ -29,11 +29,12 @@ object StreamCipherFactory {
private val SALSA_IV = byteArrayOf(0xE8.toByte(), 0x30, 0x09, 0x4B, 0x97.toByte(), 0x20, 0x5D, 0x2A) private val SALSA_IV = byteArrayOf(0xE8.toByte(), 0x30, 0x09, 0x4B, 0x97.toByte(), 0x20, 0x5D, 0x2A)
fun getInstance(alg: CrsAlgorithm?, key: ByteArray): StreamCipher? { @Throws(Exception::class)
fun getInstance(alg: CrsAlgorithm?, key: ByteArray): StreamCipher {
return when { return when {
alg === CrsAlgorithm.Salsa20 -> getSalsa20(key) alg === CrsAlgorithm.Salsa20 -> getSalsa20(key)
alg === CrsAlgorithm.ChaCha20 -> getChaCha20(key) alg === CrsAlgorithm.ChaCha20 -> getChaCha20(key)
else -> null else -> throw Exception("Invalid random cipher")
} }
} }

View File

@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.closeDatabase
class CreateDatabaseRunnable(context: Context, class CreateDatabaseRunnable(context: Context,
private val mDatabase: Database, private val mDatabase: Database,
@@ -47,7 +46,7 @@ class CreateDatabaseRunnable(context: Context,
createData(mDatabaseUri, databaseName, rootName) createData(mDatabaseUri, databaseName, rootName)
} }
} catch (e: Exception) { } catch (e: Exception) {
mDatabase.closeAndClear(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
setError(e) setError(e)
} }

View File

@@ -31,7 +31,6 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.closeDatabase
class LoadDatabaseRunnable(private val context: Context, class LoadDatabaseRunnable(private val context: Context,
private val mDatabase: Database, private val mDatabase: Database,
@@ -47,7 +46,7 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onStartRun() { override fun onStartRun() {
// Clear before we load // Clear before we load
mDatabase.closeAndClear(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
} }
override fun onActionRun() { override fun onActionRun() {
@@ -59,9 +58,6 @@ class LoadDatabaseRunnable(private val context: Context,
mFixDuplicateUUID, mFixDuplicateUUID,
progressTaskUpdater) progressTaskUpdater)
} }
catch (e: DuplicateUuidDatabaseException) {
setError(e)
}
catch (e: LoadDatabaseException) { catch (e: LoadDatabaseException) {
setError(e) setError(e)
} }
@@ -83,7 +79,7 @@ class LoadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer // Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context) PreferencesUtil.saveCurrentTime(context)
} else { } else {
mDatabase.closeAndClear(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
} }
} }

View File

@@ -26,6 +26,8 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
@@ -35,6 +37,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
@@ -44,6 +47,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
@@ -84,6 +88,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
private var serviceConnection: ServiceConnection? = null private var serviceConnection: ServiceConnection? = null
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener { private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) { override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
@@ -101,6 +106,28 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
} }
} }
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
override fun validateDatabaseChanged() {
mBinder?.getService()?.saveDatabaseInfo()
}
}
private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo) {
if (databaseChangedDialogFragment == null) {
databaseChangedDialogFragment = activity.supportFragmentManager
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
}
if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(previousDatabaseInfo, newDatabaseInfo)
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
databaseChangedDialogFragment?.show(activity.supportFragmentManager, DATABASE_CHANGED_DIALOG_TAG)
}
}
}
private fun startDialog(titleId: Int? = null, private fun startDialog(titleId: Int? = null,
messageId: Int? = null, messageId: Int? = null,
warningId: Int? = null) { warningId: Int? = null) {
@@ -140,11 +167,14 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply { mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addActionTaskListener(actionTaskListener) addActionTaskListener(actionTaskListener)
addDatabaseFileInfoListener(databaseInfoListener)
getService().checkAction() getService().checkAction()
getService().checkDatabaseInfo()
} }
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeActionTaskListener(actionTaskListener) mBinder?.removeActionTaskListener(actionTaskListener)
mBinder = null mBinder = null
} }
@@ -206,6 +236,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
fun unregisterProgressTask() { fun unregisterProgressTask() {
stopDialog() stopDialog()
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeActionTaskListener(actionTaskListener) mBinder?.removeActionTaskListener(actionTaskListener)
mBinder = null mBinder = null
@@ -264,6 +295,13 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
, ACTION_DATABASE_LOAD_TASK) , ACTION_DATABASE_LOAD_TASK)
} }
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
}
, ACTION_DATABASE_RELOAD_TASK)
}
fun startDatabaseAssignPassword(databaseUri: Uri, fun startDatabaseAssignPassword(databaseUri: Uri,
masterPasswordChecked: Boolean, masterPasswordChecked: Boolean,
masterPassword: String?, masterPassword: String?,

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.UriUtil
class ReloadDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
private val progressTaskUpdater: ProgressTaskUpdater?,
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() {
override fun onStartRun() {
// Clear before we load
mDatabase.clear(UriUtil.getBinaryDir(context))
}
override fun onActionRun() {
try {
mDatabase.reloadData(context.contentResolver,
UriUtil.getBinaryDir(context),
progressTaskUpdater)
}
catch (e: LoadDatabaseException) {
setError(e)
}
if (result.isSuccess) {
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
}
}
override fun onFinishRun() {
mLoadDatabaseResult?.invoke(result)
}
}

View File

@@ -31,10 +31,7 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
@@ -330,29 +327,11 @@ class Database {
} }
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
fun loadData(uri: Uri, password: String?, keyfile: Uri?, private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
readOnly: Boolean, openDatabaseKDB: (InputStream) -> DatabaseKDB,
contentResolver: ContentResolver, openDatabaseKDBX: (InputStream) -> DatabaseKDBX) {
cacheDirectory: File,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
this.fileUri = uri
isReadOnly = readOnly
if (uri.scheme == "file") {
val file = File(uri.path!!)
isReadOnly = !file.canWrite()
}
// Pass KeyFile Uri as InputStreams
var databaseInputStream: InputStream? = null var databaseInputStream: InputStream? = null
var keyFileInputStream: InputStream? = null
try { try {
// Get keyFile inputStream
keyfile?.let {
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
}
// Load Data, pass Uris as InputStreams // Load Data, pass Uris as InputStreams
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri) val databaseStream = UriUtil.getUriInputStream(contentResolver, uri)
?: throw IOException("Database input stream cannot be retrieve") ?: throw IOException("Database input stream cannot be retrieve")
@@ -374,22 +353,10 @@ class Database {
when { when {
// Header of database KDB // Header of database KDB
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(DatabaseInputKDB( DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(openDatabaseKDB(databaseInputStream))
cacheDirectory,
fixDuplicateUUID)
.openDatabase(databaseInputStream,
password,
keyFileInputStream,
progressTaskUpdater))
// Header of database KDBX // Header of database KDBX
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(DatabaseInputKDBX( DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(openDatabaseKDBX(databaseInputStream))
cacheDirectory,
fixDuplicateUUID)
.openDatabase(databaseInputStream,
password,
keyFileInputStream,
progressTaskUpdater))
// Header not recognized // Header not recognized
else -> throw SignatureDatabaseException() else -> throw SignatureDatabaseException()
@@ -397,14 +364,90 @@ class Database {
this.mSearchHelper = SearchHelper() this.mSearchHelper = SearchHelper()
loaded = true loaded = true
} catch (e: LoadDatabaseException) {
throw e
} finally {
databaseInputStream?.close()
}
}
@Throws(LoadDatabaseException::class)
fun loadData(uri: Uri, password: String?, keyfile: Uri?,
readOnly: Boolean,
contentResolver: ContentResolver,
cacheDirectory: File,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
// Save database URI
this.fileUri = uri
// Check if the file is writable
this.isReadOnly = readOnly
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
try {
// Get keyFile inputStream
keyfile?.let {
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
}
// Read database stream for the first time
readDatabaseStream(contentResolver, uri,
{ databaseInputStream ->
DatabaseInputKDB(cacheDirectory)
.openDatabase(databaseInputStream,
password,
keyFileInputStream,
progressTaskUpdater,
fixDuplicateUUID)
},
{ databaseInputStream ->
DatabaseInputKDBX(cacheDirectory)
.openDatabase(databaseInputStream,
password,
keyFileInputStream,
progressTaskUpdater,
fixDuplicateUUID)
}
)
} catch (e: FileNotFoundException) {
Log.e(TAG, "Unable to load keyfile", e)
throw FileNotFoundDatabaseException()
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
throw FileNotFoundDatabaseException() throw LoadDatabaseException(e)
} finally { } finally {
keyFileInputStream?.close() keyFileInputStream?.close()
databaseInputStream?.close() }
}
@Throws(LoadDatabaseException::class)
fun reloadData(contentResolver: ContentResolver,
cacheDirectory: File,
progressTaskUpdater: ProgressTaskUpdater?) {
// Retrieve the stream from the old database URI
fileUri?.let { oldDatabaseUri ->
readDatabaseStream(contentResolver, oldDatabaseUri,
{ databaseInputStream ->
DatabaseInputKDB(cacheDirectory)
.openDatabase(databaseInputStream,
masterKey,
progressTaskUpdater)
},
{ databaseInputStream ->
DatabaseInputKDBX(cacheDirectory)
.openDatabase(databaseInputStream,
masterKey,
progressTaskUpdater)
}
)
} ?: run {
Log.e(TAG, "Database URI is null, database cannot be reloaded")
throw IODatabaseException()
} }
} }
@@ -426,7 +469,7 @@ class Database {
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper?.createVirtualGroupWithSearchResult(this,
searchInfoString, SearchParameters().apply { searchInfoString, SearchParameters().apply {
searchInTitles = false searchInTitles = true
searchInUserNames = false searchInUserNames = false
searchInPasswords = false searchInPasswords = false
searchInUrls = true searchInUrls = true
@@ -531,7 +574,7 @@ class Database {
this.fileUri = uri this.fileUri = uri
} }
fun closeAndClear(filesDirectory: File? = null) { fun clear(filesDirectory: File? = null) {
drawFactory.clearCache() drawFactory.clearCache()
// Delete the cache of the database if present // Delete the cache of the database if present
mDatabaseKDB?.clearCache() mDatabaseKDB?.clearCache()
@@ -544,7 +587,10 @@ class Database {
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to clear the directory cache.", e) Log.e(TAG, "Unable to clear the directory cache.", e)
} }
}
fun clearAndClose(filesDirectory: File? = null) {
clear(filesDirectory)
this.mDatabaseKDB = null this.mDatabaseKDB = null
this.mDatabaseKDBX = null this.mDatabaseKDBX = null
this.fileUri = null this.fileUri = null

View File

@@ -426,6 +426,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryInfo.icon = icon entryInfo.icon = icon
entryInfo.username = username entryInfo.username = username
entryInfo.password = password entryInfo.password = password
entryInfo.creationTime = creationTime
entryInfo.modificationTime = lastModificationTime
entryInfo.expires = expires entryInfo.expires = expires
entryInfo.expiryTime = expiryTime entryInfo.expiryTime = expiryTime
entryInfo.url = url entryInfo.url = url
@@ -456,6 +458,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
icon = newEntryInfo.icon icon = newEntryInfo.icon
username = newEntryInfo.username username = newEntryInfo.username
password = newEntryInfo.password password = newEntryInfo.password
// Update date time, creation time stay as is
lastModificationTime = DateInstant()
lastAccessTime = DateInstant()
expires = newEntryInfo.expires expires = newEntryInfo.expires
expiryTime = newEntryInfo.expiryTime expiryTime = newEntryInfo.expiryTime
url = newEntryInfo.url url = newEntryInfo.url
@@ -464,9 +469,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
database?.binaryPool?.let { binaryPool -> database?.binaryPool?.let { binaryPool ->
addAttachments(binaryPool, newEntryInfo.attachments) addAttachments(binaryPool, newEntryInfo.attachments)
} }
// Update date time
lastAccessTime = DateInstant()
lastModificationTime = DateInstant()
database?.stopManageEntry(this) database?.stopManageEntry(this)
} }

View File

@@ -163,10 +163,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
finalKey = messageDigest.digest() finalKey = messageDigest.digest()
} }
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
return null
}
override fun createGroup(): GroupKDB { override fun createGroup(): GroupKDB {
return GroupKDB() return GroupKDB()
} }

View File

@@ -43,10 +43,12 @@ import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.VariantDictionary import com.kunzisoft.keepass.utils.VariantDictionary
import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node import org.w3c.dom.Node
import org.w3c.dom.Text
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@@ -180,7 +182,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
when (oldCompression) { when (oldCompression) {
CompressionAlgorithm.None -> { CompressionAlgorithm.None -> {
when (newCompression) { when (newCompression) {
CompressionAlgorithm.None -> {} CompressionAlgorithm.None -> {
}
CompressionAlgorithm.GZip -> { CompressionAlgorithm.GZip -> {
// Only in databaseV3.1, in databaseV4 the header is zipped during the save // Only in databaseV3.1, in databaseV4 the header is zipped during the save
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) { if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
@@ -198,7 +201,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
CompressionAlgorithm.None -> { CompressionAlgorithm.None -> {
decompressAllBinaries() decompressAllBinaries()
} }
CompressionAlgorithm.GZip -> {} CompressionAlgorithm.GZip -> {
}
} }
} }
} }
@@ -378,36 +382,82 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
try { try {
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e : ParserConfigurationException) { } catch (e : ParserConfigurationException) {
Log.e(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)", e) Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
} }
val documentBuilder = documentBuilderFactory.newDocumentBuilder() val documentBuilder = documentBuilderFactory.newDocumentBuilder()
val doc = documentBuilder.parse(keyInputStream) val doc = documentBuilder.parse(keyInputStream)
var xmlKeyFileVersion = 1F
val docElement = doc.documentElement val docElement = doc.documentElement
if (docElement == null || !docElement.nodeName.equals(RootElementName, ignoreCase = true)) { val keyFileChildNodes = docElement.childNodes
// <KeyFile> Root node
if (docElement == null
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
return null return null
} }
if (keyFileChildNodes.length < 2)
val children = docElement.childNodes
if (children.length < 2) {
return null return null
} for (keyFileChildPosition in 0 until keyFileChildNodes.length) {
val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition)
for (i in 0 until children.length) { // <Meta>
val child = children.item(i) if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) {
val metaChildNodes = keyFileChildNode.childNodes
if (child.nodeName.equals(KeyElementName, ignoreCase = true)) { for (metaChildPosition in 0 until metaChildNodes.length) {
val keyChildren = child.childNodes val metaChildNode = metaChildNodes.item(metaChildPosition)
for (j in 0 until keyChildren.length) { // <Version>
val keyChild = keyChildren.item(j) if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) {
if (keyChild.nodeName.equals(KeyDataElementName, ignoreCase = true)) { val versionChildNodes = metaChildNode.childNodes
val children2 = keyChild.childNodes for (versionChildPosition in 0 until versionChildNodes.length) {
for (k in 0 until children2.length) { val versionChildNode = versionChildNodes.item(versionChildPosition)
val text = children2.item(k) if (versionChildNode.nodeType == Node.TEXT_NODE) {
if (text.nodeType == Node.TEXT_NODE) { val versionText = versionChildNode.textContent.removeSpaceChars()
val txt = text as Text try {
return Base64.decode(txt.nodeValue, BASE_64_FLAG) xmlKeyFileVersion = versionText.toFloat()
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
} catch (e: Exception) {
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
}
}
}
}
}
}
// <Key>
if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) {
val keyChildNodes = keyFileChildNode.childNodes
for (keyChildPosition in 0 until keyChildNodes.length) {
val keyChildNode = keyChildNodes.item(keyChildPosition)
// <Data>
if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) {
var hashString : String? = null
if (keyChildNode.hasAttributes()) {
val dataNodeAttributes = keyChildNode.attributes
hashString = dataNodeAttributes
.getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue
}
val dataChildNodes = keyChildNode.childNodes
for (dataChildPosition in 0 until dataChildNodes.length) {
val dataChildNode = dataChildNodes.item(dataChildPosition)
if (dataChildNode.nodeType == Node.TEXT_NODE) {
val dataString = dataChildNode.textContent.removeSpaceChars()
when (xmlKeyFileVersion) {
1F -> {
// No hash in KeyFile XML version 1
return Base64.decode(dataString, BASE_64_FLAG)
}
2F -> {
return if (hashString != null
&& checkKeyFileHash(dataString, hashString)) {
Log.i(TAG, "Successful key file hash check.")
Hex.decodeHex(dataString.toCharArray())
} else {
Log.e(TAG, "Unable to check the hash of the key file.")
null
}
}
}
} }
} }
} }
@@ -417,10 +467,26 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} catch (e: Exception) { } catch (e: Exception) {
return null return null
} }
return null return null
} }
private fun checkKeyFileHash(data: String, hash: String): Boolean {
val digest: MessageDigest?
var success = false
try {
digest = MessageDigest.getInstance("SHA-256")
digest?.reset()
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
val dataDigest = digest.digest(Hex.decodeHex(data.toCharArray()))
.copyOfRange(0, 4)
.toHexString()
success = dataDigest == hash
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
}
return success
}
override fun newGroupId(): NodeIdUUID { override fun newGroupId(): NodeIdUUID {
var newId: NodeIdUUID var newId: NodeIdUUID
do { do {
@@ -634,11 +700,12 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited
private const val RootElementName = "KeyFile" private const val XML_NODE_ROOT_NAME = "KeyFile"
//private const val MetaElementName = "Meta"; private const val XML_NODE_META_NAME = "Meta"
//private const val VersionElementName = "Version"; private const val XML_NODE_VERSION_NAME = "Version"
private const val KeyElementName = "Key" private const val XML_NODE_KEY_NAME = "Key"
private const val KeyDataElementName = "Data" private const val XML_NODE_DATA_NAME = "Data"
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
const val BASE_64_FLAG = Base64.NO_WRAP const val BASE_64_FLAG = Base64.NO_WRAP

View File

@@ -27,7 +27,11 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import java.io.* import org.apache.commons.codec.binary.Hex
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.util.* import java.util.*
@@ -124,42 +128,35 @@ abstract class DatabaseVersioned<
@Throws(IOException::class) @Throws(IOException::class)
protected fun getFileKey(keyInputStream: InputStream): ByteArray { protected fun getFileKey(keyInputStream: InputStream): ByteArray {
val keyByteArrayOutputStream = ByteArrayOutputStream() val keyData = keyInputStream.readBytes()
keyInputStream.copyTo(keyByteArrayOutputStream)
val keyData = keyByteArrayOutputStream.toByteArray()
val keyByteArrayInputStream = ByteArrayInputStream(keyData) // Check XML key file
val key = loadXmlKeyFile(keyByteArrayInputStream) val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
if (key != null) { if (xmlKeyByteArray != null) {
return key return xmlKeyByteArray
} }
when (keyData.size.toLong()) { // Check 32 bytes key file
32L -> return keyData when (keyData.size) {
64L -> try { 32 -> return keyData
return hexStringToByteArray(String(keyData)) 64 -> try {
} catch (e: IndexOutOfBoundsException) { return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data // Key is not base 64, treat it as binary data
} }
} }
val messageDigest: MessageDigest // Hash file as binary data
try { try {
messageDigest = MessageDigest.getInstance("SHA-256") return MessageDigest.getInstance("SHA-256").digest(keyData)
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
throw IOException("SHA-256 not supported") throw IOException("SHA-256 not supported")
} }
try {
messageDigest.update(keyData)
} catch (e: Exception) {
println(e.toString())
}
return messageDigest.digest()
} }
protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
return null
}
open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
if (password == null && !containsKeyFile) if (password == null && !containsKeyFile)
@@ -391,16 +388,5 @@ abstract class DatabaseVersioned<
private const val TAG = "DatabaseVersioned" private const val TAG = "DatabaseVersioned"
val UUID_ZERO = UUID(0, 0) val UUID_ZERO = UUID(0, 0)
fun hexStringToByteArray(s: String): ByteArray {
val len = s.length
val data = ByteArray(len / 2)
var i = 0
while (i < len) {
data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte()
i += 2
}
return data
}
} }
} }

View File

@@ -43,20 +43,12 @@ abstract class DatabaseException : Exception {
} }
open class LoadDatabaseException : DatabaseException { open class LoadDatabaseException : DatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.error_load_database override var errorId: Int = R.string.error_load_database
constructor() : super() constructor() : super()
constructor(throwable: Throwable) : super(throwable) constructor(throwable: Throwable) : super(throwable)
} }
class ArcFourDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_arc4
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class FileNotFoundDatabaseException : LoadDatabaseException { class FileNotFoundDatabaseException : LoadDatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.file_not_found_content override var errorId: Int = R.string.file_not_found_content
@@ -67,7 +59,6 @@ class FileNotFoundDatabaseException : LoadDatabaseException {
class InvalidAlgorithmDatabaseException : LoadDatabaseException { class InvalidAlgorithmDatabaseException : LoadDatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.invalid_algorithm override var errorId: Int = R.string.invalid_algorithm
constructor() : super() constructor() : super()
constructor(exception: Throwable) : super(exception) constructor(exception: Throwable) : super(exception)
} }

View File

@@ -41,6 +41,13 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
abstract fun openDatabase(databaseInputStream: InputStream, abstract fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?): PwDb progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
} }

View File

@@ -45,8 +45,7 @@ import javax.crypto.spec.SecretKeySpec
/** /**
* Load a KDB database file. * Load a KDB database file.
*/ */
class DatabaseInputKDB(cacheDirectory: File, class DatabaseInputKDB(cacheDirectory: File)
private val fixDuplicateUUID: Boolean = false)
: DatabaseInput<DatabaseKDB>(cacheDirectory) { : DatabaseInput<DatabaseKDB>(cacheDirectory) {
private lateinit var mDatabaseToOpen: DatabaseKDB private lateinit var mDatabaseToOpen: DatabaseKDB
@@ -55,7 +54,28 @@ class DatabaseInputKDB(cacheDirectory: File,
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB { progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabaseToOpen.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
try { try {
// Load entire file, most of it's encrypted. // Load entire file, most of it's encrypted.
@@ -84,7 +104,7 @@ class DatabaseInputKDB(cacheDirectory: File,
mDatabaseToOpen = DatabaseKDB() mDatabaseToOpen = DatabaseKDB()
mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream) assignMasterKey?.invoke()
// Select algorithm // Select algorithm
when { when {

View File

@@ -25,9 +25,10 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.StreamCipherFactory import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
@@ -37,7 +38,6 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
@@ -63,8 +63,7 @@ import javax.crypto.Cipher
import javax.crypto.CipherInputStream import javax.crypto.CipherInputStream
import kotlin.math.min import kotlin.math.min
class DatabaseInputKDBX(cacheDirectory: File, class DatabaseInputKDBX(cacheDirectory: File)
private val fixDuplicateUUID: Boolean = false)
: DatabaseInput<DatabaseKDBX>(cacheDirectory) { : DatabaseInput<DatabaseKDBX>(cacheDirectory) {
private var randomStream: StreamCipher? = null private var randomStream: StreamCipher? = null
@@ -98,12 +97,30 @@ class DatabaseInputKDBX(cacheDirectory: File,
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX { progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.retrieveMasterKey(password, keyInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
try { try {
// TODO performance
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
mDatabase = DatabaseKDBX() mDatabase = DatabaseKDBX()
mDatabase.changeDuplicateId = fixDuplicateUUID mDatabase.changeDuplicateId = fixDuplicateUUID
@@ -116,9 +133,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
hashOfHeader = headerAndHash.hash hashOfHeader = headerAndHash.hash
val pbHeader = headerAndHash.header val pbHeader = headerAndHash.header
mDatabase.retrieveMasterKey(password, keyInputStream) assignMasterKey?.invoke()
mDatabase.makeFinalKey(header.masterSeed) mDatabase.makeFinalKey(header.masterSeed)
// TODO performance
progressTaskUpdater?.updateMessage(R.string.decrypting_db) progressTaskUpdater?.updateMessage(R.string.decrypting_db)
val engine: CipherEngine val engine: CipherEngine
@@ -185,10 +201,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
loadInnerHeader(inputStreamXml, header) loadInnerHeader(inputStreamXml, header)
} }
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) try {
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
if (randomStream == null) { } catch (e: Exception) {
throw ArcFourDatabaseException() throw LoadDatabaseException(e)
} }
readDocumentStreamed(createPullParser(inputStreamXml)) readDocumentStreamed(createPullParser(inputStreamXml))
@@ -436,8 +452,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
val strData = readString(xpp) val strData = readString(xpp)
if (strData.isNotEmpty()) { if (strData.isNotEmpty()) {
customIconData = Base64.decode(strData, BASE_64_FLAG) customIconData = Base64.decode(strData, BASE_64_FLAG)
} else {
assert(false)
} }
} else { } else {
readUnknown(xpp) readUnknown(xpp)
@@ -958,7 +972,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
// Create empty binary if not retrieved in pool // Create empty binary if not retrieved in pool
if (binaryRetrieve == null) { if (binaryRetrieve == null) {
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory, binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory,
compression = false, protection = true, binaryPoolId = id) compression = false, protection = false, binaryPoolId = id)
} }
return binaryRetrieve return binaryRetrieve
} }
@@ -1024,29 +1038,20 @@ class DatabaseInputKDBX(cacheDirectory: File,
return xpp.safeNextText() return xpp.safeNextText()
} }
@Throws(XmlPullParserException::class, IOException::class)
private fun readBase64String(xpp: XmlPullParser): ByteArray {
//readNextNode = false;
Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
val plainText = ByteArray(data.size)
randomStream?.processBytes(data, 0, data.size, plainText, 0)
return plainText
}
return ByteArray(0)
}
@Throws(XmlPullParserException::class, IOException::class) @Throws(XmlPullParserException::class, IOException::class)
private fun readProtectedBase64String(xpp: XmlPullParser): ByteArray? { private fun readProtectedBase64String(xpp: XmlPullParser): ByteArray? {
//(xpp.getEventType() == XmlPullParser.START_TAG);
if (xpp.attributeCount > 0) { if (xpp.attributeCount > 0) {
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected) val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
if (protect != null && protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)) { if (protect != null && protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)) {
return readBase64String(xpp) Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
val plainText = ByteArray(data.size)
randomStream?.processBytes(data, 0, data.size, plainText, 0)
return plainText
}
return ByteArray(0)
} }
} }
return null return null
} }

View File

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

View File

@@ -40,13 +40,16 @@ import javax.crypto.spec.SecretKeySpec
class DatabaseHeaderOutputKDBX @Throws(DatabaseOutputException::class) class DatabaseHeaderOutputKDBX @Throws(DatabaseOutputException::class)
constructor(private val databaseKDBX: DatabaseKDBX, constructor(private val databaseKDBX: DatabaseKDBX,
private val header: DatabaseHeaderKDBX, private val header: DatabaseHeaderKDBX,
outputStream: OutputStream) : DatabaseHeaderOutput() { outputStream: OutputStream) {
private val los: LittleEndianDataOutputStream private val los: LittleEndianDataOutputStream
private val mos: MacOutputStream private val mos: MacOutputStream
private val dos: DigestOutputStream private val dos: DigestOutputStream
lateinit var headerHmac: ByteArray lateinit var headerHmac: ByteArray
var hashOfHeader: ByteArray? = null
private set
init { init {
val md: MessageDigest val md: MessageDigest

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDroid 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.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
import com.kunzisoft.keepass.stream.readBytes
import com.kunzisoft.keepass.utils.UnsignedInt
import java.io.IOException
import java.io.OutputStream
import kotlin.experimental.or
class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
private val header: DatabaseHeaderKDBX,
outputStream: OutputStream) {
private val dataOutputStream: LittleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream)
@Throws(IOException::class)
fun output() {
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
dataOutputStream.writeInt(4)
if (header.innerRandomStream == null)
throw IOException("Can't write innerRandomStream")
dataOutputStream.writeUInt(header.innerRandomStream!!.id)
val streamKeySize = header.innerRandomStreamKey.size
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
dataOutputStream.writeInt(streamKeySize)
dataOutputStream.write(header.innerRandomStreamKey)
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
val protectedBinary = keyBinary.binary
// Force decompression to add binary in header
protectedBinary.decompress()
// Write type binary
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
// Write size
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
// Write protected flag
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) {
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
}
dataOutputStream.writeByte(flag)
protectedBinary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
dataOutputStream.write(buffer)
}
}
}
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
dataOutputStream.writeInt(0)
}
}

View File

@@ -26,7 +26,7 @@ import java.io.OutputStream
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.security.SecureRandom import java.security.SecureRandom
abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(protected var mOS: OutputStream) { abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(protected var mOutputStream: OutputStream) {
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
protected open fun setIVs(header: Header): SecureRandom { protected open fun setIVs(header: Header): SecureRandom {

View File

@@ -63,7 +63,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
// and remove any orphaned nodes that are no longer part of the tree hierarchy // and remove any orphaned nodes that are no longer part of the tree hierarchy
sortGroupsForOutput() sortGroupsForOutput()
val header = outputHeader(mOS) val header = outputHeader(mOutputStream)
val finalKey = getFinalKey(header) val finalKey = getFinalKey(header)
@@ -85,7 +85,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
cipher.init(Cipher.ENCRYPT_MODE, cipher.init(Cipher.ENCRYPT_MODE,
SecretKeySpec(finalKey, "AES"), SecretKeySpec(finalKey, "AES"),
IvParameterSpec(header.encryptionIV)) IvParameterSpec(header.encryptionIV))
val cos = CipherOutputStream(mOS, cipher) val cos = CipherOutputStream(mOutputStream, cipher)
val bos = BufferedOutputStream(cos) val bos = BufferedOutputStream(cos)
outputPlanGroupAndEntries(bos) outputPlanGroupAndEntries(bos)
bos.flush() bos.flush()

View File

@@ -38,7 +38,6 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
@@ -47,6 +46,7 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
import com.kunzisoft.keepass.database.file.DateKDBXUtil import com.kunzisoft.keepass.database.file.DateKDBXUtil
import com.kunzisoft.keepass.stream.* import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.utils.UnsignedInt
import org.bouncycastle.crypto.StreamCipher import org.bouncycastle.crypto.StreamCipher
import org.joda.time.DateTime import org.joda.time.DateTime
import org.xmlpull.v1.XmlSerializer import org.xmlpull.v1.XmlSerializer
@@ -58,6 +58,7 @@ import java.util.*
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
import kotlin.experimental.or
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
@@ -81,20 +82,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
throw DatabaseOutputException("No such cipher", e) throw DatabaseOutputException("No such cipher", e)
} }
header = outputHeader(mOS) header = outputHeader(mOutputStream)
val osPlain: OutputStream val osPlain: OutputStream
osPlain = if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) { osPlain = if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
val cos = attachStreamEncryptor(header!!, mOS) val cos = attachStreamEncryptor(header!!, mOutputStream)
cos.write(header!!.streamStartBytes) cos.write(header!!.streamStartBytes)
HashedBlockOutputStream(cos) HashedBlockOutputStream(cos)
} else { } else {
mOS.write(hashOfHeader!!) mOutputStream.write(hashOfHeader!!)
mOS.write(headerHmac!!) mOutputStream.write(headerHmac!!)
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!))
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOS, mDatabaseKDBX.hmacKey!!))
} }
val osXml: OutputStream val osXml: OutputStream
@@ -105,8 +105,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
if (header!!.version.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) { if (header!!.version.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
val ihOut = DatabaseInnerHeaderOutputKDBX(mDatabaseKDBX, header!!, osXml) outputInnerHeader(mDatabaseKDBX, header!!, osXml)
ihOut.output()
} }
outputDatabase(osXml) outputDatabase(osXml)
@@ -122,6 +121,49 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
} }
@Throws(IOException::class)
private fun outputInnerHeader(database: DatabaseKDBX,
header: DatabaseHeaderKDBX,
outputStream: OutputStream) {
val dataOutputStream = LittleEndianDataOutputStream(outputStream)
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
dataOutputStream.writeInt(4)
if (header.innerRandomStream == null)
throw IOException("Can't write innerRandomStream")
dataOutputStream.writeUInt(header.innerRandomStream!!.id)
val streamKeySize = header.innerRandomStreamKey.size
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
dataOutputStream.writeInt(streamKeySize)
dataOutputStream.write(header.innerRandomStreamKey)
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
val protectedBinary = keyBinary.binary
// Force decompression to add binary in header
protectedBinary.decompress()
// Write type binary
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
// Write size
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
// Write protected flag
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) {
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
}
dataOutputStream.writeByte(flag)
protectedBinary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
dataOutputStream.write(buffer)
}
}
}
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
dataOutputStream.writeInt(0)
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun outputDatabase(outputStream: OutputStream) { private fun outputDatabase(outputStream: OutputStream) {
@@ -282,9 +324,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
random.nextBytes(header.innerRandomStreamKey) random.nextBytes(header.innerRandomStreamKey)
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) try {
if (randomStream == null) { randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
throw DatabaseOutputException("Invalid random cipher") } catch (e: Exception) {
throw DatabaseOutputException(e)
} }
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) { if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
@@ -420,41 +463,56 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
writeObject(name, String(Base64.encode(data, BASE_64_FLAG))) writeObject(name, String(Base64.encode(data, BASE_64_FLAG)))
} }
/*
// Normally used by a single entry but obsolete because binaries are in meta tag with kdbx3.1-
// or in file header with kdbx4
// binary.isProtected attribute is not used to create the XML
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeBinary(binary : BinaryAttachment) { private fun writeEntryBinary(binary : BinaryAttachment) {
val binaryLength = binary.length() if (binary.length() > 0) {
if (binaryLength > 0) {
if (binary.isProtected) { if (binary.isProtected) {
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue) xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
binary.getInputDataStream().use { inputStream ->
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer -> inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
val encoded = ByteArray(buffer.size) val encoded = ByteArray(buffer.size)
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0) randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
val charArray = String(Base64.encode(encoded, BASE_64_FLAG)).toCharArray() xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
xml.text(charArray, 0, charArray.size) }
} }
} else { } else {
if (binary.isCompressed) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
}
// Write the XML // Write the XML
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer -> binary.getInputDataStream().use { inputStream ->
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray() inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
xml.text(charArray, 0, charArray.size) xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
}
} }
} }
} }
} }
*/
// Only uses with kdbx3.1 to write binaries in meta tag
// With kdbx4, don't use this method because binaries are in header file
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeMetaBinaries() { private fun writeMetaBinaries() {
xml.startTag(null, DatabaseKDBXXML.ElemBinaries) xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
// Use indexes because necessarily in DatabaseV4 (binary header ref is the order) // Use indexes because necessarily (binary header ref is the order)
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary -> mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
xml.startTag(null, DatabaseKDBXXML.ElemBinary) xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString()) xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
writeBinary(keyBinary.binary) val binary = keyBinary.binary
if (binary.length() > 0) {
if (binary.isCompressed) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
}
// Write the XML
binary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
}
}
}
xml.endTag(null, DatabaseKDBXXML.ElemBinary) xml.endTag(null, DatabaseKDBXXML.ElemBinary)
} }
@@ -523,13 +581,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
if (protect) { if (protect) {
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue) xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
val data = value.toString().toByteArray()
val data = value.toString().toByteArray(charset("UTF-8")) val dataLength = data.size
val valLength = data.size if (data.isNotEmpty()) {
val encoded = ByteArray(dataLength)
if (valLength > 0) { randomStream!!.processBytes(data, 0, dataLength, encoded, 0)
val encoded = ByteArray(valLength)
randomStream!!.processBytes(data, 0, valLength, encoded, 0)
xml.text(String(Base64.encode(encoded, BASE_64_FLAG))) xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
} }
} else { } else {

View File

@@ -26,9 +26,12 @@ import android.graphics.*
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.os.Build
import android.util.Log import android.util.Log
import android.widget.ImageView import android.widget.ImageView
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
@@ -87,6 +90,22 @@ class IconDrawableFactory {
remoteViews.setImageViewBitmap(imageId, bitmap) remoteViews.setImageViewBitmap(imageId, bitmap)
} }
/**
* Utility method to assign a drawable to a icon and tint it
*/
@RequiresApi(Build.VERSION_CODES.M)
fun assignDrawableToIcon(superDrawable: SuperDrawable,
tintColor: Int = Color.BLACK): Icon {
val bitmap = superDrawable.drawable.toBitmap()
// Tint bitmap if it's not a custom icon
if (superDrawable.tintable && bitmap.isMutable) {
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
})
}
return Icon.createWithBitmap(bitmap)
}
/** /**
* Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed * Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed
*/ */
@@ -309,3 +328,22 @@ fun RemoteViews.assignDatabaseIcon(context: Context,
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e) Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
} }
} }
@RequiresApi(Build.VERSION_CODES.M)
fun createIconFromDatabaseIcon(context: Context,
iconFactory: IconDrawableFactory,
icon: IconImage,
tintColor: Int = Color.BLACK): Icon? {
try {
return iconFactory.assignDrawableToIcon(
iconFactory.getIconSuperDrawable(context,
icon,
24,
true,
tintColor),
tintColor)
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
return null
}

View File

@@ -244,7 +244,8 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
if (entryInfoKey != null) { if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.password, 1) currentInputConnection.commitText(entryInfoKey!!.password, 1)
} }
actionGoAutomatically() val otpFieldExists = entryInfoKey?.containsCustomField(OTP_TOKEN_FIELD) ?: false
actionGoAutomatically(!otpFieldExists)
} }
KEY_OTP -> { KEY_OTP -> {
if (entryInfoKey != null) { if (entryInfoKey != null) {
@@ -280,10 +281,11 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB)) currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB))
} }
private fun actionGoAutomatically() { private fun actionGoAutomatically(switchToPreviousKeyboardIfAllowed: Boolean = true) {
if (PreferencesUtil.isAutoGoActionEnable(this)) { if (PreferencesUtil.isAutoGoActionEnable(this)) {
currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO) currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
if (PreferencesUtil.isKeyboardPreviousFillInEnable(this)) { if (switchToPreviousKeyboardIfAllowed
&& PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
switchToPreviousKeyboard() switchToPreviousKeyboard()
} }
} }

View File

@@ -39,6 +39,8 @@ class EntryInfo : Parcelable {
var icon: IconImage = IconImageStandard() var icon: IconImage = IconImageStandard()
var username: String = "" var username: String = ""
var password: String = "" var password: String = ""
var creationTime: DateInstant = DateInstant()
var modificationTime: DateInstant = DateInstant()
var expires: Boolean = false var expires: Boolean = false
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
var url: String = "" var url: String = ""
@@ -55,6 +57,8 @@ class EntryInfo : Parcelable {
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
username = parcel.readString() ?: username username = parcel.readString() ?: username
password = parcel.readString() ?: password password = parcel.readString() ?: password
creationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: creationTime
modificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: modificationTime
expires = parcel.readInt() != 0 expires = parcel.readInt() != 0
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
url = parcel.readString() ?: url url = parcel.readString() ?: url
@@ -74,6 +78,8 @@ class EntryInfo : Parcelable {
parcel.writeParcelable(icon, flags) parcel.writeParcelable(icon, flags)
parcel.writeString(username) parcel.writeString(username)
parcel.writeString(password) parcel.writeString(password)
parcel.writeParcelable(creationTime, flags)
parcel.writeParcelable(modificationTime, flags)
parcel.writeInt(if (expires) 1 else 0) parcel.writeInt(if (expires) 1 else 0)
parcel.writeParcelable(expiryTime, flags) parcel.writeParcelable(expiryTime, flags)
parcel.writeString(url) parcel.writeString(url)
@@ -91,8 +97,8 @@ class EntryInfo : Parcelable {
return customFields.any { !it.protectedValue.isProtected } return customFields.any { !it.protectedValue.isProtected }
} }
fun isAutoGeneratedField(field: Field): Boolean { fun containsCustomField(label: String): Boolean {
return field.name == OTP_TOKEN_FIELD return customFields.lastOrNull { it.name == label } != null
} }
fun getGeneratedFieldValue(label: String): String { fun getGeneratedFieldValue(label: String): String {

View File

@@ -0,0 +1,82 @@
package com.kunzisoft.keepass.model
import android.content.Context
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import android.text.format.Formatter
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
import java.text.DateFormat
import java.util.*
/**
* Utility data class to get FileDatabaseInfo at a `t` time
*/
data class SnapFileDatabaseInfo(var fileUri: Uri?,
var exists: Boolean,
var lastModification: Long?,
var size: Long?): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readParcelable(Uri::class.java.classLoader),
parcel.readByte() != 0.toByte(),
parcel.readValue(Long::class.java.classLoader) as? Long,
parcel.readValue(Long::class.java.classLoader) as? Long) {
}
fun toString(context: Context): String {
val lastModificationString = DateFormat.getDateTimeInstance()
.format(Date(lastModification ?: 0))
return "$lastModificationString, " +
Formatter.formatFileSize(context, size ?: 0)
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(fileUri, flags)
parcel.writeByte(if (exists) 1 else 0)
parcel.writeValue(lastModification)
parcel.writeValue(size)
}
override fun describeContents(): Int {
return 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SnapFileDatabaseInfo) return false
if (fileUri != other.fileUri) return false
if (exists != other.exists) return false
if (lastModification != other.lastModification) return false
if (size != other.size) return false
return true
}
override fun hashCode(): Int {
var result = fileUri?.hashCode() ?: 0
result = 31 * result + exists.hashCode()
result = 31 * result + (lastModification?.hashCode() ?: 0)
result = 31 * result + (size?.hashCode() ?: 0)
return result
}
companion object CREATOR : Parcelable.Creator<SnapFileDatabaseInfo> {
override fun createFromParcel(parcel: Parcel): SnapFileDatabaseInfo {
return SnapFileDatabaseInfo(parcel)
}
override fun newArray(size: Int): Array<SnapFileDatabaseInfo?> {
return arrayOfNulls(size)
}
fun fromFileDatabaseInfo(fileDatabaseInfo: FileDatabaseInfo): SnapFileDatabaseInfo {
return SnapFileDatabaseInfo(
fileDatabaseInfo.fileUri,
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModification(),
fileDatabaseInfo.getSize())
}
}
}

View File

@@ -62,12 +62,12 @@ class AdvancedUnlockNotificationService : NotificationService() {
action = ACTION_REMOVE_KEYS action = ACTION_REMOVE_KEYS
} }
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT) val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val deviceCredential = PreferencesUtil.isDeviceCredentialUnlockEnable(this) val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
val notificationBuilder = buildNewNotification().apply { val notificationBuilder = buildNewNotification().apply {
setSmallIcon(if (deviceCredential) { setSmallIcon(if (biometricUnlockEnabled) {
R.drawable.notification_ic_device_unlock_24dp
} else {
R.drawable.notification_ic_fingerprint_unlock_24dp R.drawable.notification_ic_fingerprint_unlock_24dp
} else {
R.drawable.notification_ic_device_unlock_24dp
}) })
intent?.let { intent?.let {
setContentTitle(getString(R.string.advanced_unlock)) setContentTitle(getString(R.string.advanced_unlock))

View File

@@ -23,6 +23,7 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import java.util.* import java.util.*
/** /**
@@ -54,6 +55,7 @@ class ClipboardEntryNotificationField : Parcelable {
NotificationFieldId.UNKNOWN -> "" NotificationFieldId.UNKNOWN -> ""
NotificationFieldId.USERNAME -> entryInfo?.username ?: "" NotificationFieldId.USERNAME -> entryInfo?.username ?: ""
NotificationFieldId.PASSWORD -> entryInfo?.password ?: "" NotificationFieldId.PASSWORD -> entryInfo?.password ?: ""
NotificationFieldId.OTP -> entryInfo?.getGeneratedFieldValue(OTP_TOKEN_FIELD) ?: ""
NotificationFieldId.FIELD_A, NotificationFieldId.FIELD_A,
NotificationFieldId.FIELD_B, NotificationFieldId.FIELD_B,
NotificationFieldId.FIELD_C -> entryInfo?.getGeneratedFieldValue(label) ?: "" NotificationFieldId.FIELD_C -> entryInfo?.getGeneratedFieldValue(label) ?: ""
@@ -81,7 +83,7 @@ class ClipboardEntryNotificationField : Parcelable {
} }
enum class NotificationFieldId { enum class NotificationFieldId {
UNKNOWN, USERNAME, PASSWORD, FIELD_A, FIELD_B, FIELD_C; UNKNOWN, USERNAME, PASSWORD, OTP, FIELD_A, FIELD_B, FIELD_C;
companion object { companion object {
val anonymousFieldId: Array<NotificationFieldId> val anonymousFieldId: Array<NotificationFieldId>

View File

@@ -25,6 +25,7 @@ import android.content.Intent
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
@@ -250,6 +251,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
val containsUsernameToCopy = entry.username.isNotEmpty() val containsUsernameToCopy = entry.username.isNotEmpty()
val containsPasswordToCopy = entry.password.isNotEmpty() val containsPasswordToCopy = entry.password.isNotEmpty()
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(context) && PreferencesUtil.allowCopyPasswordAndProtectedFields(context)
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
val containsExtraFieldToCopy = entry.customFields.isNotEmpty() val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
&& (entry.containsCustomFieldsNotProtected() && (entry.containsCustomFieldsNotProtected()
|| ||
@@ -262,7 +264,10 @@ class ClipboardEntryNotificationService : LockNotificationService() {
// If notifications enabled in settings // If notifications enabled in settings
// Don't if application timeout // Don't if application timeout
if (PreferencesUtil.isClipboardNotificationsEnable(context)) { if (PreferencesUtil.isClipboardNotificationsEnable(context)) {
if (containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) { if (containsUsernameToCopy
|| containsPasswordToCopy
|| containsOTPToCopy
|| containsExtraFieldToCopy) {
// username already copied, waiting for user's action before copy password. // username already copied, waiting for user's action before copy password.
intent.action = ACTION_NEW_NOTIFICATION intent.action = ACTION_NEW_NOTIFICATION
@@ -282,14 +287,22 @@ class ClipboardEntryNotificationService : LockNotificationService() {
ClipboardEntryNotificationField.NotificationFieldId.PASSWORD, ClipboardEntryNotificationField.NotificationFieldId.PASSWORD,
context.getString(R.string.entry_password))) context.getString(R.string.entry_password)))
} }
// Add OTP
if (containsOTPToCopy) {
notificationFields.add(
ClipboardEntryNotificationField(
ClipboardEntryNotificationField.NotificationFieldId.OTP,
OTP_TOKEN_FIELD))
}
// Add extra fields // Add extra fields
if (containsExtraFieldToCopy) { if (containsExtraFieldToCopy) {
try { try {
var anonymousFieldNumber = 0 var anonymousFieldNumber = 0
entry.customFields.forEach { field -> entry.customFields.forEach { field ->
//If value is not protected or allowed //If value is not protected or allowed
if (!field.protectedValue.isProtected if ((!field.protectedValue.isProtected
|| PreferencesUtil.allowCopyPasswordAndProtectedFields(context)) { || PreferencesUtil.allowCopyPasswordAndProtectedFields(context))
&& field.name != OTP_TOKEN_FIELD) {
notificationFields.add( notificationFields.add(
ClipboardEntryNotificationField( ClipboardEntryNotificationField(
ClipboardEntryNotificationField.NotificationFieldId.anonymousFieldId[anonymousFieldNumber], ClipboardEntryNotificationField.NotificationFieldId.anonymousFieldId[anonymousFieldNumber],

View File

@@ -22,9 +22,8 @@ package com.kunzisoft.keepass.notifications
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Binder import android.os.*
import android.os.Bundle import android.util.Log
import android.os.IBinder
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
@@ -40,6 +39,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -47,6 +47,7 @@ import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -65,6 +66,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private var mAllowFinishAction = AtomicBoolean() private var mAllowFinishAction = AtomicBoolean()
private var mActionRunning = false private var mActionRunning = false
private var mDatabaseInfoListeners = LinkedList<DatabaseInfoListener>()
private var mIconId: Int = R.drawable.notification_ic_database_load private var mIconId: Int = R.drawable.notification_ic_database_load
private var mTitleId: Int = R.string.database_opened private var mTitleId: Int = R.string.database_opened
private var mMessageId: Int? = null private var mMessageId: Int? = null
@@ -93,6 +96,14 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mAllowFinishAction.set(false) mAllowFinishAction.set(false)
} }
} }
fun addDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) {
mDatabaseInfoListeners.add(databaseInfoListener)
}
fun removeDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) {
mDatabaseInfoListeners.remove(databaseInfoListener)
}
} }
interface ActionTaskListener { interface ActionTaskListener {
@@ -101,6 +112,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
fun onStopAction(actionTask: String, result: ActionRunnable.Result) fun onStopAction(actionTask: String, result: ActionRunnable.Result)
} }
interface DatabaseInfoListener {
fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo)
}
/** /**
* Force to call [ActionTaskListener.onStartAction] if the action is still running * Force to call [ActionTaskListener.onStartAction] if the action is still running
*/ */
@@ -112,6 +128,53 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
} }
fun checkDatabaseInfo() {
try {
mDatabase.fileUri?.let {
val previousDatabaseInfo = mSnapFileDatabaseInfo
val lastFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
FileDatabaseInfo(applicationContext, it))
val oldDatabaseModification = previousDatabaseInfo?.lastModification
val newDatabaseModification = lastFileDatabaseInfo.lastModification
val conditionExists = previousDatabaseInfo != null
&& previousDatabaseInfo.exists != lastFileDatabaseInfo.exists
// To prevent dialog opening too often
val conditionLastModification = (oldDatabaseModification != null && newDatabaseModification != null
&& oldDatabaseModification < newDatabaseModification
&& mLastLocalSaveTime + 5000 < newDatabaseModification)
if (conditionExists || conditionLastModification) {
// Show the dialog only if it's real new info and not a delay after a save
Log.i(TAG, "Database file modified " +
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
// Call listener to indicate a change in database info
if (previousDatabaseInfo != null) {
mDatabaseInfoListeners.forEach { listener ->
listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo)
}
}
mSnapFileDatabaseInfo = lastFileDatabaseInfo
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to check database info", e)
}
}
fun saveDatabaseInfo() {
try {
mDatabase.fileUri?.let {
mSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
FileDatabaseInfo(applicationContext, it))
Log.i(TAG, "Database file saved $mSnapFileDatabaseInfo")
}
} catch (e: Exception) {
Log.e(TAG, "Unable to check database info", e)
}
}
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? {
super.onBind(intent) super.onBind(intent)
return mActionTaskBinder return mActionTaskBinder
@@ -138,6 +201,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val actionRunnable: ActionRunnable? = when (intentAction) { val actionRunnable: ActionRunnable? = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent) ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent) ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask()
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent) ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent) ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent)
ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent) ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent)
@@ -192,6 +256,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
actionTaskListener.onStopAction(intentAction!!, result) actionTaskListener.onStopAction(intentAction!!, result)
} }
} finally { } finally {
// Save the database info before performing action
if (intentAction == ACTION_DATABASE_LOAD_TASK) {
saveDatabaseInfo()
}
// Save the database info after performing save action
if (intentAction == ACTION_DATABASE_SAVE
|| intent?.getBooleanExtra(SAVE_DATABASE_KEY, false) == true) {
mDatabase.fileUri?.let {
val newSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
FileDatabaseInfo(applicationContext, it))
mLastLocalSaveTime = System.currentTimeMillis()
mSnapFileDatabaseInfo = newSnapFileDatabaseInfo
}
}
removeIntentData(intent) removeIntentData(intent)
TimeoutHelper.releaseTemporarilyDisableTimeout() TimeoutHelper.releaseTemporarilyDisableTimeout()
if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
@@ -214,7 +292,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
return when (intentAction) { return when (intentAction) {
ACTION_DATABASE_LOAD_TASK, null -> { ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_RELOAD_TASK,
null -> {
START_STICKY START_STICKY
} }
else -> { else -> {
@@ -248,7 +328,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
else -> { else -> {
when (intentAction) { when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK -> R.string.loading_database ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
ACTION_DATABASE_SAVE -> R.string.saving_database ACTION_DATABASE_SAVE -> R.string.saving_database
else -> { else -> {
R.string.command_execution R.string.command_execution
@@ -258,13 +339,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
mMessageId = when (intentAction) { mMessageId = when (intentAction) {
ACTION_DATABASE_LOAD_TASK -> null ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_RELOAD_TASK -> null
else -> null else -> null
} }
mWarningId = mWarningId =
if (!saveAction if (!saveAction
|| intentAction == ACTION_DATABASE_LOAD_TASK) || intentAction == ACTION_DATABASE_LOAD_TASK
|| intentAction == ACTION_DATABASE_RELOAD_TASK)
null null
else else
R.string.do_not_kill_app R.string.do_not_kill_app
@@ -342,9 +425,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
* Execute action with a coroutine * Execute action with a coroutine
*/ */
private suspend fun executeAction(progressTaskUpdater: ProgressTaskUpdater, private suspend fun executeAction(progressTaskUpdater: ProgressTaskUpdater,
onPreExecute: () -> Unit, onPreExecute: () -> Unit,
onExecute: (ProgressTaskUpdater?) -> ActionRunnable?, onExecute: (ProgressTaskUpdater?) -> ActionRunnable?,
onPostExecute: (result: ActionRunnable.Result) -> Unit) { onPostExecute: (result: ActionRunnable.Result) -> Unit) {
mAllowFinishAction.set(false) mAllowFinishAction.set(false)
TimeoutHelper.temporarilyDisableTimeout() TimeoutHelper.temporarilyDisableTimeout()
@@ -465,6 +548,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
} }
private fun buildDatabaseReloadActionTask(): ActionRunnable {
return ReloadDatabaseRunnable(
this,
mDatabase,
this
) { result ->
// No need to add each info to reload database
result.data = Bundle()
}
}
private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? { private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(DATABASE_URI_KEY) return if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY) && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
@@ -770,6 +864,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK" const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK" const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
const val ACTION_DATABASE_RELOAD_TASK = "ACTION_DATABASE_RELOAD_TASK"
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK" const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK" const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK"
const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_GROUP_TASK" const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_GROUP_TASK"
@@ -822,6 +917,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time
const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
private var mLastLocalSaveTime: Long = 0
fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> { fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> {
val nodesAction = ArrayList<Node>() val nodesAction = ArrayList<Node>()
bundle.getParcelableArrayList<NodeId<*>>(GROUPS_ID_KEY)?.forEach { bundle.getParcelableArrayList<NodeId<*>>(GROUPS_ID_KEY)?.forEach {

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.otp package com.kunzisoft.keepass.otp
import com.kunzisoft.keepass.model.OtpModel import com.kunzisoft.keepass.model.OtpModel
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import org.apache.commons.codec.binary.Base32 import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.binary.Base64 import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.binary.Hex import org.apache.commons.codec.binary.Hex
@@ -137,7 +138,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun setHexSecret(secret: String) { fun setHexSecret(secret: String) {
if (secret.isNotEmpty()) if (secret.isNotEmpty())
otpModel.secret = Hex.decodeHex(secret) otpModel.secret = Hex.decodeHex(secret.toCharArray())
else else
throw IllegalArgumentException() throw IllegalArgumentException()
} }
@@ -150,16 +151,16 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun setBase32Secret(secret: String) { fun setBase32Secret(secret: String) {
if (isValidBase32(secret)) if (isValidBase32(secret)) {
otpModel.secret = Base32().decode(replaceBase32Chars(secret).toByteArray()) otpModel.secret = Base32().decode(replaceBase32Chars(secret))
else } else
throw IllegalArgumentException() throw IllegalArgumentException()
} }
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun setBase64Secret(secret: String) { fun setBase64Secret(secret: String) {
if (isValidBase64(secret)) if (isValidBase64(secret))
otpModel.secret = Base64().decode(secret.toByteArray()) otpModel.secret = Base64().decode(secret)
else else
throw IllegalArgumentException() throw IllegalArgumentException()
} }
@@ -208,38 +209,24 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
fun isValidBase32(secret: String): Boolean { fun isValidBase32(secret: String): Boolean {
val secretChars = replaceBase32Chars(secret) val secretChars = replaceBase32Chars(secret)
return secretChars.isNotEmpty() && checkBase32Secret(secretChars) return secret.isNotEmpty()
&& (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secretChars))
} }
fun isValidBase64(secret: String): Boolean { fun isValidBase64(secret: String): Boolean {
// TODO replace base 64 chars // TODO replace base 64 chars
return secret.isNotEmpty() && checkBase64Secret(secret) return secret.isNotEmpty()
} && (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret))
fun removeLineChars(parameter: String): String {
return parameter.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "")
}
fun removeSpaceChars(parameter: String): String {
return parameter.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
} }
fun replaceBase32Chars(parameter: String): String { fun replaceBase32Chars(parameter: String): String {
// Add 'A' at end if not Base32 length // Add padding '=' at end if not Base32 length
var parameterNewSize = removeSpaceChars(parameter.toUpperCase(Locale.ENGLISH)) var parameterNewSize = parameter.toUpperCase(Locale.ENGLISH).removeSpaceChars()
while (parameterNewSize.length % 8 != 0) { while (parameterNewSize.length % 8 != 0) {
parameterNewSize += 'A' parameterNewSize += '='
} }
return parameterNewSize return parameterNewSize
} }
fun checkBase32Secret(secret: String): Boolean {
return (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secret))
}
fun checkBase64Secret(secret: String): Boolean {
return (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret))
}
} }
} }

View File

@@ -24,9 +24,9 @@ import android.net.Uri
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement.Companion.removeLineChars
import com.kunzisoft.keepass.otp.OtpElement.Companion.removeSpaceChars
import com.kunzisoft.keepass.otp.TokenCalculator.* import com.kunzisoft.keepass.otp.TokenCalculator.*
import com.kunzisoft.keepass.utils.StringUtil.removeLineChars
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
@@ -57,13 +57,25 @@ object OtpEntryFields {
private const val DIGITS_KEY = "size" private const val DIGITS_KEY = "size"
private const val STEP_KEY = "step" private const val STEP_KEY = "step"
// HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#hmacotp) // HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#otp)
private const val HMACOTP_SECRET_FIELD = "HmacOtp-Secret" private const val HMACOTP_SECRET_FIELD = "HmacOtp-Secret"
private const val HMACOTP_SECRET_HEX_FIELD = "HmacOtp-Secret-Hex" private const val HMACOTP_SECRET_HEX_FIELD = "HmacOtp-Secret-Hex"
private const val HMACOTP_SECRET_BASE32_FIELD = "HmacOtp-Secret-Base32" private const val HMACOTP_SECRET_BASE32_FIELD = "HmacOtp-Secret-Base32"
private const val HMACOTP_SECRET_BASE64_FIELD = "HmacOtp-Secret-Base64" private const val HMACOTP_SECRET_BASE64_FIELD = "HmacOtp-Secret-Base64"
private const val HMACOTP_SECRET_COUNTER_FIELD = "HmacOtp-Counter" private const val HMACOTP_SECRET_COUNTER_FIELD = "HmacOtp-Counter"
// TimeOtp KeePass2 values
private const val TIMEOTP_SECRET_FIELD = "TimeOtp-Secret"
private const val TIMEOTP_SECRET_HEX_FIELD = "TimeOtp-Secret-Hex"
private const val TIMEOTP_SECRET_BASE32_FIELD = "TimeOtp-Secret-Base32"
private const val TIMEOTP_SECRET_BASE64_FIELD = "TimeOtp-Secret-Base64"
private const val TIMEOTP_LENGTH_FIELD = "TimeOtp-Length"
private const val TIMEOTP_PERIOD_FIELD = "TimeOtp-Period"
private const val TIMEOTP_ALGORITHM_FIELD = "TimeOtp-Algorithm"
private const val TIMEOTP_ALGORITHM_SHA1_VALUE = "HMAC-SHA-1"
private const val TIMEOTP_ALGORITHM_SHA256_VALUE = "HMAC-SHA-256"
private const val TIMEOTP_ALGORITHM_SHA512_VALUE = "HMAC-SHA-512"
// Custom fields (maybe from plugin) // Custom fields (maybe from plugin)
private const val TOTP_SEED_FIELD = "TOTP Seed" private const val TOTP_SEED_FIELD = "TOTP Seed"
private const val TOTP_SETTING_FIELD = "TOTP Settings" private const val TOTP_SETTING_FIELD = "TOTP Settings"
@@ -85,14 +97,17 @@ object OtpEntryFields {
// OTP (HOTP/TOTP) from URL and field from KeePassXC // OTP (HOTP/TOTP) from URL and field from KeePassXC
if (parseOTPUri(getField, otpElement)) if (parseOTPUri(getField, otpElement))
return otpElement return otpElement
// TOTP from KeePass 2.47
if (parseTOTPFromOfficialField(getField, otpElement))
return otpElement
// TOTP from key values (maybe plugin or old KeePassXC) // TOTP from key values (maybe plugin or old KeePassXC)
if (parseTOTPKeyValues(getField, otpElement)) if (parseTOTPKeyValues(getField, otpElement))
return otpElement return otpElement
// TOTP from custom field // TOTP from custom field
if (parseTOTPFromField(getField, otpElement)) if (parseTOTPFromPluginField(getField, otpElement))
return otpElement return otpElement
// HOTP fields from KeePass 2 // HOTP fields from KeePass 2
if (parseHOTPFromField(getField, otpElement)) if (parseHOTPFromOfficialField(getField, otpElement))
return otpElement return otpElement
return null return null
} }
@@ -126,7 +141,7 @@ object OtpEntryFields {
private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val otpPlainText = getField(OTP_FIELD) val otpPlainText = getField(OTP_FIELD)
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) { if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
val uri = Uri.parse(removeSpaceChars(otpPlainText)) val uri = Uri.parse(otpPlainText.removeSpaceChars())
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) { if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
Log.e(TAG, "Invalid or missing scheme in uri") Log.e(TAG, "Invalid or missing scheme in uri")
@@ -159,16 +174,16 @@ object OtpEntryFields {
if (nameParam != null && nameParam.isNotEmpty()) { if (nameParam != null && nameParam.isNotEmpty()) {
val userIdArray = nameParam.split(":", "%3A") val userIdArray = nameParam.split(":", "%3A")
if (userIdArray.size > 1) { if (userIdArray.size > 1) {
otpElement.issuer = removeLineChars(userIdArray[0]) otpElement.issuer = userIdArray[0].removeLineChars()
otpElement.name = removeLineChars(userIdArray[1]) otpElement.name = userIdArray[1].removeLineChars()
} else { } else {
otpElement.name = removeLineChars(nameParam) otpElement.name = nameParam.removeLineChars()
} }
} }
val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM) val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM)
if (issuerParam != null && issuerParam.isNotEmpty()) if (issuerParam != null && issuerParam.isNotEmpty())
otpElement.issuer = removeLineChars(issuerParam) otpElement.issuer = issuerParam.removeLineChars()
val secretParam = uri.getQueryParameter(SECRET_URL_PARAM) val secretParam = uri.getQueryParameter(SECRET_URL_PARAM)
if (secretParam != null && secretParam.isNotEmpty()) { if (secretParam != null && secretParam.isNotEmpty()) {
@@ -247,8 +262,9 @@ object OtpEntryFields {
encodeParameter(username) encodeParameter(username)
else else
encodeParameter(otpElement.name) encodeParameter(otpElement.name)
val secret = encodeParameter(otpElement.getBase32Secret())
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer%3A$accountName" + val uriString = StringBuilder("otpauth://$otpAuthority/$issuer%3A$accountName" +
"?$SECRET_URL_PARAM=${otpElement.getBase32Secret()}" + "?$SECRET_URL_PARAM=${secret}" +
"&$counterOrPeriodLabel=$counterOrPeriodValue" + "&$counterOrPeriodLabel=$counterOrPeriodValue" +
"&$DIGITS_URL_PARAM=${otpElement.digits}" + "&$DIGITS_URL_PARAM=${otpElement.digits}" +
"&$ISSUER_URL_PARAM=$issuer") "&$ISSUER_URL_PARAM=$issuer")
@@ -262,7 +278,40 @@ object OtpEntryFields {
} }
private fun encodeParameter(parameter: String): String { private fun encodeParameter(parameter: String): String {
return Uri.encode(OtpElement.removeLineChars(parameter)) return Uri.encode(parameter.removeLineChars())
}
private fun parseTOTPFromOfficialField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val secretField = getField(TIMEOTP_SECRET_FIELD)
val secretHexField = getField(TIMEOTP_SECRET_HEX_FIELD)
val secretBase32Field = getField(TIMEOTP_SECRET_BASE32_FIELD)
val secretBase64Field = getField(TIMEOTP_SECRET_BASE64_FIELD)
val lengthField = getField(TIMEOTP_LENGTH_FIELD)
val periodField = getField(TIMEOTP_PERIOD_FIELD)
val algorithmField = getField(TIMEOTP_ALGORITHM_FIELD)
try {
when {
secretField != null -> otpElement.setUTF8Secret(secretField)
secretHexField != null -> otpElement.setHexSecret(secretHexField)
secretBase32Field != null -> otpElement.setBase32Secret(secretBase32Field)
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
lengthField != null -> otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS
periodField != null -> otpElement.period = periodField.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
algorithmField != null -> otpElement.algorithm =
when (algorithmField.toUpperCase(Locale.ENGLISH)) {
TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1
TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256
TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512
else -> HashAlgorithm.SHA1
}
else -> return false
}
} catch (exception: Exception) {
return false
}
otpElement.type = OtpType.TOTP
return true
} }
private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
@@ -290,7 +339,7 @@ object OtpEntryFields {
return false return false
} }
private fun parseTOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { private fun parseTOTPFromPluginField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val seedField = getField(TOTP_SEED_FIELD) ?: return false val seedField = getField(TOTP_SEED_FIELD) ?: return false
try { try {
otpElement.setBase32Secret(seedField) otpElement.setBase32Secret(seedField)
@@ -316,7 +365,7 @@ object OtpEntryFields {
return true return true
} }
private fun parseHOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { private fun parseHOTPFromOfficialField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val secretField = getField(HMACOTP_SECRET_FIELD) val secretField = getField(HMACOTP_SECRET_FIELD)
val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD) val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD)
val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD) val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD)
@@ -382,25 +431,43 @@ object OtpEntryFields {
val totpSeedField = Field(TOTP_SEED_FIELD) val totpSeedField = Field(TOTP_SEED_FIELD)
val totpSettingField = Field(TOTP_SETTING_FIELD) val totpSettingField = Field(TOTP_SETTING_FIELD)
val hmacOtpSecretField = Field(HMACOTP_SECRET_FIELD) val hmacOtpSecretField = Field(HMACOTP_SECRET_FIELD)
val hmacOtpSecretHewField = Field(HMACOTP_SECRET_HEX_FIELD) val hmacOtpSecretHexField = Field(HMACOTP_SECRET_HEX_FIELD)
val hmacOtpSecretBase32Field = Field(HMACOTP_SECRET_BASE32_FIELD) val hmacOtpSecretBase32Field = Field(HMACOTP_SECRET_BASE32_FIELD)
val hmacOtpSecretBase64Field = Field(HMACOTP_SECRET_BASE64_FIELD) val hmacOtpSecretBase64Field = Field(HMACOTP_SECRET_BASE64_FIELD)
val hmacOtpSecretCounterField = Field(HMACOTP_SECRET_COUNTER_FIELD) val hmacOtpSecretCounterField = Field(HMACOTP_SECRET_COUNTER_FIELD)
val timeOtpSecretField = Field(TIMEOTP_SECRET_FIELD)
val timeOtpSecretHexField = Field(TIMEOTP_SECRET_HEX_FIELD)
val timeOtpSecretBase32Field = Field(TIMEOTP_SECRET_BASE32_FIELD)
val timeOtpSecretBase64Field = Field(TIMEOTP_SECRET_BASE64_FIELD)
val timeOtpLengthField = Field(TIMEOTP_LENGTH_FIELD)
val timeOtpPeriodField = Field(TIMEOTP_PERIOD_FIELD)
val timeOtpAlgorithmField = Field(TIMEOTP_ALGORITHM_FIELD)
newCustomFields.remove(otpField) newCustomFields.remove(otpField)
newCustomFields.remove(totpSeedField) newCustomFields.remove(totpSeedField)
newCustomFields.remove(totpSettingField) newCustomFields.remove(totpSettingField)
newCustomFields.remove(hmacOtpSecretField) newCustomFields.remove(hmacOtpSecretField)
newCustomFields.remove(hmacOtpSecretHewField) newCustomFields.remove(hmacOtpSecretHexField)
newCustomFields.remove(hmacOtpSecretBase32Field) newCustomFields.remove(hmacOtpSecretBase32Field)
newCustomFields.remove(hmacOtpSecretBase64Field) newCustomFields.remove(hmacOtpSecretBase64Field)
newCustomFields.remove(hmacOtpSecretCounterField) newCustomFields.remove(hmacOtpSecretCounterField)
newCustomFields.remove(timeOtpSecretField)
newCustomFields.remove(timeOtpSecretHexField)
newCustomFields.remove(timeOtpSecretBase32Field)
newCustomFields.remove(timeOtpSecretBase64Field)
newCustomFields.remove(timeOtpLengthField)
newCustomFields.remove(timeOtpPeriodField)
newCustomFields.remove(timeOtpAlgorithmField)
// Empty auto generated OTP Token field // Empty auto generated OTP Token field
if (fieldsToParse.contains(otpField) if (fieldsToParse.contains(otpField)
|| fieldsToParse.contains(totpSeedField) || fieldsToParse.contains(totpSeedField)
|| fieldsToParse.contains(hmacOtpSecretField) || fieldsToParse.contains(hmacOtpSecretField)
|| fieldsToParse.contains(hmacOtpSecretHewField) || fieldsToParse.contains(hmacOtpSecretHexField)
|| fieldsToParse.contains(hmacOtpSecretBase32Field) || fieldsToParse.contains(hmacOtpSecretBase32Field)
|| fieldsToParse.contains(hmacOtpSecretBase64Field) || fieldsToParse.contains(hmacOtpSecretBase64Field)
|| fieldsToParse.contains(timeOtpSecretField)
|| fieldsToParse.contains(timeOtpSecretHexField)
|| fieldsToParse.contains(timeOtpSecretBase32Field)
|| fieldsToParse.contains(timeOtpSecretBase64Field)
) )
newCustomFields.add(Field(OTP_TOKEN_FIELD)) newCustomFields.add(Field(OTP_TOKEN_FIELD))
return newCustomFields return newCustomFields

View File

@@ -19,10 +19,12 @@
*/ */
package com.kunzisoft.keepass.settings package com.kunzisoft.keepass.settings
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
@@ -32,6 +34,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// Load the preferences from an XML resource // Load the preferences from an XML resource
setPreferencesFromResource(R.xml.preferences_autofill, rootKey) setPreferencesFromResource(R.xml.preferences_autofill, rootKey)
val autofillInlineSuggestionsPreference: SwitchPreference? = findPreference(getString(R.string.autofill_inline_suggestions_key))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
autofillInlineSuggestionsPreference?.isVisible = false
}
} }
override fun onDisplayPreferenceDialog(preference: Preference?) { override fun onDisplayPreferenceDialog(preference: Preference?) {

View File

@@ -103,6 +103,6 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
} }
interface Callback { interface Callback {
fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean = false)
} }
} }

View File

@@ -41,7 +41,7 @@ import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.icons.IconPackChooser import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
@@ -218,7 +218,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val tempAdvancedUnlockPreference: SwitchPreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key)) val tempAdvancedUnlockPreference: SwitchPreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
BiometricUnlockDatabaseHelper.biometricUnlockSupported(activity) AdvancedUnlockManager.biometricUnlockSupported(activity)
} else false } else false
biometricUnlockEnablePreference?.apply { biometricUnlockEnablePreference?.apply {
// False if under Marshmallow // False if under Marshmallow
@@ -258,15 +258,18 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} }
} }
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
BiometricUnlockDatabaseHelper.deviceCredentialUnlockSupported(activity) AdvancedUnlockManager.deviceCredentialUnlockSupported(activity)
} else false } else false
deviceCredentialUnlockEnablePreference?.apply { deviceCredentialUnlockEnablePreference?.apply {
// Biometric unlock already checked
if (biometricUnlockEnablePreference?.isChecked == true)
isChecked = false
if (!deviceCredentialUnlockSupported) { if (!deviceCredentialUnlockSupported) {
isChecked = false isChecked = false
setOnPreferenceClickListener { preference -> setOnPreferenceClickListener { preference ->
(preference as SwitchPreference).isChecked = false (preference as SwitchPreference).isChecked = false
UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.R) UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.M)
.show(parentFragmentManager, "unavailableFeatureDialog") .show(parentFragmentManager, "unavailableFeatureDialog")
false false
} }
@@ -337,9 +340,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
validate?.invoke() validate?.invoke()
deleteKeysAlertDialog?.setOnDismissListener(null) deleteKeysAlertDialog?.setOnDismissListener(null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric( AdvancedUnlockManager.deleteEntryKeyInKeystoreForBiometric(
activity, activity,
object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback { object : AdvancedUnlockManager.AdvancedUnlockErrorCallback {
fun showException(e: Exception) { fun showException(e: Exception) {
Toast.makeText(context, Toast.makeText(context,
getString(R.string.advanced_unlock_scanning_error, e.localizedMessage), getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
@@ -350,7 +353,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
showException(e) showException(e)
} }
override fun onBiometricException(e: Exception) { override fun onGenericException(e: Exception) {
showException(e) showException(e)
} }
}) })
@@ -383,7 +386,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} }
if (styleEnabled) { if (styleEnabled) {
Stylish.assignStyle(styleIdString) Stylish.assignStyle(styleIdString)
activity.recreate() // Relaunch the current activity to redraw theme
(activity as? SettingsActivity?)?.apply {
keepCurrentScreen()
startActivity(intent)
finish()
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
} }
styleEnabled styleEnabled
} }

View File

@@ -58,7 +58,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
private var mEncryptionAlgorithmPref: DialogListExplanationPreference? = null private var mEncryptionAlgorithmPref: DialogListExplanationPreference? = null
private var mKeyDerivationPref: DialogListExplanationPreference? = null private var mKeyDerivationPref: DialogListExplanationPreference? = null
private var mRoundPref: InputKdfNumberPreference? = null private var mRoundPref: InputKdfNumberPreference? = null
private var mMemoryPref: InputKdfNumberPreference? = null private var mMemoryPref: InputKdfSizePreference? = null
private var mParallelismPref: InputKdfNumberPreference? = null private var mParallelismPref: InputKdfNumberPreference? = null
override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) { override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) {
@@ -231,7 +231,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
} }
// Memory Usage // Memory Usage
mMemoryPref = findPreference<InputKdfNumberPreference>(getString(R.string.memory_usage_key))?.apply { mMemoryPref = findPreference<InputKdfSizePreference>(getString(R.string.memory_usage_key))?.apply {
summary = mDatabase.memoryUsage.toString() summary = mDatabase.memoryUsage.toString()
} }
@@ -552,6 +552,13 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly) settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly)
true true
} }
R.id.menu_reload_database -> {
settingActivity?.apply {
keepCurrentScreen()
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
}
return true
}
else -> { else -> {
// Check the time lock before launching settings // Check the time lock before launching settings

View File

@@ -35,9 +35,13 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
} }
fun getScreen(): Screen {
return Screen.values()[requireArguments().getInt(TAG_KEY)]
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
onCreateScreenPreference( onCreateScreenPreference(
Screen.values()[requireArguments().getInt(TAG_KEY)], getScreen(),
savedInstanceState, savedInstanceState,
rootKey) rootKey)
} }

View File

@@ -26,6 +26,7 @@ import android.net.Uri
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import java.util.* import java.util.*
@@ -240,14 +241,23 @@ object PreferencesUtil {
fun isBiometricUnlockEnable(context: Context): Boolean { fun isBiometricUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val biometricSupported = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
AdvancedUnlockManager.biometricUnlockSupported(context)
} else {
false
}
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key), return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
context.resources.getBoolean(R.bool.biometric_unlock_enable_default)) context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
&& biometricSupported
} }
fun isDeviceCredentialUnlockEnable(context: Context): Boolean { fun isDeviceCredentialUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Priority to biometric unlock
val biometricAlreadySupported = isBiometricUnlockEnable(context)
return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key), return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key),
context.resources.getBoolean(R.bool.device_credential_unlock_enable_default)) context.resources.getBoolean(R.bool.device_credential_unlock_enable_default))
&& !biometricAlreadySupported
} }
fun isTempAdvancedUnlockEnable(context: Context): Boolean { fun isTempAdvancedUnlockEnable(context: Context): Boolean {
@@ -426,13 +436,18 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.autofill_close_database_default)) context.resources.getBoolean(R.bool.autofill_close_database_default))
} }
fun isAutofillAutoSearchEnable(context: Context): Boolean { fun isAutofillAutoSearchEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key), return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key),
context.resources.getBoolean(R.bool.autofill_auto_search_default)) context.resources.getBoolean(R.bool.autofill_auto_search_default))
} }
fun isAutofillInlineSuggestionsEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_inline_suggestions_key),
context.resources.getBoolean(R.bool.autofill_inline_suggestions_default))
}
fun isAutofillSaveSearchInfoEnable(context: Context): Boolean { fun isAutofillSaveSearchInfoEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key), return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key),

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2020 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePassDX. * This file is part of KeePassDX.
* *
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.settings
import android.app.Activity import android.app.Activity
import android.app.backup.BackupManager import android.app.backup.BackupManager
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@@ -35,7 +34,9 @@ import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionError import com.kunzisoft.keepass.view.showActionError
@@ -81,7 +82,7 @@ open class SettingsActivity
} }
// Focus view to reinitialize timeout // Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout) coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
if (savedInstanceState == null) { if (savedInstanceState == null) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
@@ -94,12 +95,30 @@ open class SettingsActivity
backupManager = BackupManager(this) backupManager = BackupManager(this)
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result -> mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
// Call result in fragment when (actionTask) {
(supportFragmentManager DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?) // Reload the current activity
?.onProgressDialogThreadResult(actionTask, result) startActivity(intent)
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
else -> {
// Call result in fragment
(supportFragmentManager
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
?.onProgressDialogThreadResult(actionTask, result)
coordinatorLayout?.showActionError(result)
}
}
}
coordinatorLayout?.showActionError(result) // To reload the current screen
if (intent.extras?.containsKey(FRAGMENT_ARG) == true) {
intent.extras?.getString(FRAGMENT_ARG)?.let { fragmentScreenName ->
onNestedPreferenceSelected(NestedSettingsFragment.Screen.valueOf(fragmentScreenName), true)
}
// Eat state
intent.removeExtra(FRAGMENT_ARG)
} }
} }
@@ -192,25 +211,41 @@ open class SettingsActivity
hideOrShowLockButton(NestedSettingsFragment.Screen.APPLICATION) hideOrShowLockButton(NestedSettingsFragment.Screen.APPLICATION)
} }
private fun replaceFragment(key: NestedSettingsFragment.Screen) { private fun replaceFragment(key: NestedSettingsFragment.Screen, reload: Boolean) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction().apply {
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, if (reload) {
setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out,
R.anim.slide_in_left, R.anim.slide_out_right) R.anim.slide_in_left, R.anim.slide_out_right)
.replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, mReadOnly), TAG_NESTED) } else {
.addToBackStack(TAG_NESTED) setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left,
.commit() R.anim.slide_in_left, R.anim.slide_out_right)
}
replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, mReadOnly), TAG_NESTED)
addToBackStack(TAG_NESTED)
commit()
}
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key) toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
hideOrShowLockButton(key) hideOrShowLockButton(key)
} }
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) { /**
* To keep the current screen when activity is reloaded
*/
fun keepCurrentScreen() {
(supportFragmentManager.findFragmentByTag(TAG_NESTED) as? NestedSettingsFragment?)
?.getScreen()?.let { fragmentKey ->
intent.putExtra(FRAGMENT_ARG, fragmentKey.name)
}
}
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean) {
if (mTimeoutEnable) if (mTimeoutEnable)
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) { TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
replaceFragment(key) replaceFragment(key, reload)
} }
else else
replaceFragment(key) replaceFragment(key, reload)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@@ -225,6 +260,7 @@ open class SettingsActivity
private const val SHOW_LOCK = "SHOW_LOCK" private const val SHOW_LOCK = "SHOW_LOCK"
private const val TITLE_KEY = "TITLE_KEY" private const val TITLE_KEY = "TITLE_KEY"
private const val TAG_NESTED = "TAG_NESTED" private const val TAG_NESTED = "TAG_NESTED"
private const val FRAGMENT_ARG = "FRAGMENT_ARG"
fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) { fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) {
val intent = Intent(activity, SettingsActivity::class.java) val intent = Intent(activity, SettingsActivity::class.java)

View File

@@ -25,7 +25,7 @@ import androidx.preference.DialogPreference
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
class InputKdfNumberPreference @JvmOverloads constructor(context: Context, open class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.settings.preference
import android.content.Context
import android.util.AttributeSet
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
class InputKdfSizePreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun setSummary(summary: CharSequence) {
if (summary == UNKNOWN_VALUE_STRING) {
super.setSummary("")
} else {
var summaryString = summary
try {
val memorySize = summary.toString().toLong()
summaryString = if (memorySize > 0) {
// To convert bytes to mebibytes
DataByte(memorySize, DataByte.ByteFormat.BYTE)
.toBetterByteFormat().toString(context)
} else {
memorySize.toString()
}
} catch (e: Exception) {
} finally {
super.setSummary(summaryString)
}
}
}
}

View File

@@ -36,7 +36,7 @@ open class InputNumberPreference @JvmOverloads constructor(context: Context,
override fun setSummary(summary: CharSequence) { override fun setSummary(summary: CharSequence) {
if (summary == INFINITE_VALUE_STRING) { if (summary == INFINITE_VALUE_STRING) {
super.setSummary("") super.setSummary("")
} else { } else {
super.setSummary(summary) super.setSummary(summary)
} }

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.settings.preference
import android.content.Context
import android.util.AttributeSet
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
open class InputSizePreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun setSummary(summary: CharSequence) {
var summaryString = summary
try {
val memorySize = summary.toString().toLong()
summaryString = if (memorySize >= 0) {
// To convert bytes to mebibytes
DataByte(memorySize, DataByte.ByteFormat.BYTE)
.toBetterByteFormat().toString(context)
} else {
memorySize.toString()
}
} catch (e: Exception) {
} finally {
super.setSummary(summaryString)
}
}
}

View File

@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.R
abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() { abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() {
private var inputTextView: EditText? = null private var inputTextView: EditText? = null
private var textUnitView: TextView? = null
private var textExplanationView: TextView? = null private var textExplanationView: TextView? = null
private var switchElementView: CompoundButton? = null private var switchElementView: CompoundButton? = null
@@ -47,6 +48,14 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
} }
} }
fun setInoutText(@StringRes inputTextId: Int) {
inputText = getString(inputTextId)
}
fun showInputText(show: Boolean) {
inputTextView?.visibility = if (show) View.VISIBLE else View.GONE
}
fun setInputTextError(error: CharSequence) { fun setInputTextError(error: CharSequence) {
this.inputTextView?.error = error this.inputTextView?.error = error
} }
@@ -55,6 +64,24 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
this.mOnInputTextEditorActionListener = onEditorActionListener this.mOnInputTextEditorActionListener = onEditorActionListener
} }
var unitText: String?
get() = textUnitView?.text?.toString() ?: ""
set(unitText) {
textUnitView?.apply {
if (unitText != null && unitText.isNotEmpty()) {
text = unitText
visibility = View.VISIBLE
} else {
text = ""
visibility = View.GONE
}
}
}
fun setUnitText(@StringRes unitTextId: Int) {
unitText = getString(unitTextId)
}
var explanationText: String? var explanationText: String?
get() = textExplanationView?.text?.toString() ?: "" get() = textExplanationView?.text?.toString() ?: ""
set(explanationText) { set(explanationText) {
@@ -69,6 +96,10 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
} }
} }
fun setExplanationText(@StringRes explanationTextId: Int) {
explanationText = getString(explanationTextId)
}
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
@@ -93,6 +124,8 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
} }
} }
} }
textUnitView = view.findViewById(R.id.input_text_unit)
textUnitView?.visibility = View.GONE
textExplanationView = view.findViewById(R.id.explanation_text) textExplanationView = view.findViewById(R.id.explanation_text)
textExplanationView?.visibility = View.GONE textExplanationView?.visibility = View.GONE
switchElementView = view.findViewById(R.id.switch_element) switchElementView = view.findViewById(R.id.switch_element)
@@ -113,18 +146,6 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
return false return false
} }
fun setInoutText(@StringRes inputTextId: Int) {
inputText = getString(inputTextId)
}
fun showInputText(show: Boolean) {
inputTextView?.visibility = if (show) View.VISIBLE else View.GONE
}
fun setExplanationText(@StringRes explanationTextId: Int) {
explanationText = getString(explanationTextId)
}
fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) { fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) {
switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE
switchElementView?.isChecked = defaultChecked switchElementView?.isChecked = defaultChecked

View File

@@ -22,50 +22,76 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
private var dataByte = DataByte(2L, DataByte.ByteFormat.MEBIBYTE)
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
setExplanationText(R.string.max_history_size_summary) setExplanationText(R.string.max_history_size_summary)
database?.historyMaxSize?.let { maxItemsDatabase -> database?.historyMaxSize?.let { maxItemsDatabase ->
inputText = maxItemsDatabase.toString() dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
if (dataByte.number >= 0) {
setUnitText(dataByte.format.stringId)
} else {
unitText = null
}
setSwitchAction({ isChecked -> setSwitchAction({ isChecked ->
inputText = if (!isChecked) { if (!isChecked) {
INFINITE_MAX_HISTORY_SIZE.toString() dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
} else inputText = INFINITE_MAX_HISTORY_SIZE.toString()
DEFAULT_MAX_HISTORY_SIZE.toString() unitText = null
} else {
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
showInputText(isChecked) showInputText(isChecked)
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE) }, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
} }
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) { if (positiveResult) {
database?.let { database -> database?.let { database ->
var maxHistorySize: Long = try { val maxHistorySize: Long = try {
inputText.toLong() inputText.toLong()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
DEFAULT_MAX_HISTORY_SIZE DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE.toBytes()
} }
if (maxHistorySize < INFINITE_MAX_HISTORY_SIZE) { val numberOfBytes = if (maxHistorySize >= 0) {
maxHistorySize = INFINITE_MAX_HISTORY_SIZE val dataByteConversion = DataByte(maxHistorySize, dataByte.format)
var bytes = dataByteConversion.toBytes()
if (bytes > Long.MAX_VALUE) {
bytes = Long.MAX_VALUE
}
bytes
} else {
INFINITE_MAX_HISTORY_SIZE
} }
val oldMaxHistorySize = database.historyMaxSize val oldMaxHistorySize = database.historyMaxSize
database.historyMaxSize = maxHistorySize database.historyMaxSize = numberOfBytes
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable) mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, numberOfBytes, mDatabaseAutoSaveEnable)
} }
} }
} }
companion object { companion object {
const val DEFAULT_MAX_HISTORY_SIZE = 134217728L
const val INFINITE_MAX_HISTORY_SIZE = -1L const val INFINITE_MAX_HISTORY_SIZE = -1L
private val INFINITE_MAX_HISTORY_SIZE_DATA_BYTE = DataByte(INFINITE_MAX_HISTORY_SIZE, DataByte.ByteFormat.MEBIBYTE)
private val DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE = DataByte(6L, DataByte.ByteFormat.MEBIBYTE)
fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat { fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat {
val fragment = MaxHistorySizePreferenceDialogFragmentCompat() val fragment = MaxHistorySizePreferenceDialogFragmentCompat()
val bundle = Bundle(1) val bundle = Bundle(1)

View File

@@ -22,33 +22,46 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
private var dataByte = DataByte(MIN_MEMORY_USAGE, DataByte.ByteFormat.BYTE)
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
setExplanationText(R.string.memory_usage_explanation) setExplanationText(R.string.memory_usage_explanation)
inputText = database?.memoryUsage?.toString()?: MIN_MEMORY_USAGE.toString()
val memoryBytes = database?.memoryUsage ?: MIN_MEMORY_USAGE
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) { if (positiveResult) {
database?.let { database -> database?.let { database ->
var memoryUsage: Long = try { var newMemoryUsage: Long = try {
inputText.toLong() inputText.toLong()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
MIN_MEMORY_USAGE MIN_MEMORY_USAGE
} }
if (memoryUsage < MIN_MEMORY_USAGE) { if (newMemoryUsage < MIN_MEMORY_USAGE) {
memoryUsage = MIN_MEMORY_USAGE newMemoryUsage = MIN_MEMORY_USAGE
}
// To transform in bytes
dataByte.number = newMemoryUsage
var numberOfBytes = dataByte.toBytes()
if (numberOfBytes > Long.MAX_VALUE) {
numberOfBytes = Long.MAX_VALUE
} }
// TODO Max Memory
val oldMemoryUsage = database.memoryUsage val oldMemoryUsage = database.memoryUsage
database.memoryUsage = memoryUsage database.memoryUsage = numberOfBytes
mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable) mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, numberOfBytes, mDatabaseAutoSaveEnable)
} }
} }
} }

View File

@@ -138,5 +138,5 @@ fun Context.closeDatabase() {
cancelAll() cancelAll()
} }
// Clear data // Clear data
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this)) Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
} }

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2021 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.utils
import android.content.Context
import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
class DataByte(var number: Long, var format: ByteFormat) {
fun toBetterByteFormat(): DataByte {
return when (this.format) {
ByteFormat.BYTE -> {
when {
//this.number % GIBIBYTES == 0L -> {
// DataByte((this.number / GIBIBYTES), ByteFormat.GIBIBYTE)
//}
this.number % MEBIBYTES == 0L -> {
DataByte((this.number / MEBIBYTES), ByteFormat.MEBIBYTE)
}
this.number % KIBIBYTES == 0L -> {
DataByte((this.number / KIBIBYTES), ByteFormat.KIBIBYTE)
}
else -> {
DataByte(this.number, ByteFormat.BYTE)
}
}
}
else -> {
DataByte(toBytes(), ByteFormat.BYTE).toBetterByteFormat()
}
}
}
/**
* Number of bytes in current DataByte
*/
fun toBytes(): Long {
return when (this.format) {
ByteFormat.BYTE -> this.number
ByteFormat.KIBIBYTE -> this.number * KIBIBYTES
ByteFormat.MEBIBYTE -> this.number * MEBIBYTES
//ByteFormat.GIBIBYTE -> this.number * GIBIBYTES
}
}
override fun toString(): String {
return "$number ${format.name}"
}
fun toString(context: Context): String {
return "$number ${context.getString(format.stringId)}"
}
enum class ByteFormat(@StringRes var stringId: Int) {
BYTE(R.string.unit_byte),
KIBIBYTE(R.string.unit_kibibyte),
MEBIBYTE(R.string.unit_mebibyte)
//GIBIBYTE(R.string.unit_gibibyte)
}
companion object {
const val KIBIBYTES = 1024L
const val MEBIBYTES = 1048576L
const val GIBIBYTES = 1073741824L
}
}

View File

@@ -53,23 +53,19 @@ object MenuUtil {
fun onDefaultMenuOptionsItemSelected(activity: Activity, fun onDefaultMenuOptionsItemSelected(activity: Activity,
item: MenuItem, item: MenuItem,
readOnly: Boolean = READ_ONLY_DEFAULT, readOnly: Boolean = READ_ONLY_DEFAULT,
timeoutEnable: Boolean = false): Boolean { timeoutEnable: Boolean = false) {
when (item.itemId) { when (item.itemId) {
R.id.menu_contribute -> { R.id.menu_contribute -> {
onContributionItemSelected(activity) onContributionItemSelected(activity)
return true
} }
R.id.menu_app_settings -> { R.id.menu_app_settings -> {
// To avoid flickering when launch settings in a LockingActivity // To avoid flickering when launch settings in a LockingActivity
SettingsActivity.launch(activity, readOnly, timeoutEnable) SettingsActivity.launch(activity, readOnly, timeoutEnable)
return true
} }
R.id.menu_about -> { R.id.menu_about -> {
val intent = Intent(activity, AboutActivity::class.java) val intent = Intent(activity, AboutActivity::class.java)
activity.startActivity(intent) activity.startActivity(intent)
return true
} }
else -> return true
} }
} }
} }

View File

@@ -0,0 +1,14 @@
package com.kunzisoft.keepass.utils
object StringUtil {
fun String.removeLineChars(): String {
return this.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "")
}
fun String.removeSpaceChars(): String {
return this.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
}
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
}

View File

@@ -48,8 +48,8 @@ import java.util.*
class EntryContentsView @JvmOverloads constructor(context: Context, class EntryContentsView @JvmOverloads constructor(context: Context,
var attrs: AttributeSet? = null, attrs: AttributeSet? = null,
var defStyle: Int = 0) defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) { : LinearLayout(context, attrs, defStyle) {
private var fontInVisibility: Boolean = false private var fontInVisibility: Boolean = false
@@ -67,7 +67,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val creationDateView: TextView private val creationDateView: TextView
private val modificationDateView: TextView private val modificationDateView: TextView
private val lastAccessDateView: TextView
private val expiresImageView: ImageView private val expiresImageView: ImageView
private val expiresDateView: TextView private val expiresDateView: TextView
@@ -117,7 +116,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
creationDateView = findViewById(R.id.entry_created) creationDateView = findViewById(R.id.entry_created)
modificationDateView = findViewById(R.id.entry_modified) modificationDateView = findViewById(R.id.entry_modified)
lastAccessDateView = findViewById(R.id.entry_accessed)
expiresImageView = findViewById(R.id.entry_expires_image) expiresImageView = findViewById(R.id.entry_expires_image)
expiresDateView = findViewById(R.id.entry_expires_date) expiresDateView = findViewById(R.id.entry_expires_date)
@@ -258,20 +256,13 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
modificationDateView.text = date.getDateTimeString(resources) modificationDateView.text = date.getDateTimeString(resources)
} }
fun assignLastAccessDate(date: DateInstant) { fun setExpires(isExpires: Boolean, expiryTime: DateInstant) {
lastAccessDateView.text = date.getDateTimeString(resources)
}
fun setExpires(isExpires: Boolean) {
expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE
} expiresDateView.text = if (isExpires) {
expiryTime.getDateTimeString(resources)
fun assignExpiresDate(date: DateInstant) { } else {
assignExpiresDate(date.getDateTimeString(resources)) resources.getString(R.string.never)
} }
fun assignExpiresDate(constString: String) {
expiresDateView.text = constString
} }
fun assignUUID(uuid: UUID) { fun assignUUID(uuid: UUID) {
@@ -279,7 +270,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
uuidReferenceView.text = UuidUtil.toHexString(uuid) uuidReferenceView.text = UuidUtil.toHexString(uuid)
} }
fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) { fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) {
passwordFieldView.hiddenProtectedValue = hiddenProtectedValue passwordFieldView.hiddenProtectedValue = hiddenProtectedValue
// Hidden style for custom fields // Hidden style for custom fields
@@ -306,7 +296,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
allowCopy: Boolean, allowCopy: Boolean,
onCopyButtonClickListener: OnClickListener?) { onCopyButtonClickListener: OnClickListener?) {
val entryCustomField: EntryField? = EntryField(context, attrs, defStyle) val entryCustomField: EntryField? = EntryField(context)
entryCustomField?.apply { entryCustomField?.apply {
setLabel(title) setLabel(title)
setValue(value.toString(), value.isProtected) setValue(value.toString(), value.isProtected)

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
@@ -20,7 +19,6 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
private val keyFileNameInputLayout: TextInputLayout private val keyFileNameInputLayout: TextInputLayout
private val keyFileNameView: TextView private val keyFileNameView: TextView
private val keyFileOpenView: ImageView
init { init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
@@ -28,7 +26,6 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
keyFileNameInputLayout = findViewById(R.id.input_entry_keyfile) keyFileNameInputLayout = findViewById(R.id.input_entry_keyfile)
keyFileNameView = findViewById(R.id.keyfile_name) keyFileNameView = findViewById(R.id.keyfile_name)
keyFileOpenView = findViewById(R.id.keyfile_open_button)
} }
override fun setOnClickListener(l: OnClickListener?) { override fun setOnClickListener(l: OnClickListener?) {

View File

@@ -23,7 +23,6 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.text.format.Formatter import android.text.format.Formatter
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import java.io.Serializable import java.io.Serializable
import java.text.DateFormat import java.text.DateFormat
@@ -58,7 +57,11 @@ class FileDatabaseInfo : Serializable {
} }
private set private set
fun getModificationString(): String? { fun getLastModification(): Long? {
return documentFile?.lastModified()
}
fun getLastModificationString(): String? {
return documentFile?.lastModified()?.let { return documentFile?.lastModified()?.let {
if (it != 0L) { if (it != 0L) {
DateFormat.getDateTimeInstance() DateFormat.getDateTimeInstance()
@@ -69,6 +72,10 @@ class FileDatabaseInfo : Serializable {
} }
} }
fun getSize(): Long? {
return documentFile?.length()
}
fun getSizeString(): String? { fun getSizeString(): String? {
return documentFile?.let { return documentFile?.let {
Formatter.formatFileSize(context, it.length()) Formatter.formatFileSize(context, it.length())

View File

@@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportHeight="24"
android:viewportWidth="24" >
<path android:fillColor="#FFFFFF" android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
</vector>

View File

@@ -68,17 +68,10 @@
android:padding="0dp" android:padding="0dp"
android:contentDescription="@string/about" android:contentDescription="@string/about"
android:src="@drawable/ic_launcher_foreground"/> android:src="@drawable/ic_launcher_foreground"/>
<FrameLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_advanced_unlock_container_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent" />
<com.kunzisoft.keepass.view.AdvancedUnlockInfoView
android:id="@+id/biometric_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/colorPrimary"
android:visibility="gone"/>
</FrameLayout>
</FrameLayout> </FrameLayout>
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
@@ -164,8 +157,8 @@
android:inputType="textPassword" android:inputType="textPassword"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:importantForAutofill="yes" android:importantForAutofill="yes"
android:autofillHints="password|" android:autofillHints="password"
android:imeOptions="actionDone" android:imeOptions="actionDone|flagNoPersonalizedLearning"
android:maxLines="1"/> android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
</RelativeLayout> </RelativeLayout>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.kunzisoft.keepass.view.AdvancedUnlockInfoView
android:id="@+id/advanced_unlock_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/colorPrimary"
android:visibility="gone"/>
</FrameLayout>

View File

@@ -106,6 +106,7 @@
android:inputType="textPassword|textMultiLine" android:inputType="textPassword|textMultiLine"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:importantForAutofill="no" android:importantForAutofill="no"
android:imeOptions="flagNoPersonalizedLearning"
android:ems="10" android:ems="10"
android:maxLines="10" android:maxLines="10"
android:hint="@string/entry_password"/> android:hint="@string/entry_password"/>

View File

@@ -29,6 +29,18 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants" android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o"> tools:targetApi="o">
<TextView
android:id="@+id/setup_otp_type_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/card_view_margin"
android:layout_marginLeft="@dimen/card_view_margin"
android:layout_marginEnd="@dimen/card_view_margin"
android:layout_marginRight="@dimen/card_view_margin"
android:text="@string/error_otp_type"
style="@style/KeepassDXStyle.TextAppearance.WarningTextStyle"/>
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/card_view_otp_selection" android:id="@+id/card_view_otp_selection"
android:layout_margin="4dp" android:layout_margin="4dp"

View File

@@ -36,6 +36,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textMultiLine" android:inputType="textMultiLine"
android:imeOptions="flagNoPersonalizedLearning"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:importantForAccessibility="no" android:importantForAccessibility="no"
@@ -46,9 +47,9 @@
android:id="@+id/entry_extra_field_edit" android:id="@+id/entry_extra_field_edit"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignTop="@+id/entry_extra_field_value_container"
android:src="@drawable/ic_more_white_24dp" android:src="@drawable/ic_more_white_24dp"
android:contentDescription="@string/menu_edit" android:contentDescription="@string/menu_edit"
style="@style/KeepassDXStyle.ImageButton.Simple"/> style="@style/KeepassDXStyle.ImageButton.Simple"/>

View File

@@ -46,14 +46,24 @@
app:layout_constraintTop_toBottomOf="@+id/explanation_text" app:layout_constraintTop_toBottomOf="@+id/explanation_text"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:minHeight="48dp"/> android:minHeight="48dp"/>
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
android:id="@+id/input_text" android:id="@+id/input_text"
android:layout_height="wrap_content"
android:layout_width="0dp" android:layout_width="0dp"
app:layout_constraintTop_toBottomOf="@+id/switch_element" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:minHeight="48dp"
android:digits="0123456789" android:digits="0123456789"
android:inputType="number"/> android:inputType="number"
android:minHeight="48dp"
app:layout_constraintEnd_toStartOf="@+id/input_text_unit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/switch_element" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/input_text_unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/input_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/input_text" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -175,12 +175,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/entry_accessed" android:text="@string/entry_accessed"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" /> style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:visibility="gone"
android:id="@+id/entry_accessed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<!-- Expires --> <!-- Expires -->
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView

View File

@@ -18,7 +18,7 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/keyfile_open_button"> app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/keyfile_name" android:id="@+id/keyfile_name"
@@ -33,16 +33,4 @@
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:maxLines="1"/> android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/keyfile_open_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="@string/content_description_open_file"
android:focusable="true"
android:background="@drawable/background_item_selection"
android:src="@drawable/ic_folder_white_24dp"
style="@style/KeepassDXStyle.ImageButton.Simple" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -22,6 +22,6 @@
<item android:id="@+id/menu_contribute" <item android:id="@+id/menu_contribute"
android:icon="@drawable/ic_heart_white_24dp" android:icon="@drawable/ic_heart_white_24dp"
android:title="@string/contribute" android:title="@string/contribute"
android:orderInCategory="95" android:orderInCategory="99"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
</menu> </menu>

View File

@@ -24,4 +24,9 @@
android:title="@string/menu_save_database" android:title="@string/menu_save_database"
android:orderInCategory="95" android:orderInCategory="95"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item android:id="@+id/menu_reload_database"
android:icon="@drawable/ic_reload_white_24dp"
android:title="@string/menu_reload_database"
android:orderInCategory="96"
app:showAsAction="ifRoom" />
</menu> </menu>

View File

@@ -248,7 +248,7 @@
<string name="content_description_background">الخلفية</string> <string name="content_description_background">الخلفية</string>
<string name="rounds">دورات التحويل</string> <string name="rounds">دورات التحويل</string>
<string name="rounds_explanation">توفر الدورات الاضافية ضد هجوم توليد التركيبات ،لكنها تبطئ التحميل والحفظ.</string> <string name="rounds_explanation">توفر الدورات الاضافية ضد هجوم توليد التركيبات ،لكنها تبطئ التحميل والحفظ.</string>
<string name="memory_usage_explanation">مقدار الذاكرة (بالبايت) لاستخدامها في دالة اشتقاق المفتاح.</string> <string name="memory_usage_explanation">مقدار الذاكرة لاستخدامها في دالة اشتقاق المفتاح.</string>
<string name="parallelism_explanation">درجة التوازي (عدد العمليات) لدالة اشتقاق المفتاح.</string> <string name="parallelism_explanation">درجة التوازي (عدد العمليات) لدالة اشتقاق المفتاح.</string>
<string name="sort_groups_before">مجموعات قبل</string> <string name="sort_groups_before">مجموعات قبل</string>
<string name="selection_mode">نمط التحديد</string> <string name="selection_mode">نمط التحديد</string>
@@ -404,7 +404,7 @@
<string name="settings_database_force_changing_master_key_summary">يطالبك بتغيير المفتاح الرئيسي (بالأيام)</string> <string name="settings_database_force_changing_master_key_summary">يطالبك بتغيير المفتاح الرئيسي (بالأيام)</string>
<string name="settings_database_recommend_changing_master_key_summary">اقترح تجديد المفتاح الرئيسي (بالأيام)</string> <string name="settings_database_recommend_changing_master_key_summary">اقترح تجديد المفتاح الرئيسي (بالأيام)</string>
<string name="settings_database_recommend_changing_master_key_title">اقترح التجديد</string> <string name="settings_database_recommend_changing_master_key_title">اقترح التجديد</string>
<string name="max_history_size_summary">حجم التأريخ ( بالبايت) لكل مدخل</string> <string name="max_history_size_summary">حجم التأريخ لكل مدخل</string>
<string name="max_history_size_title">الحجم الأقصى</string> <string name="max_history_size_title">الحجم الأقصى</string>
<string name="max_history_items_summary">عدد عناصر التأريخ لكل مدخل</string> <string name="max_history_items_summary">عدد عناصر التأريخ لكل مدخل</string>
<string name="education_generate_password_title">أنشئ كلمة سر قوية</string> <string name="education_generate_password_title">أنشئ كلمة سر قوية</string>

View File

@@ -175,7 +175,7 @@
<string name="settings_database_force_changing_master_key_title">Forsiraj obnovu</string> <string name="settings_database_force_changing_master_key_title">Forsiraj obnovu</string>
<string name="settings_database_recommend_changing_master_key_summary">Preporučiti promenu glavnog ključa (u danima)</string> <string name="settings_database_recommend_changing_master_key_summary">Preporučiti promenu glavnog ključa (u danima)</string>
<string name="settings_database_recommend_changing_master_key_title">Preporučiti obnavljanje</string> <string name="settings_database_recommend_changing_master_key_title">Preporučiti obnavljanje</string>
<string name="max_history_size_summary">Ograniči veličinu istorije (u bajtovima) po unosu</string> <string name="max_history_size_summary">Ograniči veličinu istorije po unosu</string>
<string name="max_history_size_title">Maksimalna veličina</string> <string name="max_history_size_title">Maksimalna veličina</string>
<string name="max_history_items_summary">Ograniči broja stavki istorije po unosu</string> <string name="max_history_items_summary">Ograniči broja stavki istorije po unosu</string>
<string name="max_history_items_title">Maksimalan broj</string> <string name="max_history_items_title">Maksimalan broj</string>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feedback">Обратна връзка</string>
<string name="encryption_algorithm">Алгоритъм за криптиране</string>
<string name="encryption">Криптиране</string>
<string name="security">Сигурност</string>
<string name="master_key">Главен ключ</string>
<string name="add_group">Добави група</string>
<string name="edit_entry">Редактирай</string>
<string name="add_entry">Добави</string>
<string name="accept">Приемам</string>
</resources>

View File

@@ -207,7 +207,7 @@
<string name="command_execution">S\'executa l\'ordre…</string> <string name="command_execution">S\'executa l\'ordre…</string>
<string name="parallelism_explanation">Grau de paral·lelisme (és a dir, nombre de fils) fets servir per la funció de derivació de la clau.</string> <string name="parallelism_explanation">Grau de paral·lelisme (és a dir, nombre de fils) fets servir per la funció de derivació de la clau.</string>
<string name="parallelism">Paral·lelisme</string> <string name="parallelism">Paral·lelisme</string>
<string name="memory_usage_explanation">Quantitat de memòria (en bytes) usada per la funció de derivació de la clau.</string> <string name="memory_usage_explanation">Quantitat de memòria usada per la funció de derivació de la clau.</string>
<string name="memory_usage">Ús de la memòria</string> <string name="memory_usage">Ús de la memòria</string>
<string name="hide_broken_locations_summary">Amaga els enllaços trencats en la llista de bases de dades recents</string> <string name="hide_broken_locations_summary">Amaga els enllaços trencats en la llista de bases de dades recents</string>
<string name="show_recent_files_summary">Mostra la ubicació de les bases de dades recents</string> <string name="show_recent_files_summary">Mostra la ubicació de les bases de dades recents</string>

View File

@@ -19,12 +19,12 @@
Czech translation by Jan Vaněk Czech translation by Jan Vaněk
--><resources> --><resources>
<string name="homepage">Domovská stránka</string> <string name="homepage">Domovská stránka</string>
<string name="about_description">Androidová verze správce hesel KeePass</string> <string name="about_description">Implementace správce hesel KeePass pro Android</string>
<string name="accept">Přijmout</string> <string name="accept">Přijmout</string>
<string name="add_entry">Přidat záznam</string> <string name="add_entry">Přidat záznam</string>
<string name="add_group">Přidat skupinu</string> <string name="add_group">Přidat skupinu</string>
<string name="encryption_algorithm">Šifrovací algoritmus</string> <string name="encryption_algorithm">Šifrovací algoritmus</string>
<string name="app_timeout">Časový limit aplikace</string> <string name="app_timeout">Časový limit</string>
<string name="app_timeout_summary">Doba nečinnosti, po které se aplikace zamkne</string> <string name="app_timeout_summary">Doba nečinnosti, po které se aplikace zamkne</string>
<string name="application">Aplikace</string> <string name="application">Aplikace</string>
<string name="menu_app_settings">Nastavení aplikace</string> <string name="menu_app_settings">Nastavení aplikace</string>
@@ -35,16 +35,16 @@
<string name="clipboard_error">Některá zařízení nedovolují aplikacím používat schránku.</string> <string name="clipboard_error">Některá zařízení nedovolují aplikacím používat schránku.</string>
<string name="clipboard_error_clear">Nelze vyprázdnit schránku</string> <string name="clipboard_error_clear">Nelze vyprázdnit schránku</string>
<string name="clipboard_timeout">Časový limit schránky</string> <string name="clipboard_timeout">Časový limit schránky</string>
<string name="clipboard_timeout_summary">Doba uchování ve schránce</string> <string name="clipboard_timeout_summary">Doba uchování ve schránce (je-li podporována zařízením)</string>
<string name="select_to_copy">Vyberte zkopírovat %1$s do schránky</string> <string name="select_to_copy">Vyberte zkopírovat %1$s do schránky</string>
<string name="retrieving_db_key">Načítá klíče databáze…</string> <string name="retrieving_db_key">Načítám klíč databáze…</string>
<string name="database">Databáze</string> <string name="database">Databáze</string>
<string name="decrypting_db">Dešifrování obsahu databáze…</string> <string name="decrypting_db">Dešifruji obsah databáze…</string>
<string name="default_checkbox">Použít jako výchozí databázi</string> <string name="default_checkbox">Použít jako výchozí databázi</string>
<string name="digits">Číslice</string> <string name="digits">Číslice</string>
<string name="select_database_file">Otevřít existující databázi</string> <string name="select_database_file">Otevřít existující databázi</string>
<string name="entry_accessed">Poslední přístup</string> <string name="entry_accessed">Poslední přístup</string>
<string name="entry_cancel">Storno</string> <string name="entry_cancel">Zrušit</string>
<string name="entry_notes">Poznámky</string> <string name="entry_notes">Poznámky</string>
<string name="entry_confpassword">Potvrďte heslo</string> <string name="entry_confpassword">Potvrďte heslo</string>
<string name="entry_created">Vytvořeno</string> <string name="entry_created">Vytvořeno</string>
@@ -55,27 +55,27 @@
<string name="entry_password">Heslo</string> <string name="entry_password">Heslo</string>
<string name="save">Uložit</string> <string name="save">Uložit</string>
<string name="entry_title">Název</string> <string name="entry_title">Název</string>
<string name="entry_url">URL adresa</string> <string name="entry_url">URL</string>
<string name="entry_user_name">Uživatelské jméno</string> <string name="entry_user_name">Uživatelské jméno</string>
<string name="error_arc4">Arcfour proudová šifra není podporována.</string> <string name="error_arc4">Arcfour proudová šifra není podporována.</string>
<string name="error_can_not_handle_uri">KeePassDX nemůže zpracovat toto URI.</string> <string name="error_can_not_handle_uri">KeePassDX nemůže zpracovat toto URI.</string>
<string name="error_file_not_create">Soubor se nedaří vytvořit</string> <string name="error_file_not_create">Soubor se nepodařilo vytvořit</string>
<string name="error_invalid_db">Databázi nelze číst.</string> <string name="error_invalid_db">Databázi se nepodařilo načíst.</string>
<string name="error_invalid_path">Ujistěte se, že cesta je správná.</string> <string name="error_invalid_path">Ujistěte se, že cesta je správná.</string>
<string name="error_no_name">Zadejte jméno.</string> <string name="error_no_name">Zadejte jméno.</string>
<string name="error_nokeyfile">Vyberte soubor s klíčem.</string> <string name="error_nokeyfile">Vyberte soubor s klíčem.</string>
<string name="error_out_of_memory">Nedostatek paměti k načtení celé databáze.</string> <string name="error_out_of_memory">Nedostatek paměti k načtení celé databáze.</string>
<string name="error_pass_gen_type">Je třeba zvolit alespoň jeden způsob vytváření hesla.</string> <string name="error_pass_gen_type">Je třeba zvolit alespoň jeden způsob vytváření hesla.</string>
<string name="error_pass_match">Zadaná hesla se neshodují.</string> <string name="error_pass_match">Hesla se neshodují.</string>
<string name="error_rounds_too_large">Příliš vysoký „Počet průchodů“. Nastavuji na 2147483648.</string> <string name="error_rounds_too_large">Příliš vysoký „Počet průchodů“. Nastavuji na 2147483648.</string>
<string name="error_string_key">Je třeba, aby každý řetězec měl název kolonky.</string> <string name="error_string_key">Je třeba, aby každý řetězec měl název kolonky.</string>
<string name="error_wrong_length">Do pole „Délka“ zadejte celé kladné číslo.</string> <string name="error_wrong_length">Do pole „Délka“ zadejte celé kladné číslo.</string>
<string name="field_name">Název pole</string> <string name="field_name">Název kolonky</string>
<string name="field_value">Hodnota pole</string> <string name="field_value">Hodnota kolonky</string>
<string name="file_browser">Správce souborů</string> <string name="file_browser">Správce souborů</string>
<string name="generate_password">Vytvoř heslo</string> <string name="generate_password">Generovat heslo</string>
<string name="hint_conf_pass">Potvrdit heslo</string> <string name="hint_conf_pass">Potvrdit heslo</string>
<string name="hint_generated_password">Vytvořené heslo</string> <string name="hint_generated_password">Generované heslo</string>
<string name="hint_group_name">Název skupiny</string> <string name="hint_group_name">Název skupiny</string>
<string name="hint_keyfile">Soubor s klíčem</string> <string name="hint_keyfile">Soubor s klíčem</string>
<string name="hint_length">Délka</string> <string name="hint_length">Délka</string>
@@ -83,8 +83,8 @@
<string name="password">Heslo</string> <string name="password">Heslo</string>
<string name="invalid_credentials">Nebylo možno načíst autentizační údaje.</string> <string name="invalid_credentials">Nebylo možno načíst autentizační údaje.</string>
<string name="invalid_algorithm">Nesprávný algoritmus.</string> <string name="invalid_algorithm">Nesprávný algoritmus.</string>
<string name="invalid_db_sig">Nedaří se rozpoznat formát databáze.</string> <string name="invalid_db_sig">Nepodařilo se rozpoznat formát databáze.</string>
<string name="keyfile_is_empty">Soubor s klíčem je prázdný.</string> <string name="keyfile_is_empty">Soubor klíče je prázdný.</string>
<string name="length">Délka</string> <string name="length">Délka</string>
<string name="list_size_title">Velikost položek seznamu</string> <string name="list_size_title">Velikost položek seznamu</string>
<string name="list_size_summary">Velikost textu v seznamu prvků</string> <string name="list_size_summary">Velikost textu v seznamu prvků</string>
@@ -97,28 +97,28 @@
<string name="settings">Nastavení</string> <string name="settings">Nastavení</string>
<string name="menu_database_settings">Nastavení databáze</string> <string name="menu_database_settings">Nastavení databáze</string>
<string name="menu_delete">Smazat</string> <string name="menu_delete">Smazat</string>
<string name="menu_donate">Podpořit vývoj darem</string> <string name="menu_donate">Přispět darem</string>
<string name="menu_edit">Upravit</string> <string name="menu_edit">Upravit</string>
<string name="menu_hide_password">Skrýt heslo</string> <string name="menu_hide_password">Skrýt heslo</string>
<string name="menu_lock">Zamknout databázi</string> <string name="menu_lock">Zamknout databázi</string>
<string name="menu_open">Otevřít</string> <string name="menu_open">Otevřít</string>
<string name="menu_search">Hledat</string> <string name="menu_search">Hledat</string>
<string name="menu_showpass">Uk heslo</string> <string name="menu_showpass">Ukázat heslo</string>
<string name="menu_url">Jít na URL</string> <string name="menu_url">Přejít na URL</string>
<string name="minus">Mínus</string> <string name="minus">Mínus</string>
<string name="never">Nikdy</string> <string name="never">Nikdy</string>
<string name="no_results">Žádné výsledky hledání</string> <string name="no_results">Žádné výsledky hledání</string>
<string name="no_url_handler">Pro otevření tohoto URL nainstalujte webový prohlížeč.</string> <string name="no_url_handler">Pro otevření tohoto URL nainstalujte webový prohlížeč.</string>
<string name="omit_backup_search_title">Neprohledávat položky v záloze</string> <string name="omit_backup_search_title">Neprohledávat položky v záloze</string>
<string name="omit_backup_search_summary">Vynechat skupiny „Záloha“ a \"Koš\" z výsledků vyhledávání</string> <string name="omit_backup_search_summary">Vynechat skupiny „Záloha“ a \"Koš\" z výsledků vyhledávání</string>
<string name="progress_create">Vytvářím novou databázi…</string> <string name="progress_create">Zakládám novou databázi…</string>
<string name="progress_title">Zpracování</string> <string name="progress_title">Pracuji</string>
<string name="protection">Ochrana</string> <string name="protection">Ochrana</string>
<string name="read_only_warning">Ke změně v databázi potřebuje KeePassDX oprávnění pro zápis.</string> <string name="read_only_warning">Ke změně v databáze potřebuje KeePassDX oprávnění pro zápis.</string>
<string name="content_description_remove_from_list">Odstranit</string> <string name="content_description_remove_from_list">Odstranit</string>
<string name="encryption_rijndael">Rijndael (AES)</string> <string name="encryption_rijndael">Rijndael (AES)</string>
<string name="root">Kořen</string> <string name="root">Kořen</string>
<string name="rounds">Počet šifrovacích průchodů</string> <string name="rounds">Transformační průchody</string>
<string name="rounds_explanation">Vyšší počet šifrovacích průchodů zvýší odolnost proti útoku zkoušením všech možných hesel, ale může výrazně zpomalit načítání a ukládání.</string> <string name="rounds_explanation">Vyšší počet šifrovacích průchodů zvýší odolnost proti útoku zkoušením všech možných hesel, ale může výrazně zpomalit načítání a ukládání.</string>
<string name="saving_database">Ukládám databázi…</string> <string name="saving_database">Ukládám databázi…</string>
<string name="space">Místo</string> <string name="space">Místo</string>
@@ -134,7 +134,7 @@
<string name="version_label">Verze %1$s</string> <string name="version_label">Verze %1$s</string>
<string name="education_unlock_summary">Databázi odemknete zadáním hesla a/nebo souboru s klíčem. <string name="education_unlock_summary">Databázi odemknete zadáním hesla a/nebo souboru s klíčem.
\n \n
\nNezapomeňte si po každé úpravě zazálohovat kopii svého .kdbx souboru na bezpečné místo.</string> \nNezapomeňte po každé úpravě zálohovat kopii svého .kdbx souboru na bezpečné místo.</string>
<string-array name="timeout_options"> <string-array name="timeout_options">
<item>5 sekund</item> <item>5 sekund</item>
<item>10 sekund</item> <item>10 sekund</item>
@@ -155,41 +155,41 @@
<string name="encryption">Šifrování</string> <string name="encryption">Šifrování</string>
<string name="key_derivation_function">Funkce pro tvorbu klíče</string> <string name="key_derivation_function">Funkce pro tvorbu klíče</string>
<string name="extended_ASCII">Rozšířené ASCII</string> <string name="extended_ASCII">Rozšířené ASCII</string>
<string name="allow">Umožnit</string> <string name="allow">Povolit</string>
<string name="error_load_database">Databázi se nedaří načíst.</string> <string name="error_load_database">Databázi se nepodařilo načíst.</string>
<string name="error_load_database_KDF_memory">Klíč se nedaří načíst, zkuste snížit množství paměti, využívané funkcí pro tvorbu klíče.</string> <string name="error_load_database_KDF_memory">Klíč se nepodařilo načíst, zkuste snížit \"využití paměti\" pro KDF.</string>
<string name="error_autofill_enable_service">Službu automatického vyplňování se nedaří zapnout.</string> <string name="error_autofill_enable_service">Službu automatického vyplňování se nepodařilo zapnout.</string>
<string name="error_move_folder_in_itself">Není možné přesunout skupinu do ní samotné.</string> <string name="error_move_folder_in_itself">Není možné přesunout skupinu do ní samotné.</string>
<string name="file_not_found_content">Soubor nenalezen. Zkuste jej otevřít ze správce souborů.</string> <string name="file_not_found_content">Soubor nenalezen. Zkuste jej otevřít ze správce souborů.</string>
<string name="list_entries_show_username_title">Zobrazit uživatelská jména</string> <string name="list_entries_show_username_title">Zobrazit uživatelská jména</string>
<string name="list_entries_show_username_summary">V seznamech položek zobrazit uživatelská jména</string> <string name="list_entries_show_username_summary">V seznamech záznamů zobrazit uživatelská jména</string>
<string name="copy_field">Kopie %1$s</string> <string name="copy_field">Kopie %1$s</string>
<string name="menu_form_filling_settings">Vyplňování formulářů</string> <string name="menu_form_filling_settings">Vyplňování formulářů</string>
<string name="menu_copy">Zkopírovat</string> <string name="menu_copy">Zkopírovat</string>
<string name="menu_move">Přesunout</string> <string name="menu_move">Přesunout</string>
<string name="menu_paste">Vložit</string> <string name="menu_paste">Vložit</string>
<string name="menu_cancel">Storno</string> <string name="menu_cancel">Zrušit</string>
<string name="menu_file_selection_read_only">Chráněno před zápisem</string> <string name="menu_file_selection_read_only">Chráněno před zápisem</string>
<string name="menu_open_file_read_and_write">Čtení a zápis</string> <string name="menu_open_file_read_and_write">Čtení a zápis</string>
<string name="read_only">Chráněno před zápisem</string> <string name="read_only">Chráněno před zápisem</string>
<string name="encryption_explanation">Algoritmus šifrování databáze užitý pro všechna data.</string> <string name="encryption_explanation">Algoritmus šifrování databáze použit pro všechna data.</string>
<string name="kdf_explanation">Klíč pro šifrovací algoritmus je vytvořen transformací hlavního klíče skrze odvozovací funkci klíče s náhodně přidanou složkou, tzv. solí.</string> <string name="kdf_explanation">Klíč pro šifrovací algoritmus je vytvořen transformací hlavního klíče pomocí funkce odvození klíče s náhodně přidanou složkou, tzv. solí.</string>
<string name="memory_usage">Využití paměti</string> <string name="memory_usage">Využití paměti</string>
<string name="memory_usage_explanation">Množství paměti (v bajtech) použitých funkcí pro odvození klíče.</string> <string name="memory_usage_explanation">Množství paměti použitých funkcí pro odvození klíče.</string>
<string name="parallelism">Souběžné zpracovávání</string> <string name="parallelism">Souběžné zpracovávání</string>
<string name="parallelism_explanation">Stupeň souběžného zpracovávání (počet vláken) použitý funkcí pro vytvoření klíče.</string> <string name="parallelism_explanation">Stupeň souběžného zpracovávání (počet vláken) použitý funkcí pro odvození klíče.</string>
<string name="sort_menu">Seřadit</string> <string name="sort_menu">Seřadit</string>
<string name="sort_ascending">Nejnižší první ↓</string> <string name="sort_ascending">Nejnižší první ↓</string>
<string name="sort_groups_before">Skupiny první</string> <string name="sort_groups_before">Skupiny první</string>
<string name="sort_recycle_bin_bottom">Koš jako poslední</string> <string name="sort_recycle_bin_bottom">Koš jako poslední</string>
<string name="sort_title">Nadpis</string> <string name="sort_title">Nadpis</string>
<string name="sort_username">Uživatelské jméno</string> <string name="sort_username">Uživatelské jméno</string>
<string name="sort_creation_time">Vytvoření</string> <string name="sort_creation_time">Založeno</string>
<string name="sort_last_modify_time">Změna</string> <string name="sort_last_modify_time">Změněno</string>
<string name="sort_last_access_time">Přístup</string> <string name="sort_last_access_time">Přístup</string>
<string name="warning">Varování</string> <string name="warning">Varování</string>
<string name="warning_password_encoding">Nepoužívejte v hesle pro databázový soubor znaky mimo znakovou sadu Latin-1 (nepoužívejte znaky s diakritikou).</string> <string name="warning_password_encoding">Nepoužívejte v hesle pro databázový soubor znaky mimo znaky kódování textu (nerozpoznané znaky budou konvertovány na stejné písmeno).</string>
<string name="warning_empty_password">Pokračovat bez ochrany odemknutím heslem\?</string> <string name="warning_empty_password">Pokračovat bez ochrany odemknutí heslem\?</string>
<string name="warning_no_encryption_key">Pokračovat bez šifrovacího klíče\?</string> <string name="warning_no_encryption_key">Pokračovat bez šifrovacího klíče\?</string>
<string name="encrypted_value_stored">Šifrované heslo uloženo</string> <string name="encrypted_value_stored">Šifrované heslo uloženo</string>
<string name="no_credentials_stored">Tato databáze zatím nemá uložené heslo.</string> <string name="no_credentials_stored">Tato databáze zatím nemá uložené heslo.</string>
@@ -198,7 +198,7 @@
<string name="general">Obecné</string> <string name="general">Obecné</string>
<string name="autofill">Automatické vyplnění</string> <string name="autofill">Automatické vyplnění</string>
<string name="autofill_service_name">KeePassDX automatické vyplňování formulářů</string> <string name="autofill_service_name">KeePassDX automatické vyplňování formulářů</string>
<string name="autofill_sign_in_prompt">Přihlásit se pomocí KeePassDX</string> <string name="autofill_sign_in_prompt">Přihlásit se s KeePassDX</string>
<string name="set_autofill_service_title">Nastavit výchozí službu automatického vyplňování</string> <string name="set_autofill_service_title">Nastavit výchozí službu automatického vyplňování</string>
<string name="autofill_explanation_summary">Povolit rychlé automatické vyplňování formulářů v ostatních aplikacích</string> <string name="autofill_explanation_summary">Povolit rychlé automatické vyplňování formulářů v ostatních aplikacích</string>
<string name="password_size_title">Délka generovaného hesla</string> <string name="password_size_title">Délka generovaného hesla</string>
@@ -206,28 +206,28 @@
<string name="list_password_generator_options_title">Znaky hesla</string> <string name="list_password_generator_options_title">Znaky hesla</string>
<string name="list_password_generator_options_summary">Nastavit povolené znaky pro generátor hesel</string> <string name="list_password_generator_options_summary">Nastavit povolené znaky pro generátor hesel</string>
<string name="clipboard">Schránka</string> <string name="clipboard">Schránka</string>
<string name="clipboard_notifications_title">Oznamování schránky</string> <string name="clipboard_notifications_title">Oznámení schránky</string>
<string name="clipboard_notifications_summary">Ukázat oznamení schránky o kopírování pole při prohlížení záznamu</string> <string name="clipboard_notifications_summary">Ukázat oznámení schránky o kopírování pole při prohlížení záznamu</string>
<string name="clipboard_warning">Vymazat historii schránky manuálně, pokud automatické vymazání schránky selže.</string> <string name="clipboard_warning">Vymazat historii schránky manuálně, pokud automatické vymazání schránky selže.</string>
<string name="lock">Zamknout</string> <string name="lock">Zamknout</string>
<string name="lock_database_screen_off_title">Zámek obrazovky</string> <string name="lock_database_screen_off_title">Zámek obrazovky</string>
<string name="lock_database_screen_off_summary">Při zhasnutí obrazovky uzamknout databázi</string> <string name="lock_database_screen_off_summary">Při zhasnutí obrazovky uzamknout databázi</string>
<string name="advanced_unlock">Pokročilé odemčení</string> <string name="advanced_unlock">Rozšířené odemknutí</string>
<string name="biometric_unlock_enable_title">Biometrické odemčení</string> <string name="biometric_unlock_enable_title">Biometrické odemknutí</string>
<string name="biometric_unlock_enable_summary">Nechá otevřít databázi snímáním biometrického údaje</string> <string name="biometric_unlock_enable_summary">Nechá otevřít databázi snímáním biometrického údaje</string>
<string name="biometric_delete_all_key_title">Smazat šifrovací klíče</string> <string name="biometric_delete_all_key_title">Smazat šifrovací klíče</string>
<string name="biometric_delete_all_key_summary">Smazat všechny šifrovací klíče související s rozpoznáním pokročilého odemknutí</string> <string name="biometric_delete_all_key_summary">Smazat všechny šifrovací klíče související s rozpoznáním rozšířeného odemknutí</string>
<string name="unavailable_feature_text">Tuto funkci se nedaří spustit.</string> <string name="unavailable_feature_text">Tuto funkci se nedaří spustit.</string>
<string name="unavailable_feature_version">V zařízení je instalován Android %1$s, ale potřebná je verze %2$s a novější.</string> <string name="unavailable_feature_version">V zařízení je instalován Android %1$s, ale potřebná je verze %2$s a novější.</string>
<string name="unavailable_feature_hardware">Hardware nebyl rozpoznán.</string> <string name="unavailable_feature_hardware">Odpovídající hardware nebyl rozpoznán.</string>
<string name="file_name">Název souboru</string> <string name="file_name">Název souboru</string>
<string name="path">Cesta</string> <string name="path">Cesta</string>
<string name="assign_master_key">Přiřadit hlavní klíč</string> <string name="assign_master_key">Přiřadit hlavní klíč</string>
<string name="create_keepass_file">Vytvořit novou databázi</string> <string name="create_keepass_file">Založit novou databázi</string>
<string name="recycle_bin_title">Využití koše</string> <string name="recycle_bin_title">Využití koše</string>
<string name="recycle_bin_summary">Před smazáním přesune skupiny a položky do skupiny „Koš“</string> <string name="recycle_bin_summary">Před smazáním přesune skupiny a položky do skupiny „Koš“</string>
<string name="monospace_font_fields_enable_title">Písmo položek</string> <string name="monospace_font_fields_enable_title">Písmo kolonek</string>
<string name="monospace_font_fields_enable_summary">Čitelnost znaků v položkách můžete přizpůsobit změnou písma</string> <string name="monospace_font_fields_enable_summary">Čitelnost znaků v kolonkách můžete přizpůsobit změnou písma</string>
<string name="allow_copy_password_title">Důvěřovat schránce</string> <string name="allow_copy_password_title">Důvěřovat schránce</string>
<string name="allow_copy_password_summary">Povolit kopírování hesla záznamu a chráněných položek do schránky</string> <string name="allow_copy_password_summary">Povolit kopírování hesla záznamu a chráněných položek do schránky</string>
<string name="allow_copy_password_warning">Varování: Schránka je sdílena všemi aplikacemi. Pokud jsou do ní zkopírovány citlivé údaje, mohl by se k nim dostat další software.</string> <string name="allow_copy_password_warning">Varování: Schránka je sdílena všemi aplikacemi. Pokud jsou do ní zkopírovány citlivé údaje, mohl by se k nim dostat další software.</string>
@@ -253,47 +253,47 @@
<string name="education_create_database_summary">Založte svůj první soubor pro správu hesel.</string> <string name="education_create_database_summary">Založte svůj první soubor pro správu hesel.</string>
<string name="education_select_database_title">Otevřít existující databázi</string> <string name="education_select_database_title">Otevřít existující databázi</string>
<string name="education_select_database_summary">Otevřete svou dříve používanou databázi ze správce souborů a pokračujte v jejím používání.</string> <string name="education_select_database_summary">Otevřete svou dříve používanou databázi ze správce souborů a pokračujte v jejím používání.</string>
<string name="education_new_node_title">Přidejte položky do databáze</string> <string name="education_new_node_title">Přidejte záznamy do databáze</string>
<string name="education_new_node_summary">Položky pomáhají se správou vašich digitálních identit. <string name="education_new_node_summary">Záznamy pomáhají se správou Vašich digitálních identit.
\n \n
\nSkupiny (ekvivalent složek) organizují záznamy v databázi.</string> \nSkupiny (ekvivalent složek) organizují záznamy v databázi.</string>
<string name="education_search_title">Hledejte v položkách</string> <string name="education_search_title">Hledat v záznamech</string>
<string name="education_search_summary">Zadejte název, uživatelské jméno nebo jiné položky k nalezení svých hesel.</string> <string name="education_search_summary">Zadejte název, uživatelské jméno nebo jiné kolonky k nalezení svých hesel.</string>
<string name="education_entry_edit_title">Upravit položku</string> <string name="education_entry_edit_title">Upravit záznam</string>
<string name="education_entry_edit_summary">Přidejte ke své položce vlastní kolonky. Společná data mohou být sdílena mezi více různými kolonkami.</string> <string name="education_entry_edit_summary">Přidejte ke svému záznamu vlastní kolonky. Společná data mohou být sdílena mezi různými kolonkami záznamu odkazem.</string>
<string name="education_generate_password_title">Vytvořit silné heslo</string> <string name="education_generate_password_title">Vytvořit silné heslo</string>
<string name="education_generate_password_summary">Vygenerujte silné heslo pro svou položku, definujte je podle kritérií formuláře, a nezapomeňte na bezpečné heslo.</string> <string name="education_generate_password_summary">Generujte silné heslo pro svůj záznam, definujte je podle kritérií formuláře, a nezapomeňte na bezpečné heslo.</string>
<string name="education_entry_new_field_title">Přidat vlastní kolonky</string> <string name="education_entry_new_field_title">Přidat vlastní kolonky</string>
<string name="education_entry_new_field_summary">Registrovat další kolonku, zadat hodnotu a volitelně ji ochránit.</string> <string name="education_entry_new_field_summary">Registrovat další kolonku, zadat hodnotu a volitelně ji ochránit.</string>
<string name="education_unlock_title">Odemknout databázi</string> <string name="education_unlock_title">Odemknout databázi</string>
<string name="education_read_only_title">Ochraňte svou databázi před zápisem</string> <string name="education_read_only_title">Ochraňte svou databázi před zápisem</string>
<string name="education_read_only_summary">Změnit režim otevírání pro dané sezení. <string name="education_read_only_summary">Změnit režim otevírání pro dané sezení.
\n \n
\nV režimu \"pouze pro čtení\" zabráníte nechtěným změnám do databáze. \nV režimu \"pouze pro čtení\" zabráníte nechtěným změnám v databázi.
\nV režimu \"zápisu\" je možné přidávat, mazat nebo měnit všechny prvky podle libosti.</string> \nV režimu \"zápisu\" je možné přidávat, mazat nebo měnit všechny prvky podle libosti.</string>
<string name="education_field_copy_title">Zkopírujte kolonku</string> <string name="education_field_copy_title">Zkopírovat kolonku</string>
<string name="education_field_copy_summary">Zkopírované kolonky lze vkládat kam chcete <string name="education_field_copy_summary">Zkopírované kolonky lze vkládat podle libosti.
\n \n
\nK vyplňování formulářů použijte svou oblíbenou metodu.</string> \nK vyplňování formulářů použijte svou oblíbenou metodu.</string>
<string name="education_lock_title">Uzamkni databáze</string> <string name="education_lock_title">Uzamknout databázi</string>
<string name="education_lock_summary">Rychlé uzamkni databázi. Je možné nastavit, aby se databáze zamkla po určitém čase a také po zhasnutí obrazovky.</string> <string name="education_lock_summary">Rychle uzamknout databázi. Je možné nastavit, aby se databáze zamkla po určitém čase a také po zhasnutí obrazovky.</string>
<string name="education_sort_title">Řazení položek</string> <string name="education_sort_title">Řazení položek</string>
<string name="education_sort_summary">Vyberte řazení položek a skupin.</string> <string name="education_sort_summary">Vyberte řazení položek a skupin.</string>
<string name="education_donation_title">Zapojit se</string> <string name="education_donation_title">Zapojit se</string>
<string name="education_donation_summary">Zapojte se a pomozte zvýšit stabilitu, bezpečnost a přidávání dalších funkcí.</string> <string name="education_donation_summary">Zapojte se a pomozte zvýšit stabilitu, bezpečnost a doplnění dalších funkcí.</string>
<string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel, tato je &lt;strong&gt;bez reklam&lt;/strong&gt;", je "&lt;strong&gt;svobodný software pod copyleft licencí&lt;/strong&gt; a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string> <string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel je tato &lt;strong&gt;bez reklam&lt;/strong&gt;, je \u0020&lt;strong&gt;svobodný software pod copyleft licencí&lt;/strong&gt; a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string>
<string name="html_text_buy_pro">Zakoupením varianty pro získáte přístup k tomuto &lt;strong&gt;vizuálnímu stylu&lt;/strong&gt; a hlavně pomůžete &lt;strong&gt;uskutečnění komunitních projektů.&lt;/strong&gt;</string> <string name="html_text_buy_pro">Zakoupením varianty \"pro\" získáte přístup k tomuto &lt;strong&gt;vizuálnímu stylu&lt;/strong&gt; a hlavně pomůžete &lt;strong&gt;uskutečnění komunitních projektů.&lt;/strong&gt;</string>
<string name="html_text_feature_generosity">Tento &lt;strong&gt;vizuální styl&lt;/strong&gt; je k dispozici díky vaší štědrosti.</string> <string name="html_text_feature_generosity">Tento &lt;strong&gt;vizuální styl&lt;/strong&gt; je k dispozici díky vaší štědrosti.</string>
<string name="html_text_donation">Pro zajištění svobody nás všech a pokračování aktivity, počítáme s vaším &lt;strong&gt;přispěním.&lt;/strong&gt;</string> <string name="html_text_donation">Pro zajištění svobody nás všech a pokračování aktivity počítáme s Vaším &lt;strong&gt;přispěním.&lt;/strong&gt;</string>
<string name="html_text_dev_feature">Tato funkce je &lt;strong&gt;ve vývoji&lt;/strong&gt; a potřebuje váš &lt;strong&gt;příspěvek&lt;/strong&gt;, aby byla brzy k dispozici.</string> <string name="html_text_dev_feature">Tato funkce je &lt;strong&gt;ve vývoji&lt;/strong&gt; a potřebuje Váš &lt;strong&gt;příspěvek&lt;/strong&gt;, aby byla brzy k dispozici.</string>
<string name="html_text_dev_feature_buy_pro">Zakoupením &lt;strong&gt;pro&lt;/strong&gt; varianty,</string> <string name="html_text_dev_feature_buy_pro">Zakoupením &lt;strong&gt;pro&lt;/strong&gt; varianty,</string>
<string name="html_text_dev_feature_contibute">&lt;strong&gt;Zapojením se&lt;/strong&gt;,</string> <string name="html_text_dev_feature_contibute">&lt;strong&gt;Zapojením se&lt;/strong&gt;,</string>
<string name="html_text_dev_feature_encourage">povzbudíte vývojáře k přidává&lt;strong&gt;nových funkcí&lt;/strong&gt; a &lt;strong&gt;opravování chyb&lt;/strong&gt; dle vašich připomínek.</string> <string name="html_text_dev_feature_encourage">povzbudíte vývojáře k doplně&lt;strong&gt;nových funkcí&lt;/strong&gt; a &lt;strong&gt;opravám chyb&lt;/strong&gt; dle vašich připomínek.</string>
<string name="html_text_dev_feature_thanks">Mnohé díky za vašeispě.</string> <string name="html_text_dev_feature_thanks">Mockrát děkujeme za Vášíspěvek.</string>
<string name="html_text_dev_feature_work_hard">Tvrdě pracujeme na brzkém vydání této funkce.</string> <string name="html_text_dev_feature_work_hard">Tvrdě pracujeme na brzkém vydání této funkce.</string>
<string name="html_text_dev_feature_upgrade">Pamatujte na aktualizaci aplikace instalováním nových verzí.</string> <string name="html_text_dev_feature_upgrade">Pamatujte na aktualizaci aplikace instalováním nových verzí.</string>
<string name="download">Stáhnout</string> <string name="download">Stáhnout</string>
<string name="contribute">Zapojit se</string> <string name="contribute">Přispět</string>
<string name="encryption_chacha20">ChaCha20</string> <string name="encryption_chacha20">ChaCha20</string>
<string name="kdf_AES">AES</string> <string name="kdf_AES">AES</string>
<string name="style_choose_title">Vzhled aplikace</string> <string name="style_choose_title">Vzhled aplikace</string>
@@ -304,16 +304,16 @@
<string name="keyboard_name">Magikeyboard</string> <string name="keyboard_name">Magikeyboard</string>
<string name="keyboard_label">Magikeyboard (KeePassDX)</string> <string name="keyboard_label">Magikeyboard (KeePassDX)</string>
<string name="keyboard_setting_label">Magikeyboard nastavení</string> <string name="keyboard_setting_label">Magikeyboard nastavení</string>
<string name="keyboard_entry_category">Položka</string> <string name="keyboard_entry_category">Záznam</string>
<string name="keyboard_entry_timeout_title">Časový limit</string> <string name="keyboard_entry_timeout_title">Časový limit</string>
<string name="keyboard_entry_timeout_summary">Doba uchování položky v Magikeyboardu</string> <string name="keyboard_entry_timeout_summary">Doba uchování položky v Magikeyboardu</string>
<string name="keyboard_notification_entry_title">Informace o oznámení</string> <string name="keyboard_notification_entry_title">Informace o oznámení</string>
<string name="keyboard_notification_entry_summary">Zobrazit oznámení, když je položka dostupná</string> <string name="keyboard_notification_entry_summary">Zobrazit oznámení, když je položka dostupná</string>
<string name="keyboard_notification_entry_content_title_text">Položka</string> <string name="keyboard_notification_entry_content_title_text">Záznam</string>
<string name="keyboard_notification_entry_content_title">%1$s dostupné v Magikeyboardu</string> <string name="keyboard_notification_entry_content_title">%1$s dostupné v Magikeyboardu</string>
<string name="keyboard_notification_entry_content_text">%1$s</string> <string name="keyboard_notification_entry_content_text">%1$s</string>
<string name="keyboard_notification_entry_clear_close_title">Vymazat při zavření</string> <string name="keyboard_notification_entry_clear_close_title">Vymazat při zavření</string>
<string name="keyboard_notification_entry_clear_close_summary">Zavři databázi při zavření oznámení</string> <string name="keyboard_notification_entry_clear_close_summary">Zavřít databázi při zavření oznámení</string>
<string name="keyboard_appearance_category">Vzhled</string> <string name="keyboard_appearance_category">Vzhled</string>
<string name="keyboard_theme_title">Vzhled klávesnice</string> <string name="keyboard_theme_title">Vzhled klávesnice</string>
<string name="keyboard_keys_category">Klávesy</string> <string name="keyboard_keys_category">Klávesy</string>
@@ -326,37 +326,37 @@
<string name="clear_clipboard_notification_title">Vymazat při ukončení</string> <string name="clear_clipboard_notification_title">Vymazat při ukončení</string>
<string name="clear_clipboard_notification_summary">Uzamknout databázi, jakmile trvání schránky vyprší nebo po uzavření oznámení</string> <string name="clear_clipboard_notification_summary">Uzamknout databázi, jakmile trvání schránky vyprší nebo po uzavření oznámení</string>
<string name="recycle_bin">Koš</string> <string name="recycle_bin">Koš</string>
<string name="keyboard_selection_entry_title">Výběr položky</string> <string name="keyboard_selection_entry_title">Výběr záznamu</string>
<string name="keyboard_selection_entry_summary">Při prohlížení záznamu ukázat na Magikeyboard pole položek</string> <string name="keyboard_selection_entry_summary">Při prohlížení záznamu ukázat na Magikeyboard kolonky</string>
<string name="delete_entered_password_title">Smazat heslo</string> <string name="delete_entered_password_title">Smazat heslo</string>
<string name="delete_entered_password_summary">Smaže heslo zadané po pokusu o připojení k databázi</string> <string name="delete_entered_password_summary">Smaže heslo zadané po pokusu o připojení k databázi</string>
<string name="content_description_open_file">Otevřít soubor</string> <string name="content_description_open_file">Otevřít soubor</string>
<string name="content_description_node_children">Potomci uzlu</string> <string name="content_description_node_children">Podřazené prvky uzlu</string>
<string name="content_description_add_node">Přidej uzel</string> <string name="content_description_add_node">Přidat uzel</string>
<string name="content_description_add_entry">Přidej záznam</string> <string name="content_description_add_entry">Přidat záznam</string>
<string name="content_description_add_group">Přidat skupinu</string> <string name="content_description_add_group">Přidat skupinu</string>
<string name="content_description_file_information">Informace o souboru</string> <string name="content_description_file_information">Informace o souboru</string>
<string name="content_description_password_checkbox">Checkbox hesla</string> <string name="content_description_password_checkbox">Checkbox hesla</string>
<string name="content_description_keyfile_checkbox">Checkbox souboru s klíčem</string> <string name="content_description_keyfile_checkbox">Checkbox souboru s klíčem</string>
<string name="content_description_repeat_toggle_password_visibility">Přepni ukázání hesla</string> <string name="content_description_repeat_toggle_password_visibility">Opakovat přepnutí viditelnosti hesla</string>
<string name="content_description_entry_icon">Ikona záznamu</string> <string name="content_description_entry_icon">Ikona záznamu</string>
<string name="entry_password_generator">Generátor hesel</string> <string name="entry_password_generator">Generátor hesel</string>
<string name="content_description_password_length">Délka hesla</string> <string name="content_description_password_length">Délka hesla</string>
<string name="entry_add_field">Přidej pole</string> <string name="entry_add_field">Přidat pole</string>
<string name="content_description_remove_field">Odeber pole</string> <string name="content_description_remove_field">Odebrat pole</string>
<string name="entry_UUID">UUID</string> <string name="entry_UUID">UUID</string>
<string name="error_move_entry_here">Sem záznam přesunout nelze.</string> <string name="error_move_entry_here">Sem záznam přesunout nelze.</string>
<string name="error_copy_entry_here">Sem záznam zkopírovat nelze.</string> <string name="error_copy_entry_here">Sem záznam zkopírovat nelze.</string>
<string name="list_groups_show_number_entries_title">Uk počet záznamů</string> <string name="list_groups_show_number_entries_title">Ukázat počet záznamů</string>
<string name="list_groups_show_number_entries_summary">Uk počet záznamů ve skupině</string> <string name="list_groups_show_number_entries_summary">Ukázat počet záznamů ve skupině</string>
<string name="content_description_background">Pozadí</string> <string name="content_description_background">Pozadí</string>
<string name="content_description_update_from_list">Aktualizovat</string> <string name="content_description_update_from_list">Aktualizovat</string>
<string name="content_description_keyboard_close_fields">Zavři kolonky</string> <string name="content_description_keyboard_close_fields">Zavřít pole</string>
<string name="error_create_database_file">Nelze vytvořit databázi s tímto heslem a klíčem ze souboru.</string> <string name="error_create_database_file">Nelze vytvořit databázi s tímto heslem a souborem klíče.</string>
<string name="menu_advanced_unlock_settings">Pokročilé odemčení</string> <string name="menu_advanced_unlock_settings">Rozšířené odemknutí</string>
<string name="biometric">Biometrika</string> <string name="biometric">Biometrika</string>
<string name="biometric_auto_open_prompt_title">Automaticky otevřít pobídku</string> <string name="biometric_auto_open_prompt_title">Automaticky otevřít pobídku</string>
<string name="biometric_auto_open_prompt_summary">Automaticky žádat pokročilé odemknutí, je-li databáze nastavena k jejímu použití</string> <string name="biometric_auto_open_prompt_summary">Automaticky žádat rozšířené odemknutí, je-li databáze nastavena k jejímu použití</string>
<string name="enable">Zapnout</string> <string name="enable">Zapnout</string>
<string name="disable">Vypnout</string> <string name="disable">Vypnout</string>
<string name="master_key">Hlavní klíč</string> <string name="master_key">Hlavní klíč</string>
@@ -372,7 +372,7 @@
<string name="entry_otp">OTP</string> <string name="entry_otp">OTP</string>
<string name="error_invalid_OTP">Neplatná OTP tajnost.</string> <string name="error_invalid_OTP">Neplatná OTP tajnost.</string>
<string name="error_disallow_no_credentials">Nejméně jeden přihlašovací údaj musí být zadán.</string> <string name="error_disallow_no_credentials">Nejméně jeden přihlašovací údaj musí být zadán.</string>
<string name="error_copy_group_here">Sem skupinu kopírovat nemůžete.</string> <string name="error_copy_group_here">Sem skupinu kopírovat nelze.</string>
<string name="error_otp_secret_key">Tajný klíč musí mít formát Base32.</string> <string name="error_otp_secret_key">Tajný klíč musí mít formát Base32.</string>
<string name="error_otp_counter">Čítač musít být mezi %1$d a %2$d.</string> <string name="error_otp_counter">Čítač musít být mezi %1$d a %2$d.</string>
<string name="error_otp_period">Interval musít být mezi %1$d a %2$d vteřinami.</string> <string name="error_otp_period">Interval musít být mezi %1$d a %2$d vteřinami.</string>
@@ -384,14 +384,14 @@
<string name="contains_duplicate_uuid">Databáze obsahuje duplikátní UUID.</string> <string name="contains_duplicate_uuid">Databáze obsahuje duplikátní UUID.</string>
<string name="contains_duplicate_uuid_procedure">Opravit chybu založením nového UUID pro duplikáty a pokračovat\?</string> <string name="contains_duplicate_uuid_procedure">Opravit chybu založením nového UUID pro duplikáty a pokračovat\?</string>
<string name="database_opened">Databáze otevřena</string> <string name="database_opened">Databáze otevřena</string>
<string name="clipboard_explanation_summary">Kopírujte pole záznamů pomocí schránky Vašeho zařízení</string> <string name="clipboard_explanation_summary">Kopírovat kolonky záznamů pomocí schránky svého zařízení</string>
<string name="advanced_unlock_explanation_summary">K snadnějšímu otevření databáze použijte pokročilé odemknutí</string> <string name="advanced_unlock_explanation_summary">K snadnějšímu otevření databáze použijte rozšířené odemknutí</string>
<string name="database_data_compression_title">Komprese dat</string> <string name="database_data_compression_title">Komprese dat</string>
<string name="database_data_compression_summary">Komprese dat snižuje velikost databáze</string> <string name="database_data_compression_summary">Komprese dat snižuje velikost databáze</string>
<string name="max_history_items_title">Maximální počet</string> <string name="max_history_items_title">Maximální počet</string>
<string name="max_history_items_summary">Omezit počet položek v historii záznamu</string> <string name="max_history_items_summary">Omezit počet položek v historii záznamu</string>
<string name="max_history_size_title">Maximální velikost</string> <string name="max_history_size_title">Maximální velikost</string>
<string name="max_history_size_summary">Omezit velikost historie na záznam (v bajtech)</string> <string name="max_history_size_summary">Omezit velikost historie na záznam</string>
<string name="settings_database_recommend_changing_master_key_title">Doporučit změnu</string> <string name="settings_database_recommend_changing_master_key_title">Doporučit změnu</string>
<string name="settings_database_recommend_changing_master_key_summary">Dporučit změnu hlavního klíče (dny)</string> <string name="settings_database_recommend_changing_master_key_summary">Dporučit změnu hlavního klíče (dny)</string>
<string name="settings_database_force_changing_master_key_title">Vynutit změnu</string> <string name="settings_database_force_changing_master_key_title">Vynutit změnu</string>
@@ -413,20 +413,20 @@
<string name="recycle_bin_group_title">Skupina Koš</string> <string name="recycle_bin_group_title">Skupina Koš</string>
<string name="enable_auto_save_database_title">Uložit databázi automaticky</string> <string name="enable_auto_save_database_title">Uložit databázi automaticky</string>
<string name="enable_auto_save_database_summary">Uložit databázi po každé důležité akci (v režimu \"Zápis\")</string> <string name="enable_auto_save_database_summary">Uložit databázi po každé důležité akci (v režimu \"Zápis\")</string>
<string name="entry_attachments">ipojené soubory</string> <string name="entry_attachments">ílohy</string>
<string name="menu_restore_entry_history">Obnovit historii</string> <string name="menu_restore_entry_history">Obnovit historii</string>
<string name="menu_delete_entry_history">Smazat historii</string> <string name="menu_delete_entry_history">Smazat historii</string>
<string name="keyboard_auto_go_action_title">Akce auto-klávesy</string> <string name="keyboard_auto_go_action_title">Akce auto-klávesy</string>
<string name="keyboard_auto_go_action_summary">Akce klávesy \"Jít\" po stisknutí klávesy \"Položka\"</string> <string name="keyboard_auto_go_action_summary">Akce klávesy \"Jít\" po stisknutí klávesy \"Kolonka\"</string>
<string name="download_attachment">Stáhnout %1$s</string> <string name="download_attachment">Stáhnout %1$s</string>
<string name="download_initialization">Zahajuji…</string> <string name="download_initialization">Zahajuji…</string>
<string name="download_progression">Probíhá: %1$d%%</string> <string name="download_progression">Probíhá: %1$d%%</string>
<string name="download_finalization">Dokončuji…</string> <string name="download_finalization">Dokončuji…</string>
<string name="download_complete">Kompletní!</string> <string name="download_complete">Kompletní!</string>
<string name="hide_expired_entries_title">Skrýt propadlé záznamy</string> <string name="hide_expired_entries_title">Skrýt propadlé záznamy</string>
<string name="hide_expired_entries_summary">Propadlé záznamy nejsou ukázány</string> <string name="hide_expired_entries_summary">Propadlé záznamy nebudou ukázány</string>
<string name="contact">Kontakt</string> <string name="contact">Kontakt</string>
<string name="contribution">Příspěvky</string> <string name="contribution">Příspě</string>
<string name="feedback">Feedback</string> <string name="feedback">Feedback</string>
<string name="auto_focus_search_title">Snadné hledání</string> <string name="auto_focus_search_title">Snadné hledání</string>
<string name="auto_focus_search_summary">Při otevření databáze žádat hledání</string> <string name="auto_focus_search_summary">Při otevření databáze žádat hledání</string>
@@ -436,23 +436,23 @@
<string name="remember_keyfile_locations_summary">Uchová informaci o tom, kde jsou uloženy soubory s klíči</string> <string name="remember_keyfile_locations_summary">Uchová informaci o tom, kde jsou uloženy soubory s klíči</string>
<string name="show_recent_files_title">Ukázat nedávné soubory</string> <string name="show_recent_files_title">Ukázat nedávné soubory</string>
<string name="show_recent_files_summary">Ukázat umístění nedávných databází</string> <string name="show_recent_files_summary">Ukázat umístění nedávných databází</string>
<string name="hide_broken_locations_title">Skrýt špatné odkazy na databáze</string> <string name="hide_broken_locations_title">Skrýt chybné odkazy na databáze</string>
<string name="hide_broken_locations_summary">Skrýt nesprávné odkazy v seznamu nedávných databází</string> <string name="hide_broken_locations_summary">Skrýt chybné odkazy v seznamu nedávných databází</string>
<string name="warning_database_read_only">Udělit právo zápisu pro uložení změn v databázi</string> <string name="warning_database_read_only">Udělit právo zápisu pro uložení změn v databázi</string>
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft je &lt;strong&gt;open source&lt;/strong&gt; a &lt;strong&gt;bey reklam&lt;/strong&gt;. <string name="html_about_licence">KeePassDX © %1$d Kunzisoft je &lt;strong&gt;open source&lt;/strong&gt; a &lt;strong&gt;bey reklam&lt;/strong&gt;.
\nJe poskytován jak je, pod licencí &lt;strong&gt;GPLv3&lt;/strong&gt;, bez jakékoli záruky.</string> \nJe poskytován jak je, pod licencí &lt;strong&gt;GPLv3&lt;/strong&gt;, bez jakékoli záruky.</string>
<string name="html_about_contribution">Abychom si &lt;strong&gt;udrželi svoji svobodu&lt;/strong&gt;, &lt;strong&gt;opravili chyby&lt;/strong&gt;,&lt;strong&gt;doplnili funkce&lt;/strong&gt; a &lt;strong&gt;byli vždy aktivní&lt;/strong&gt;, počítáme s Vaším &lt;strong&gt;přispěním&lt;/strong&gt;.</string> <string name="html_about_contribution">Abychom si &lt;strong&gt;udrželi svoji svobodu&lt;/strong&gt;, &lt;strong&gt;opravili chyby&lt;/strong&gt;,&lt;strong&gt;doplnili funkce&lt;/strong&gt; a &lt;strong&gt;byli vždy aktivní&lt;/strong&gt;, počítáme s Vaším &lt;strong&gt;přispěním&lt;/strong&gt;.</string>
<string name="error_create_database">Nedaří se vytvořit soubor s databází.</string> <string name="error_create_database">Nepodařilo se vytvořit soubor databáze.</string>
<string name="entry_add_attachment">Přidat přílohu</string> <string name="entry_add_attachment">Přidat přílohu</string>
<string name="discard">Zahodit</string> <string name="discard">Zavrhnout</string>
<string name="discard_changes">Zahodit změny\?</string> <string name="discard_changes">Zavrhnout změny\?</string>
<string name="validate">Ověřit</string> <string name="validate">Zkontrolovat</string>
<string name="education_setup_OTP_summary">Nastavit správu One-Time hesla (HOTP / TOTP) pro založení tokenu požadovaného pro dvoufázové ověření (2FA).</string> <string name="education_setup_OTP_summary">Nastavit správu One-Time hesla (HOTP / TOTP) pro založení tokenu požadovaného pro dvoufázové ověření (2FA).</string>
<string name="education_setup_OTP_title">Nastavit OTP</string> <string name="education_setup_OTP_title">Nastavit OTP</string>
<string name="autofill_auto_search_summary">Automaticky navrhnout výsledky hledání z webové domény nebo ID aplikace</string> <string name="autofill_auto_search_summary">Automaticky navrhnout výsledky hledání z webové domény nebo ID aplikace</string>
<string name="autofill_auto_search_title">Samočinné hledání</string> <string name="autofill_auto_search_title">Samočinné hledání</string>
<string name="lock_database_show_button_summary">Ukáže tlačítko zámku v uživatelském rozhraní</string> <string name="lock_database_show_button_summary">Zobrazí tlačítko zámku v uživatelském rozhraní</string>
<string name="lock_database_show_button_title">Ukázat tlačítko zámku</string> <string name="lock_database_show_button_title">Zobrazit tlačítko zámku</string>
<string name="autofill_preference_title">Nastavení samovyplnění</string> <string name="autofill_preference_title">Nastavení samovyplnění</string>
<string name="warning_database_link_revoked">Přístup k souboru zrušenému správcem souborů</string> <string name="warning_database_link_revoked">Přístup k souboru zrušenému správcem souborů</string>
<string name="error_label_exists">Tento štítek již existuje.</string> <string name="error_label_exists">Tento štítek již existuje.</string>
@@ -469,14 +469,14 @@
<string name="subdomain_search_title">Hledat v subdoméně</string> <string name="subdomain_search_title">Hledat v subdoméně</string>
<string name="error_string_type">Tento text se s požadovanou položkou neshoduje.</string> <string name="error_string_type">Tento text se s požadovanou položkou neshoduje.</string>
<string name="content_description_add_item">Přidat položku</string> <string name="content_description_add_item">Přidat položku</string>
<string name="keyboard_previous_fill_in_summary">Automaticky přepnout na předchozí klávesnici po provedení Akce auto-klávesy</string> <string name="keyboard_previous_fill_in_summary">Automaticky přepnout na předchozí klávesnici po provedení \"Akce auto-klávesy\"</string>
<string name="keyboard_previous_fill_in_title">Akce auto-klávesy</string> <string name="keyboard_previous_fill_in_title">Akce auto-klávesy</string>
<string name="keyboard_previous_database_credentials_summary">Automaticky přepnout zpět na předchozí klávesnici na obrazovce ověřovacích údajů databáze</string> <string name="keyboard_previous_database_credentials_summary">Automaticky přepnout zpět na předchozí klávesnici na obrazovce ověřovacích údajů databáze</string>
<string name="keyboard_previous_database_credentials_title">Obrazovka ověřovacích údajů databáze</string> <string name="keyboard_previous_database_credentials_title">Obrazovka ověřovacích údajů databáze</string>
<string name="keyboard_change">Přepnout klávesnici</string> <string name="keyboard_change">Přepnout klávesnici</string>
<string name="upload_attachment">Nahrát %1$s</string> <string name="upload_attachment">Nahrát %1$s</string>
<string name="content_description_credentials_information">Info o údajích</string> <string name="content_description_credentials_information">Informace o údajích</string>
<string name="warning_file_too_big">Databáze KeePassu předpokládá uchovávat jen malé pomocné sobory (např. PGP soubory). <string name="warning_file_too_big">Databáze KeePassu předpokládá uchovávat jen malé pomocné sobory (např. PGP soubory s klíči).
\n \n
\nVaše databáze by se mohla značně zvětšit a tímto nahráním tak ztratit na výkonnosti.</string> \nVaše databáze by se mohla značně zvětšit a tímto nahráním tak ztratit na výkonnosti.</string>
<string name="warning_replace_file">Nahráním tohoto souboru nahradíte existující soubor.</string> <string name="warning_replace_file">Nahráním tohoto souboru nahradíte existující soubor.</string>
@@ -497,41 +497,60 @@
<string name="autofill_ask_to_save_data_title">Zeptat se před uložením</string> <string name="autofill_ask_to_save_data_title">Zeptat se před uložením</string>
<string name="autofill_save_search_info_summary">Pokuste se uložit údaje hledání, když manuálně vybíráte položku</string> <string name="autofill_save_search_info_summary">Pokuste se uložit údaje hledání, když manuálně vybíráte položku</string>
<string name="autofill_save_search_info_title">Uložit info hledání</string> <string name="autofill_save_search_info_title">Uložit info hledání</string>
<string name="autofill_close_database_summary">Zavřít databázi po samodoplnění polí</string> <string name="autofill_close_database_summary">Zavřít databázi po samovyplnění polí</string>
<string name="autofill_close_database_title">Zavřít databázi</string> <string name="autofill_close_database_title">Zavřít databázi</string>
<string name="keyboard_previous_lock_summary">Po uzamknutí databáze automaticky přepnout zpět na předchozí klávesnici</string> <string name="keyboard_previous_lock_summary">Po uzamknutí databáze automaticky přepnout zpět na předchozí klávesnici</string>
<string name="keyboard_previous_lock_title">Uzamknout databázi</string> <string name="keyboard_previous_lock_title">Uzamknout databázi</string>
<string name="keyboard_save_search_info_summary">Pokuste se uložit sdílené info, když manuálné vybíráte položku</string> <string name="keyboard_save_search_info_summary">Pokuste se uložit sdílené info, když manuálné vybíráte položku</string>
<string name="keyboard_save_search_info_title">Uložit sdílené info</string> <string name="keyboard_save_search_info_title">Uložit sdílené info</string>
<string name="notification">Oznámení</string> <string name="notification">Oznámení</string>
<string name="crypto_object_not_initialized">Krypto objekt nelze načíst.</string>
<string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string> <string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string>
<string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string> <string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string>
<string name="warning_empty_recycle_bin">Trvale odstranit všechny položky z koše\?</string> <string name="warning_empty_recycle_bin">Trvale odstranit všechny uzly z koše\?</string>
<string name="registration_mode">Režim registrace</string> <string name="registration_mode">Režim registrace</string>
<string name="save_mode">Režim ukládání</string> <string name="save_mode">Režim ukládání</string>
<string name="search_mode">Režim vyhledávání</string> <string name="search_mode">Režim vyhledávání</string>
<string name="error_field_name_already_exists">Jméno položky již existuje.</string> <string name="error_field_name_already_exists">Jméno kolonky již existuje.</string>
<string name="error_registration_read_only">Uložení nové položky v režimu databáze pouze pro čtení není povoleno</string> <string name="error_registration_read_only">Uložení nové položky v režimu databáze pouze pro čtení není povoleno</string>
<string name="enter">Enter</string> <string name="enter">Enter</string>
<string name="backspace">Backspace</string> <string name="backspace">Backspace</string>
<string name="select_entry">Vybrat záznam</string> <string name="select_entry">Vybrat záznam</string>
<string name="back_to_previous_keyboard">Zpět na předchozí klávesnici</string> <string name="back_to_previous_keyboard">Zpět na předchozí klávesnici</string>
<string name="custom_fields">Vlastní položky</string> <string name="custom_fields">Vlastní kolonky</string>
<string name="advanced_unlock_delete_all_key_warning">Smazat všechny šifrovací klíče související s rozpoznáním pokročilého odemknutí\?</string> <string name="advanced_unlock_delete_all_key_warning">Smazat všechny šifrovací klíče související s rozpoznáním rozšířeného odemknutí\?</string>
<string name="device_credential_unlock_enable_summary">Dovolí pro otevření databáze použít heslo Vašeho zařízení</string> <string name="device_credential_unlock_enable_summary">Dovolí pro otevření databáze použít heslo Vašeho zařízení</string>
<string name="device_credential_unlock_enable_title">Odemknutí heslem zařízení</string> <string name="device_credential_unlock_enable_title">Odemknutí heslem zařízení</string>
<string name="device_credential">Heslo zařízení</string> <string name="device_credential">Heslo zařízení</string>
<string name="credential_before_click_advanced_unlock_button">Zadejte heslo a klikněte na tlačítko \"Pokročilé odemknutí\".</string> <string name="credential_before_click_advanced_unlock_button">Zadejte heslo a klikněte na tlačítko \"Rozšířené odemknutí\".</string>
<string name="advanced_unlock_prompt_not_initialized">Nelze inicializovat pobídku pro pokročilé odemknutí.</string> <string name="advanced_unlock_prompt_not_initialized">Nelze inicializovat pobídku pro rozšířené odemknutí.</string>
<string name="advanced_unlock_scanning_error">Chyba při pokročilém odemknutí: %1$s</string> <string name="advanced_unlock_scanning_error">Chyba při rozšířeném odemknutí: %1$s</string>
<string name="advanced_unlock_not_recognized">Otisk pro pokročilé odemknutí nebyl rozpoznán</string> <string name="advanced_unlock_not_recognized">Otisk pro rozšířené odemknutí nebyl rozpoznán</string>
<string name="advanced_unlock_invalid_key">Nelze načíst klíč pokročilého odemknutí. Prosím, smažte jej a opakujte proces rozpoznání uzamknutí.</string> <string name="advanced_unlock_invalid_key">Nelze načíst klíč rozšířeného odemknutí. Prosím, smažte jej a opakujte proces rozpoznání odemknutí.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Načíst důvěrný údaj pomocí dat pokročilého odemknutí</string> <string name="advanced_unlock_prompt_extract_credential_message">Načíst důvěrný údaj pomocí dat rozšířeného odemknutí</string>
<string name="advanced_unlock_prompt_extract_credential_title">Otevřít databázi pomocí rozpoznání pokročilého odemknutí</string> <string name="advanced_unlock_prompt_extract_credential_title">Otevřít databázi pomocí rozpoznání rozšířeného odemknutí</string>
<string name="advanced_unlock_prompt_store_credential_message">Varování: Pokud použijete rozpoznání pokročilého odemknutí, musíte si i nadále pamatovat hlavní heslo.</string> <string name="advanced_unlock_prompt_store_credential_message">Varování: Pokud použijete rozpoznání rozšířeného odemknutí, musíte si i nadále pamatovat hlavní heslo.</string>
<string name="advanced_unlock_prompt_store_credential_title">Rozpoznání pokročilého odemknutí</string> <string name="advanced_unlock_prompt_store_credential_title">Rozpoznání rozšířeného odemknutí</string>
<string name="open_advanced_unlock_prompt_store_credential">Pro uložení důvěrných údajů otevřete pobídku pokročilého odemknutí</string> <string name="open_advanced_unlock_prompt_store_credential">Pro uložení důvěrných údajů otevřete pobídku rozšířeného odemknutí</string>
<string name="open_advanced_unlock_prompt_unlock_database">Pro odemknutí databáze otevřete pobídku pokročilého odemknutí</string> <string name="open_advanced_unlock_prompt_unlock_database">Pro odemknutí databáze otevřete pobídku rozšířeného odemknutí</string>
<string name="menu_keystore_remove_key">Smazat klíč pokročilého odemknutí</string> <string name="menu_keystore_remove_key">Smazat klíč rozšířeného odemknutí</string>
<string name="education_advanced_unlock_title">Rozšířené odemknutí databáze</string>
<string name="advanced_unlock_timeout">Časový limit rozšířeného odemknutí</string>
<string name="temp_advanced_unlock_timeout_summary">Trvání použití rozšířeného odemknutí než bude obsah téhož smazán</string>
<string name="temp_advanced_unlock_enable_summary">Za účelem rozšířeného odemknutí neukládat žádný šifrovaný obsah</string>
<string name="temp_advanced_unlock_enable_title">Přechodné rozšířené odemknutí</string>
<string name="advanced_unlock_tap_delete">Pro odstranění klíčů rozšířeného odemknutí klepnout</string>
<string name="kdf_Argon2id">Argon2id</string>
<string name="kdf_Argon2d">Argon2d</string>
<string name="education_advanced_unlock_summary">Abyste rychle odemknuli databázi, propojte své heslo s naskenovanou biometrikou nebo údaji zámku zařízení.</string>
<string name="temp_advanced_unlock_timeout_title">Vypršení pokročilého odemknutí</string>
<string name="content">Obsah</string>
<string name="error_rebuild_list">Seznam nelze řádně sestavit.</string>
<string name="error_database_uri_null">URI databáze nelze načíst.</string>
<string name="autofill_inline_suggestions_keyboard">Návrhy samovyplnění přidány.</string>
<string name="autofill_inline_suggestions_title">Návrhy inline</string>
<string name="autofill_inline_suggestions_summary">Pokusí ze zobrazit návrhy samovyplnění přímo z kompatibilní klávesnice</string>
<string name="warning_database_revoked">Přístup k souboru odebrán správcem souborů, uzavřete databázi a nově ji otevřete z jejího adresáře.</string>
<string name="menu_reload_database">Databázi nově načíst</string>
<string name="warning_database_info_changed_options">Přepsat externí změny uložením databáze nebo databázi včetně posledních změn nově načíst.</string>
<string name="warning_database_info_changed">Informace obsažená ve Vašem databázovém souboru by změněna mimo aplikaci.</string>
</resources> </resources>

View File

@@ -174,7 +174,7 @@
<string name="encryption_explanation">Databasekrypteringsalgoritme anvendt for alle data.</string> <string name="encryption_explanation">Databasekrypteringsalgoritme anvendt for alle data.</string>
<string name="kdf_explanation">For at generere nøglen til krypteringsalgoritmen, omdannes hovednøglen ved hjælp af en tilfældigt saltet nøgleafledningsfunktion.</string> <string name="kdf_explanation">For at generere nøglen til krypteringsalgoritmen, omdannes hovednøglen ved hjælp af en tilfældigt saltet nøgleafledningsfunktion.</string>
<string name="memory_usage">Hukommelsesforbrug</string> <string name="memory_usage">Hukommelsesforbrug</string>
<string name="memory_usage_explanation">Hukommelse (i bytes), som anvendes af nøgleafledningsfunktion.</string> <string name="memory_usage_explanation">Hukommelse, som anvendes af nøgleafledningsfunktion.</string>
<string name="parallelism">Parallelitet</string> <string name="parallelism">Parallelitet</string>
<string name="parallelism_explanation">Grad af parallelitet (dvs. antallet af tråde), som anvendes af nøgleafledningsfunktion.</string> <string name="parallelism_explanation">Grad af parallelitet (dvs. antallet af tråde), som anvendes af nøgleafledningsfunktion.</string>
<string name="sort_menu">Sorter</string> <string name="sort_menu">Sorter</string>
@@ -391,7 +391,7 @@
<string name="max_history_items_title">Max. antal</string> <string name="max_history_items_title">Max. antal</string>
<string name="max_history_items_summary">Begræns antallet af historikposter pr. indtastning</string> <string name="max_history_items_summary">Begræns antallet af historikposter pr. indtastning</string>
<string name="max_history_size_title">Max. størrelse</string> <string name="max_history_size_title">Max. størrelse</string>
<string name="max_history_size_summary">Begræns historikstørrelsen (i bytes) pr. post</string> <string name="max_history_size_summary">Begræns historikstørrelsen pr. post</string>
<string name="settings_database_recommend_changing_master_key_title">Anbefalet fornyelse</string> <string name="settings_database_recommend_changing_master_key_title">Anbefalet fornyelse</string>
<string name="settings_database_recommend_changing_master_key_summary">Anbefal ændring af hovednøglen (dage)</string> <string name="settings_database_recommend_changing_master_key_summary">Anbefal ændring af hovednøglen (dage)</string>
<string name="settings_database_force_changing_master_key_title">Gennemtving fornyelse</string> <string name="settings_database_force_changing_master_key_title">Gennemtving fornyelse</string>
@@ -491,7 +491,6 @@
<string name="database_data_remove_unlinked_attachments_summary">Fjerner vedhæftede filer indeholdt i databasen, men ikke knyttet til en post</string> <string name="database_data_remove_unlinked_attachments_summary">Fjerner vedhæftede filer indeholdt i databasen, men ikke knyttet til en post</string>
<string name="database_data_remove_unlinked_attachments_title">Fjern ikke-sammenkædede data</string> <string name="database_data_remove_unlinked_attachments_title">Fjern ikke-sammenkædede data</string>
<string name="data">Data</string> <string name="data">Data</string>
<string name="crypto_object_not_initialized">Kryptoobjektet kunne ikke hentes.</string>
<string name="biometric_security_update_required">Biometrisk sikkerhedsopdatering påkrævet.</string> <string name="biometric_security_update_required">Biometrisk sikkerhedsopdatering påkrævet.</string>
<string name="warning_empty_keyfile_explanation">Indholdet af nøglefilen bør aldrig ændres og bør i bedste fald indeholde tilfældigt genererede data.</string> <string name="warning_empty_keyfile_explanation">Indholdet af nøglefilen bør aldrig ændres og bør i bedste fald indeholde tilfældigt genererede data.</string>
<string name="warning_empty_keyfile">Det anbefales ikke at tilføje en tom nøglefil.</string> <string name="warning_empty_keyfile">Det anbefales ikke at tilføje en tom nøglefil.</string>

View File

@@ -118,8 +118,8 @@
<string name="never">Nie</string> <string name="never">Nie</string>
<string name="no_results">Keine Suchergebnisse</string> <string name="no_results">Keine Suchergebnisse</string>
<string name="no_url_handler">Bitte einen Webbrowser installieren, um diese URL zu öffnen.</string> <string name="no_url_handler">Bitte einen Webbrowser installieren, um diese URL zu öffnen.</string>
<string name="omit_backup_search_title">Papierkorb/Sicherung nicht durchsuchen</string> <string name="omit_backup_search_title">Recycle bin und Backup nicht durchsuchen</string>
<string name="omit_backup_search_summary">Die Gruppen „Sicherung“ und „Papierkorb“ werden bei der Suche nicht berücksichtigt</string> <string name="omit_backup_search_summary">Die Gruppen „Backup“ und „Recycle bin“ werden bei der Suche nicht berücksichtigt</string>
<string name="auto_focus_search_title">Schnellsuche</string> <string name="auto_focus_search_title">Schnellsuche</string>
<string name="auto_focus_search_summary">Beim Öffnen einer Datenbank eine Suche veranlassen</string> <string name="auto_focus_search_summary">Beim Öffnen einer Datenbank eine Suche veranlassen</string>
<string name="progress_create">Neue Datenbank anlegen </string> <string name="progress_create">Neue Datenbank anlegen </string>
@@ -199,7 +199,7 @@
<string name="encryption_explanation">Verschlüsselungsalgorithmus der Datenbank wird für sämtliche Daten verwendet.</string> <string name="encryption_explanation">Verschlüsselungsalgorithmus der Datenbank wird für sämtliche Daten verwendet.</string>
<string name="kdf_explanation">Um den Schlüssel für den Verschlüsselungsalgorithmus zu generieren, wird der Hauptschlüssel umgewandelt, wobei ein zufälliger Salt in der Schlüsselberechnung verwendet wird.</string> <string name="kdf_explanation">Um den Schlüssel für den Verschlüsselungsalgorithmus zu generieren, wird der Hauptschlüssel umgewandelt, wobei ein zufälliger Salt in der Schlüsselberechnung verwendet wird.</string>
<string name="memory_usage">Speichernutzung</string> <string name="memory_usage">Speichernutzung</string>
<string name="memory_usage_explanation">Größe des Speichers (in Bytes) der für die Schlüsselableitung genutzt wird.</string> <string name="memory_usage_explanation">Größe des Speichers der für die Schlüsselableitung genutzt wird.</string>
<string name="parallelism">Parallelismus</string> <string name="parallelism">Parallelismus</string>
<string name="parallelism_explanation">Grad des Parallelismus (d. h. Anzahl der Threads), der für die Schlüsselableitung genutzt wird.</string> <string name="parallelism_explanation">Grad des Parallelismus (d. h. Anzahl der Threads), der für die Schlüsselableitung genutzt wird.</string>
<string name="sort_menu">Sortieren</string> <string name="sort_menu">Sortieren</string>
@@ -218,7 +218,7 @@
<string name="autofill_explanation_summary">Automatisches Ausfüllen aktivieren, um schnell Formulare in anderen Apps auszufüllen</string> <string name="autofill_explanation_summary">Automatisches Ausfüllen aktivieren, um schnell Formulare in anderen Apps auszufüllen</string>
<string name="clipboard">Zwischenablage</string> <string name="clipboard">Zwischenablage</string>
<string name="biometric_delete_all_key_title">Verschlüsselungsschlüssel löschen</string> <string name="biometric_delete_all_key_title">Verschlüsselungsschlüssel löschen</string>
<string name="biometric_delete_all_key_summary">Alle mit der biometrischen Erkennung verbundenen Verschlüsselungsschlüssel löschen.</string> <string name="biometric_delete_all_key_summary">Lösche alle Verschlüsselungsschlüssel, die mit der erweiterten Entsperrerkennung zusammenhängen</string>
<string name="unavailable_feature_version">Auf dem Gerät läuft Android %1$s, eine Version ab %2$s wäre aber nötig.</string> <string name="unavailable_feature_version">Auf dem Gerät läuft Android %1$s, eine Version ab %2$s wäre aber nötig.</string>
<string name="unavailable_feature_hardware">Keine entsprechende Hardware.</string> <string name="unavailable_feature_hardware">Keine entsprechende Hardware.</string>
<string name="recycle_bin_title">Papierkorb-Nutzung</string> <string name="recycle_bin_title">Papierkorb-Nutzung</string>
@@ -231,7 +231,7 @@
<string name="database_description_title">Datenbankbeschreibung</string> <string name="database_description_title">Datenbankbeschreibung</string>
<string name="database_version_title">Datenbankversion</string> <string name="database_version_title">Datenbankversion</string>
<string name="text_appearance">Text</string> <string name="text_appearance">Text</string>
<string name="application_appearance">App</string> <string name="application_appearance">Interface</string>
<string name="other">Andere</string> <string name="other">Andere</string>
<string name="keyboard">Tastatur</string> <string name="keyboard">Tastatur</string>
<string name="magic_keyboard_title">Magikeyboard</string> <string name="magic_keyboard_title">Magikeyboard</string>
@@ -267,7 +267,7 @@
<string name="education_donation_title">Mitmachen</string> <string name="education_donation_title">Mitmachen</string>
<string name="education_donation_summary">Mithelfen, um Stabilität und Sicherheit zu verbessern und weitere Funktionen zu ermöglichen.</string> <string name="education_donation_summary">Mithelfen, um Stabilität und Sicherheit zu verbessern und weitere Funktionen zu ermöglichen.</string>
<string name="html_text_ad_free">Anders als viele andere Passwortmanager ist dieser &lt;strong&gt;werbefrei&lt;/strong&gt;, &lt;strong&gt;quelloffen&lt;/strong&gt; und unter einer &lt;strong&gt;Copyleft-Lizenz&lt;/strong&gt;. Es werden keine persönlichen Daten gesammelt, in welcher Form auch immer, unabhängig von der verwendeten Version (kostenlos oder Pro).</string> <string name="html_text_ad_free">Anders als viele andere Passwortmanager ist dieser &lt;strong&gt;werbefrei&lt;/strong&gt;, &lt;strong&gt;quelloffen&lt;/strong&gt; und unter einer &lt;strong&gt;Copyleft-Lizenz&lt;/strong&gt;. Es werden keine persönlichen Daten gesammelt, in welcher Form auch immer, unabhängig von der verwendeten Version (kostenlos oder Pro).</string>
<string name="html_text_buy_pro">Mit dem Kauf der Pro-Version erhält man Zugang zu diesem &lt;strong&gt;visuellen Stil&lt;/strong&gt; und unterstützt insbesondere &lt;strong&gt;die Umsetzung gemeinschaftlicher Projektarbeiten.&lt;/strong&gt;</string> <string name="html_text_buy_pro">Mit dem Kauf der Pro-Version erhalten Sie Zugriff auf diesen &lt;strong&gt;visuellen Stil&lt;/strong&gt; und unterstützen insbesondere &lt;strong&gt;die Umsetzung gemeinschaftlicher Projekte.&lt;/strong&gt;</string>
<string name="html_text_feature_generosity">Dieser &lt;strong&gt;visuelle Stil&lt;/strong&gt; wurde wegen Ihrer Großzügigkeit freigeschaltet.</string> <string name="html_text_feature_generosity">Dieser &lt;strong&gt;visuelle Stil&lt;/strong&gt; wurde wegen Ihrer Großzügigkeit freigeschaltet.</string>
<string name="html_text_donation">Um unsere Freiheit zu bewahren und immer aktiv zu bleiben, zählen wir auf Ihren &lt;strong&gt;Beitrag.&lt;/strong&gt;</string> <string name="html_text_donation">Um unsere Freiheit zu bewahren und immer aktiv zu bleiben, zählen wir auf Ihren &lt;strong&gt;Beitrag.&lt;/strong&gt;</string>
<string name="html_text_dev_feature">Diese Funktion ist &lt;strong&gt;in Entwicklung&lt;/strong&gt; und erfordert &lt;strong&gt;Ihren Beitrag&lt;/strong&gt;, um bald verfügbar zu sein.</string> <string name="html_text_dev_feature">Diese Funktion ist &lt;strong&gt;in Entwicklung&lt;/strong&gt; und erfordert &lt;strong&gt;Ihren Beitrag&lt;/strong&gt;, um bald verfügbar zu sein.</string>
@@ -276,7 +276,7 @@
<string name="html_text_dev_feature_encourage">Sie ermutigen die Entwickler:innen, &lt;strong&gt;neue Funktionen&lt;/strong&gt; einzuführen und gemäß Ihren Anmerkungen &lt;strong&gt;Fehler zu beheben&lt;/strong&gt;.</string> <string name="html_text_dev_feature_encourage">Sie ermutigen die Entwickler:innen, &lt;strong&gt;neue Funktionen&lt;/strong&gt; einzuführen und gemäß Ihren Anmerkungen &lt;strong&gt;Fehler zu beheben&lt;/strong&gt;.</string>
<string name="html_text_dev_feature_thanks">Vielen Dank für Ihre Unterstützung.</string> <string name="html_text_dev_feature_thanks">Vielen Dank für Ihre Unterstützung.</string>
<string name="html_text_dev_feature_work_hard">Wir bemühen uns, diese Funktion bald zu veröffentlichen.</string> <string name="html_text_dev_feature_work_hard">Wir bemühen uns, diese Funktion bald zu veröffentlichen.</string>
<string name="html_text_dev_feature_upgrade">Vergessen Sie nicht, Ihre Anwendung aktuell zu halten, indem Sie neue Versionen installieren.</string> <string name="html_text_dev_feature_upgrade">Denken Sie daran, Ihre App auf dem neuesten Stand zu halten, indem Sie neue Versionen installieren.</string>
<string name="download">Download</string> <string name="download">Download</string>
<string name="contribute">Unterstützen</string> <string name="contribute">Unterstützen</string>
<string name="encryption_chacha20">ChaCha20</string> <string name="encryption_chacha20">ChaCha20</string>
@@ -299,10 +299,10 @@
<string name="enable_read_only_title">Schreibgeschützt</string> <string name="enable_read_only_title">Schreibgeschützt</string>
<string name="education_read_only_title">Schreibschutz der Datenbank aktivieren</string> <string name="education_read_only_title">Schreibschutz der Datenbank aktivieren</string>
<string name="enable_read_only_summary">Datenbank standardmäßig schreibgeschützt öffnen</string> <string name="enable_read_only_summary">Datenbank standardmäßig schreibgeschützt öffnen</string>
<string name="education_read_only_summary">Den Öffnungsmodus für die Sitzung ändern. <string name="education_read_only_summary">Den Öffnungsmodus für die Sitzung ändern.
\n \n
\n„Schreibgeschützt“ verhindert unbeabsichtigte Änderungen an der Datenbank. \n„Schreibgeschützt“ verhindert unbeabsichtigte Änderungen an der Datenbank.
\nMit „Änderbar“ können alle Elemente hinzugefügt, gelöscht oder geändert werden.</string> \nMit „Änderbar“ können Sie alle Elemente nach Belieben hinzufügen, löschen oder ändern.</string>
<string name="edit_entry">Eintrag bearbeiten</string> <string name="edit_entry">Eintrag bearbeiten</string>
<string name="error_load_database">Datenbank kann nicht geladen werden.</string> <string name="error_load_database">Datenbank kann nicht geladen werden.</string>
<string name="error_load_database_KDF_memory">Laden des Schlüssels fehlgeschlagen. Bitte versuchen, die „Speicherplatznutzung“ von KDF zu verringern.</string> <string name="error_load_database_KDF_memory">Laden des Schlüssels fehlgeschlagen. Bitte versuchen, die „Speicherplatznutzung“ von KDF zu verringern.</string>
@@ -336,7 +336,7 @@
<string name="show_recent_files_summary">Speicherort zuletzt verwendeter Datenbanken anzeigen</string> <string name="show_recent_files_summary">Speicherort zuletzt verwendeter Datenbanken anzeigen</string>
<string name="hide_broken_locations_title">Defekte Datenbankverknüpfungen ausblenden</string> <string name="hide_broken_locations_title">Defekte Datenbankverknüpfungen ausblenden</string>
<string name="hide_broken_locations_summary">Nicht funktionierende Verknüpfungen in der Liste der zuletzt verwendeten Datenbanken ausblenden</string> <string name="hide_broken_locations_summary">Nicht funktionierende Verknüpfungen in der Liste der zuletzt verwendeten Datenbanken ausblenden</string>
<string name="do_not_kill_app">Nicht die Anwendung beenden …</string> <string name="do_not_kill_app">App nicht beenden …</string>
<string name="lock_database_back_root_summary">Datenbank sperren, wenn auf dem Hauptbildschirm der Zurück-Button gedrückt wird</string> <string name="lock_database_back_root_summary">Datenbank sperren, wenn auf dem Hauptbildschirm der Zurück-Button gedrückt wird</string>
<string name="clear_clipboard_notification_title">Beim Schließen löschen</string> <string name="clear_clipboard_notification_title">Beim Schließen löschen</string>
<string name="recycle_bin">Papierkorb</string> <string name="recycle_bin">Papierkorb</string>
@@ -373,8 +373,8 @@
<string name="biometric">Biometrisch</string> <string name="biometric">Biometrisch</string>
<string name="enable">Aktivieren</string> <string name="enable">Aktivieren</string>
<string name="disable">Deaktivieren</string> <string name="disable">Deaktivieren</string>
<string name="biometric_auto_open_prompt_title">Biometrische Abfrage automatisch öffnen</string> <string name="biometric_auto_open_prompt_title">Abfrage automatisch öffnen</string>
<string name="biometric_auto_open_prompt_summary">Automatisch nach Biometrie fragen, wenn die Datenbank dafür eingerichtet ist</string> <string name="biometric_auto_open_prompt_summary">Automatisch nach der erweiterten Entsperrung fragen, wenn die Datenbank dafür eingerichtet ist</string>
<string name="master_key">Hauptschlüssel</string> <string name="master_key">Hauptschlüssel</string>
<string name="security">Sicherheit</string> <string name="security">Sicherheit</string>
<string name="entry_history">Verlauf</string> <string name="entry_history">Verlauf</string>
@@ -407,7 +407,7 @@
<string name="max_history_items_title">Maximale Anzahl</string> <string name="max_history_items_title">Maximale Anzahl</string>
<string name="max_history_items_summary">Anzahl der Verlaufseinträge pro Eintrag begrenzen</string> <string name="max_history_items_summary">Anzahl der Verlaufseinträge pro Eintrag begrenzen</string>
<string name="max_history_size_title">Maximale Größe</string> <string name="max_history_size_title">Maximale Größe</string>
<string name="max_history_size_summary">Verlaufsumfang (in Bytes) pro Eintrag begrenzen</string> <string name="max_history_size_summary">Verlaufsumfang pro Eintrag begrenzen</string>
<string name="settings_database_recommend_changing_master_key_title">Erneuerung empfehlen</string> <string name="settings_database_recommend_changing_master_key_title">Erneuerung empfehlen</string>
<string name="settings_database_recommend_changing_master_key_summary">(Nach Tagen) Änderung des Hauptschlüssels empfehlen</string> <string name="settings_database_recommend_changing_master_key_summary">(Nach Tagen) Änderung des Hauptschlüssels empfehlen</string>
<string name="settings_database_force_changing_master_key_title">Erneuerung erzwingen</string> <string name="settings_database_force_changing_master_key_title">Erneuerung erzwingen</string>
@@ -500,7 +500,7 @@
<string name="upload_attachment">%1$s hochladen</string> <string name="upload_attachment">%1$s hochladen</string>
<string name="education_add_attachment_title">Anhang hinzufügen</string> <string name="education_add_attachment_title">Anhang hinzufügen</string>
<string name="database_data_remove_unlinked_attachments_summary">Entfernt Anhänge die in der Datenbank enthalten, aber keinem Eintrag zugeordnet sind</string> <string name="database_data_remove_unlinked_attachments_summary">Entfernt Anhänge die in der Datenbank enthalten, aber keinem Eintrag zugeordnet sind</string>
<string name="warning_sure_add_file">Die Datei trotzdem hinzufügen\?</string> <string name="warning_sure_add_file">Soll die Datei trotzdem hinzugefügt werden\?</string>
<string name="show_uuid_summary">Zeigt die mit einem Eintrag verknüpfte UUID an</string> <string name="show_uuid_summary">Zeigt die mit einem Eintrag verknüpfte UUID an</string>
<string name="show_uuid_title">UUID anzeigen</string> <string name="show_uuid_title">UUID anzeigen</string>
<string name="autofill_read_only_save">Das Speichern von Daten ist für eine als schreibgeschützt geöffnete Datenbank nicht zulässig.</string> <string name="autofill_read_only_save">Das Speichern von Daten ist für eine als schreibgeschützt geöffnete Datenbank nicht zulässig.</string>
@@ -508,7 +508,6 @@
<string name="keyboard_previous_lock_summary">Automatisches Zurückschalten zur vorherigen Tastatur nach dem Sperren der Datenbank</string> <string name="keyboard_previous_lock_summary">Automatisches Zurückschalten zur vorherigen Tastatur nach dem Sperren der Datenbank</string>
<string name="keyboard_previous_lock_title">Datenbank sperren</string> <string name="keyboard_previous_lock_title">Datenbank sperren</string>
<string name="notification">Benachrichtigung</string> <string name="notification">Benachrichtigung</string>
<string name="crypto_object_not_initialized">Kryptoobjekt kann nicht abgerufen werden.</string>
<string name="biometric_security_update_required">Biometrisches Sicherheitsupdate erforderlich.</string> <string name="biometric_security_update_required">Biometrisches Sicherheitsupdate erforderlich.</string>
<string name="configure_biometric">Es sind keine biometrischen oder Geräteanmeldeinformationen registriert.</string> <string name="configure_biometric">Es sind keine biometrischen oder Geräteanmeldeinformationen registriert.</string>
<string name="registration_mode">Registrierungsmodus</string> <string name="registration_mode">Registrierungsmodus</string>
@@ -528,5 +527,35 @@
<string name="open_advanced_unlock_prompt_store_credential">Öffne den erweiterten Entsperrdialog zum Speichern von Anmeldeinformationen</string> <string name="open_advanced_unlock_prompt_store_credential">Öffne den erweiterten Entsperrdialog zum Speichern von Anmeldeinformationen</string>
<string name="open_advanced_unlock_prompt_unlock_database">Öffnen des erweiterten Entsperrdialogs zum Entsperren der Datenbank</string> <string name="open_advanced_unlock_prompt_unlock_database">Öffnen des erweiterten Entsperrdialogs zum Entsperren der Datenbank</string>
<string name="menu_keystore_remove_key">Löschen des Schlüssels zum erweiterten Entsperren</string> <string name="menu_keystore_remove_key">Löschen des Schlüssels zum erweiterten Entsperren</string>
<string name="advanced_unlock_prompt_store_credential_title">Fortschrittliche Entsperrerkennung</string> <string name="advanced_unlock_prompt_store_credential_title">Erweiterte Entsperrerkennung</string>
<string name="education_advanced_unlock_summary">Ihr Passwort verbinden mit Ihrem gescannten biometrischen oder berechtigen Gerät um schnell Ihre Datenbank zu entsperren.</string>
<string name="education_advanced_unlock_title">Erweiterte Entsperrung der Datenbank</string>
<string name="advanced_unlock_timeout">Verfallzeit der erweiterten Entsperrung</string>
<string name="temp_advanced_unlock_timeout_summary">Dauer der erweiterten Entsperrung bevor sein Inhalt gelöscht wird</string>
<string name="temp_advanced_unlock_timeout_title">Verfall der erweiterten Entsperrung</string>
<string name="temp_advanced_unlock_enable_summary">Keinen verschlüsselten Inhalt speichern, um erweiterte Entsperrung zu benutzen</string>
<string name="temp_advanced_unlock_enable_title">Temporäre erweiterte Entsperrung</string>
<string name="device_credential_unlock_enable_summary">Erlaubt Ihn die Geräteanmeldedaten zum Öffnen der Datenbank zu verwenden</string>
<string name="advanced_unlock_tap_delete">Drücken um erweiterte Entsperrschlüssel zu löschen</string>
<string name="content">Inhalt</string>
<string name="advanced_unlock_prompt_extract_credential_title">Öffne Datenbank mit erweiterter Entsperrerkennung</string>
<string name="enter">Eingabetaste</string>
<string name="backspace">Rücktaste</string>
<string name="select_entry">Wähle Eintrag</string>
<string name="back_to_previous_keyboard">Zurück zur vorherigen Tastatur</string>
<string name="custom_fields">Benutzerdefinierte Felder</string>
<string name="advanced_unlock_delete_all_key_warning">Alle Verschlüsselungsschlüssel, die mit der erweiterten Entsperrerkennung zusammenhängen, löschen\?</string>
<string name="device_credential_unlock_enable_title">Geräteanmeldedaten entsperren</string>
<string name="device_credential">Geräteanmeldedaten</string>
<string name="credential_before_click_advanced_unlock_button">Geben sie das Passwort ein, und dann klicken sie den \"Erweitertes Entsperren\" Knopf.</string>
<string name="advanced_unlock_prompt_not_initialized">Initialisieren des erweitertes Entsperren Dialogs fehlgeschlagen.</string>
<string name="advanced_unlock_scanning_error">Erweitertes Entsperren Fehler: %1$s</string>
<string name="advanced_unlock_not_recognized">Konnte den Abdruck des erweiterten Entsperrens nicht erkennen</string>
<string name="advanced_unlock_invalid_key">Kann den Schlüssel zum erweiterten Entsperren nicht lesen. Bitte löschen sie ihn und wiederholen sie Prozedur zum Erkennen des Entsperrens.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Extrahiere Datenbankanmeldedaten mit Daten aus erweitertem Entsperren</string>
<string name="kdf_Argon2id">Argon2id</string>
<string name="kdf_Argon2d">Argon2d</string>
<string name="error_rebuild_list">Die Liste kann nicht ordnungsgemäß neu erstellt werden.</string>
<string name="error_database_uri_null">Datenbank-URI kann nicht abgerufen werden.</string>
<string name="menu_reload_database">Datenbank neu laden</string>
</resources> </resources>

View File

@@ -165,7 +165,7 @@
<string name="encryption_explanation">Αλγόριθμος κρυπτογράφησης βάσης δεδομένων που χρησιμοποιείται για όλα τα δεδομένα.</string> <string name="encryption_explanation">Αλγόριθμος κρυπτογράφησης βάσης δεδομένων που χρησιμοποιείται για όλα τα δεδομένα.</string>
<string name="kdf_explanation">Για να δημιουργηθεί το κλειδί για τον αλγόριθμο κρυπτογράφησης, το κύριο κλειδί μετασχηματίζεται χρησιμοποιώντας μια τυχαία αλατισμένη λειτουργία εξαγωγής κλειδιών.</string> <string name="kdf_explanation">Για να δημιουργηθεί το κλειδί για τον αλγόριθμο κρυπτογράφησης, το κύριο κλειδί μετασχηματίζεται χρησιμοποιώντας μια τυχαία αλατισμένη λειτουργία εξαγωγής κλειδιών.</string>
<string name="memory_usage">Χρήση μνήμης</string> <string name="memory_usage">Χρήση μνήμης</string>
<string name="memory_usage_explanation">Ποσότητα μνήμης (σε bytes) που θα χρησιμοποιηθεί από τη λειτουργία εξαγωγής κλειδιών.</string> <string name="memory_usage_explanation">Ποσότητα μνήμης που θα χρησιμοποιηθεί από τη λειτουργία εξαγωγής κλειδιών.</string>
<string name="parallelism">Παραλληλισμός</string> <string name="parallelism">Παραλληλισμός</string>
<string name="parallelism_explanation">Βαθμός παραλληλισμού (δηλ. Αριθμός νημάτων) που χρησιμοποιείται από τη συνάρτηση εξαγωγής κλειδιών.</string> <string name="parallelism_explanation">Βαθμός παραλληλισμού (δηλ. Αριθμός νημάτων) που χρησιμοποιείται από τη συνάρτηση εξαγωγής κλειδιών.</string>
<string name="sort_menu">Ταξινόμηση</string> <string name="sort_menu">Ταξινόμηση</string>
@@ -392,7 +392,7 @@
<string name="max_history_items_title">Μέγιστος αριθμός</string> <string name="max_history_items_title">Μέγιστος αριθμός</string>
<string name="max_history_items_summary">Περιορίστε τον αριθμό των στοιχείων ιστορικού ανά καταχώριση</string> <string name="max_history_items_summary">Περιορίστε τον αριθμό των στοιχείων ιστορικού ανά καταχώριση</string>
<string name="max_history_size_title">Μέγιστο μέγεθος</string> <string name="max_history_size_title">Μέγιστο μέγεθος</string>
<string name="max_history_size_summary">Περιορίστε το μέγεθος ιστορικού (σε bytes) ανά καταχώριση</string> <string name="max_history_size_summary">Περιορίστε το μέγεθος ιστορικού ανά καταχώριση</string>
<string name="settings_database_recommend_changing_master_key_title">Συστήστε ανανέωση</string> <string name="settings_database_recommend_changing_master_key_title">Συστήστε ανανέωση</string>
<string name="settings_database_recommend_changing_master_key_summary">Προτεινόμενη αλλαγή του κύριου κλειδιού (ημέρες)</string> <string name="settings_database_recommend_changing_master_key_summary">Προτεινόμενη αλλαγή του κύριου κλειδιού (ημέρες)</string>
<string name="settings_database_force_changing_master_key_title">Εξαναγκαστική ανανέωση</string> <string name="settings_database_force_changing_master_key_title">Εξαναγκαστική ανανέωση</string>
@@ -503,7 +503,6 @@
<string name="keyboard_save_search_info_summary">Προσπαθήστε να αποθηκεύσετε κοινόχρηστες πληροφορίες όταν κάνετε μια χειροκίνητη επιλογή καταχώρησης</string> <string name="keyboard_save_search_info_summary">Προσπαθήστε να αποθηκεύσετε κοινόχρηστες πληροφορίες όταν κάνετε μια χειροκίνητη επιλογή καταχώρησης</string>
<string name="keyboard_save_search_info_title">Αποθήκευση κοινόχρηστων πληροφοριών</string> <string name="keyboard_save_search_info_title">Αποθήκευση κοινόχρηστων πληροφοριών</string>
<string name="notification">Ειδοποίηση</string> <string name="notification">Ειδοποίηση</string>
<string name="crypto_object_not_initialized">Δεν είναι δυνατή η ανάκτηση κρυπτογραφικού αντικειμένου.</string>
<string name="biometric_security_update_required">Απαιτείται ενημέρωση βιομετρικής ασφάλειας.</string> <string name="biometric_security_update_required">Απαιτείται ενημέρωση βιομετρικής ασφάλειας.</string>
<string name="configure_biometric">Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο.</string> <string name="configure_biometric">Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο.</string>
<string name="warning_empty_recycle_bin">Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης;</string> <string name="warning_empty_recycle_bin">Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης;</string>
@@ -542,4 +541,15 @@
<string name="temp_advanced_unlock_timeout_title">Λήξη προηγμένου ξεκλειδώματος</string> <string name="temp_advanced_unlock_timeout_title">Λήξη προηγμένου ξεκλειδώματος</string>
<string name="advanced_unlock_tap_delete">Πατήστε για διαγραφή προηγμένων κλειδιών ξεκλειδώματος</string> <string name="advanced_unlock_tap_delete">Πατήστε για διαγραφή προηγμένων κλειδιών ξεκλειδώματος</string>
<string name="content">Περιεχόμενα</string> <string name="content">Περιεχόμενα</string>
<string name="kdf_Argon2id">Argon2id</string>
<string name="kdf_Argon2d">Argon2d</string>
<string name="error_rebuild_list">Δεν είναι δυνατή η σωστή αναδημιουργία της λίστας.</string>
<string name="error_database_uri_null">Δεν είναι δυνατή η ανάκτηση του URI βάσης δεδομένων.</string>
<string name="autofill_inline_suggestions_keyboard">Προστέθηκαν προτάσεις αυτόματης συμπλήρωσης.</string>
<string name="autofill_inline_suggestions_summary">Απόπειρα εμφάνισης προτάσεων αυτόματης συμπλήρωσης απευθείας από ένα συμβατό πληκτρολόγιο</string>
<string name="autofill_inline_suggestions_title">Προτάσεις στην ίδια γραμμή</string>
<string name="warning_database_revoked">Πρόσβαση στο αρχείο που ανακλήθηκε από το διαχειριστή αρχείων, κλείστε τη βάση δεδομένων και ανοίξτε το ξανά από τη θέση του.</string>
<string name="warning_database_info_changed_options">Αντικαταστήστε τις εξωτερικές τροποποιήσεις αποθηκεύοντας τη βάση δεδομένων ή φορτώστε την ξανά με τις πιο πρόσφατες αλλαγές.</string>
<string name="warning_database_info_changed">Οι πληροφορίες που περιέχονται στο αρχείο της βάσης δεδομένων σας έχουν τροποποιηθεί εκτός της εφαρμογής.</string>
<string name="menu_reload_database">Επαναφόρτωση βάσης δεδομένων</string>
</resources> </resources>

View File

@@ -19,9 +19,9 @@
Spanish translation by José I. Paños. Updated by David García-Abad (23-09-2013) Spanish translation by José I. Paños. Updated by David García-Abad (23-09-2013)
--><resources> --><resources>
<string name="feedback">Commentario</string> <string name="feedback">Comentarios</string>
<string name="homepage">Página de inicio</string> <string name="homepage">Página de inicio</string>
<string name="about_description">Implementación para Android del gestor de contraseñas KeePass</string> <string name="about_description">Implementación en Android del gestor de contraseñas KeePass</string>
<string name="accept">Aceptar</string> <string name="accept">Aceptar</string>
<string name="add_entry">Añadir entrada</string> <string name="add_entry">Añadir entrada</string>
<string name="add_group">Añadir grupo</string> <string name="add_group">Añadir grupo</string>
@@ -31,30 +31,30 @@
<string name="application">Aplicación</string> <string name="application">Aplicación</string>
<string name="menu_app_settings">Configuración de la aplicación</string> <string name="menu_app_settings">Configuración de la aplicación</string>
<string name="brackets">Paréntesis</string> <string name="brackets">Paréntesis</string>
<string name="file_manager_install_description">Explora ficheros con OpenIntents File Manager</string> <string name="file_manager_install_description">Se requiere un administrador de archivos que acepte la acciones de intención ACTION_CREATE_DOCUMENT y ACTION_OPEN_DOCUMENT para crear, abrir y guardar los archivos de la base de datos.</string>
<string name="clipboard_cleared">Portapapeles limpiado</string> <string name="clipboard_cleared">Portapapeles limpiado</string>
<string name="clipboard_timeout">Portapapeles caducado</string> <string name="clipboard_timeout">Portapapeles caducado</string>
<string name="clipboard_timeout_summary">Duración del almacenamiento en el portapapeles (si lo admite el dispositivo)</string> <string name="clipboard_timeout_summary">Duración del almacenamiento en el portapapeles (si lo admite su dispositivo)</string>
<string name="select_to_copy">Seleccionar para copiar %1$s en el portapapeles</string> <string name="select_to_copy">Seleccionar para copiar %1$s en el portapapeles</string>
<string name="retrieving_db_key">Creando clave de la base de datos…</string> <string name="retrieving_db_key">Recuperando la clave de la base de datos…</string>
<string name="database">Base de datos</string> <string name="database">Base de datos</string>
<string name="decrypting_db">Descifrando el contenido de la base de datos…</string> <string name="decrypting_db">Descifrando el contenido de la base de datos…</string>
<string name="default_checkbox">Utilice como base de datos por defecto</string> <string name="default_checkbox">Utilizar como base de datos por defecto</string>
<string name="digits">Dígitos</string> <string name="digits">Dígitos</string>
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft es \u0020de &lt;strong&gt;código abierto&lt;/strong&gt; y &lt;strong&gt;sin publicidad&lt;/strong&gt;. <string name="html_about_licence">KeePassDX © %1$d Kunzisoft es de &lt;strong&gt;código abierto&lt;/strong&gt; y &lt;strong&gt;sin publicidad&lt;/strong&gt;.
\nSe proporciona tal cual, bajo licencia &lt;strong&gt;GPLv3&lt;/strong&gt;, sin ninguna garantía.</string> \nSe proporciona tal cual, bajo licencia &lt;strong&gt;GPLv3&lt;/strong&gt;, sin ninguna garantía.</string>
<string name="select_database_file">Abrir base de datos existente</string> <string name="select_database_file">Abrir base de datos existente</string>
<string name="entry_accessed">Acceso</string> <string name="entry_accessed">Accedido</string>
<string name="entry_cancel">Cancelar</string> <string name="entry_cancel">Cancelar</string>
<string name="entry_notes">Notas</string> <string name="entry_notes">Notas</string>
<string name="entry_confpassword">Confirmar contraseña</string> <string name="entry_confpassword">Confirmar contraseña</string>
<string name="entry_created">Creación</string> <string name="entry_created">Creado</string>
<string name="entry_expires">Caducidad</string> <string name="entry_expires">Caduca</string>
<string name="entry_keyfile">Archivo de clave</string> <string name="entry_keyfile">Archivo de clave</string>
<string name="entry_modified">Modificación</string> <string name="entry_modified">Modificada</string>
<string name="entry_password">Contraseña</string> <string name="entry_password">Contraseña</string>
<string name="save">Guardar</string> <string name="save">Guardar</string>
<string name="entry_title">Nombre</string> <string name="entry_title">Título</string>
<string name="entry_url">URL</string> <string name="entry_url">URL</string>
<string name="entry_user_name">Nombre de usuario</string> <string name="entry_user_name">Nombre de usuario</string>
<string name="error_arc4">No se admite el cifrador de flujo Arcfour.</string> <string name="error_arc4">No se admite el cifrador de flujo Arcfour.</string>
@@ -64,11 +64,11 @@
<string name="error_invalid_path">Asegúrese de que la ruta sea correcta.</string> <string name="error_invalid_path">Asegúrese de que la ruta sea correcta.</string>
<string name="error_no_name">Proporcione un nombre.</string> <string name="error_no_name">Proporcione un nombre.</string>
<string name="error_nokeyfile">Seleccione un archivo de clave.</string> <string name="error_nokeyfile">Seleccione un archivo de clave.</string>
<string name="error_out_of_memory">No queda memoria para cargar toda la base de datos.</string> <string name="error_out_of_memory">No hay memoria para cargar toda la base de datos.</string>
<string name="error_pass_gen_type">Debe seleccionar al menos un tipo de generación de contraseñas.</string> <string name="error_pass_gen_type">Debe seleccionar al menos un tipo de generación de contraseñas.</string>
<string name="error_pass_match">Las contraseñas no coinciden.</string> <string name="error_pass_match">Las contraseñas no coinciden.</string>
<string name="error_rounds_too_large">Pasadas demasiado grande. Establecido a 2147483648.</string> <string name="error_rounds_too_large">Pasadas demasiado grande. Establecido a 2147483648.</string>
<string name="error_wrong_length">Proporcione un número entero positivo en el campo «Longitud».</string> <string name="error_wrong_length">Introduzca un número entero positivo en el campo \"Longitud\".</string>
<string name="file_browser">Explorador de archivos</string> <string name="file_browser">Explorador de archivos</string>
<string name="generate_password">Generar contraseña</string> <string name="generate_password">Generar contraseña</string>
<string name="hint_conf_pass">Confirmar contraseña</string> <string name="hint_conf_pass">Confirmar contraseña</string>
@@ -78,7 +78,7 @@
<string name="hint_length">Longitud</string> <string name="hint_length">Longitud</string>
<string name="password">Contraseña</string> <string name="password">Contraseña</string>
<string name="hint_pass">Contraseña</string> <string name="hint_pass">Contraseña</string>
<string name="invalid_credentials">Contraseña o archivo de clave no válido.</string> <string name="invalid_credentials">No se pudieron leer las credenciales.</string>
<string name="invalid_db_sig">No se pudo reconocer el formato de la base de datos.</string> <string name="invalid_db_sig">No se pudo reconocer el formato de la base de datos.</string>
<string name="length">Longitud</string> <string name="length">Longitud</string>
<string name="list_size_title">Tamaño de los elementos de la lista</string> <string name="list_size_title">Tamaño de los elementos de la lista</string>
@@ -103,7 +103,7 @@
<string name="minus">Menos</string> <string name="minus">Menos</string>
<string name="never">Nunca</string> <string name="never">Nunca</string>
<string name="no_results">Sin resultado de búsqueda</string> <string name="no_results">Sin resultado de búsqueda</string>
<string name="no_url_handler">Instale un navegador web para abrir este URL.</string> <string name="no_url_handler">Instale un navegador web para abrir esta URL.</string>
<string name="omit_backup_search_title">No buscar en las entradas de respaldo</string> <string name="omit_backup_search_title">No buscar en las entradas de respaldo</string>
<string name="omit_backup_search_summary">Omite los grupos «Respaldo» y «Papelera de reciclaje» de los resultados de búsqueda</string> <string name="omit_backup_search_summary">Omite los grupos «Respaldo» y «Papelera de reciclaje» de los resultados de búsqueda</string>
<string name="progress_create">Creando nueva base de datos…</string> <string name="progress_create">Creando nueva base de datos…</string>
@@ -124,7 +124,9 @@
<string name="unsupported_db_version">No se admite esta versión de la base de datos.</string> <string name="unsupported_db_version">No se admite esta versión de la base de datos.</string>
<string name="uppercase">Mayúsculas</string> <string name="uppercase">Mayúsculas</string>
<string name="version_label">Versión %1$s</string> <string name="version_label">Versión %1$s</string>
<string name="education_unlock_summary">Introduzca una contraseña y/o un archivo de clave para desbloquear su base de datos.</string> <string name="education_unlock_summary">Introduzca la contraseña y/o el archivo clave para desbloquear su base de datos.
\n
\nHaga una copia de seguridad de su archivo de base de datos en un lugar seguro después de cada cambio.</string>
<string-array name="timeout_options"> <string-array name="timeout_options">
<item>5 segundos</item> <item>5 segundos</item>
<item>10 segundos</item> <item>10 segundos</item>
@@ -146,28 +148,28 @@
<string name="extended_ASCII">ASCII extendido</string> <string name="extended_ASCII">ASCII extendido</string>
<string name="allow">Permitir</string> <string name="allow">Permitir</string>
<string name="clipboard_error_title">Error del portapapeles</string> <string name="clipboard_error_title">Error del portapapeles</string>
<string name="clipboard_error">Algunos dispositivos Android tienen un error en la implementación del portapapeles que provoca fallos al copiar desde las aplicaciones.</string> <string name="clipboard_error">Algunos dispositivos no permiten que las aplicaciones usen el portapapeles.</string>
<string name="clipboard_error_clear">Falló la limpieza del portapapeles</string> <string name="clipboard_error_clear">No se pudo limpiar el portapapeles</string>
<string name="error_string_key">Cada cadena debe tener un nombre de campo.</string> <string name="error_string_key">Cada cadena debe tener un nombre de campo.</string>
<string name="error_autofill_enable_service">El servicio de autocompletado no se ha podido habilitar.</string> <string name="error_autofill_enable_service">El servicio de autocompletado no se ha podido habilitar.</string>
<string name="field_name">Nombre del campo</string> <string name="field_name">Nombre del campo</string>
<string name="field_value">Valor del campo</string> <string name="field_value">Valor del campo</string>
<string name="file_not_found_content">No se pudo encontrar el archivo. Intente volver a abrirlo en el explorador de archivos.</string> <string name="file_not_found_content">No se pudo encontrar el archivo. Intente volver a abrirlo en el explorador de archivos.</string>
<string name="invalid_algorithm">El algoritmo es incorrecto.</string> <string name="invalid_algorithm">Algoritmo incorrecto.</string>
<string name="keyfile_is_empty">El archivo de clave está vacío.</string> <string name="keyfile_is_empty">El archivo de clave está vacío.</string>
<string name="copy_field">Copia de %1$s</string> <string name="copy_field">Copia de %1$s</string>
<string name="menu_form_filling_settings">Llenado de formulario</string> <string name="menu_form_filling_settings">Llenado de formulario</string>
<string name="protection">Protección</string> <string name="protection">Protección</string>
<string name="read_only">Protegida contra escritura</string> <string name="read_only">Protegida contra escritura</string>
<string name="read_only_warning">KeePassDX necesita permiso de escritura para modificar la base de datos.</string> <string name="read_only_warning">Dependiendo del administrador de archivos, puede que KeePassDX no permita escribir en su almacenamiento.</string>
<string name="encryption_explanation">Algoritmo de cifrado utilizado en todos los datos.</string> <string name="encryption_explanation">Algoritmo de cifrado utilizado para todos los datos.</string>
<string name="kdf_explanation">Para generar la clave del algoritmo de cifrado, la clave maestra se transforma mediante una función de derivación de claves con una sal aleatoria.</string> <string name="kdf_explanation">Para generar la clave del algoritmo de cifrado, la clave maestra se transforma mediante una función de derivación de claves con una sal aleatoria.</string>
<string name="memory_usage">Uso de memoria</string> <string name="memory_usage">Uso de memoria</string>
<string name="memory_usage_explanation">Cantidad de memoria (en bytes) que usará la función de derivación de clave.</string> <string name="memory_usage_explanation">Cantidad de memoria que usará la función de derivación de clave.</string>
<string name="parallelism">Paralelismo</string> <string name="parallelism">Paralelismo</string>
<string name="parallelism_explanation">Grado de paralelismo (p. ej. número de hilos) usados por la función de derivación de clave.</string> <string name="parallelism_explanation">Grado de paralelismo (p. ej. número de hilos) usados por la función de derivación de clave.</string>
<string name="sort_menu">Ordenar</string> <string name="sort_menu">Ordenar</string>
<string name="sort_ascending">Inferiores primero ↓</string> <string name="sort_ascending">Más bajo primero ↓</string>
<string name="sort_groups_before">Agrupar antes</string> <string name="sort_groups_before">Agrupar antes</string>
<string name="sort_recycle_bin_bottom">Papelera debajo</string> <string name="sort_recycle_bin_bottom">Papelera debajo</string>
<string name="sort_title">Título</string> <string name="sort_title">Título</string>
@@ -177,8 +179,8 @@
<string name="sort_last_access_time">Acceso</string> <string name="sort_last_access_time">Acceso</string>
<string name="search_results">Resultados de búsqueda</string> <string name="search_results">Resultados de búsqueda</string>
<string name="warning">Atención</string> <string name="warning">Atención</string>
<string name="entry_not_found">Datos de entrada no encontrados.</string> <string name="entry_not_found">No se pudieron encontrar los datos de entrada.</string>
<string name="warning_password_encoding">Evite emplear en la base de datos caracteres que no pertenezcan al formato de codificación del texto (los caracteres no reconocidos se convierten a la misma letra).</string> <string name="warning_password_encoding">Evite los caracteres de la contraseña fuera del formato de codificación de texto en el archivo de la base de datos (los caracteres no reconocidos se convierten a la misma letra).</string>
<string name="warning_empty_password">¿Continuar sin la protección de desbloqueo de contraseña\?</string> <string name="warning_empty_password">¿Continuar sin la protección de desbloqueo de contraseña\?</string>
<string name="warning_no_encryption_key">¿Continuar sin clave de cifrado\?</string> <string name="warning_no_encryption_key">¿Continuar sin clave de cifrado\?</string>
<string name="encrypted_value_stored">Contraseña cifrada almacenada</string> <string name="encrypted_value_stored">Contraseña cifrada almacenada</string>
@@ -197,25 +199,25 @@
<string name="list_password_generator_options_summary">Establecer los caracteres permitidos del generador de contraseñas</string> <string name="list_password_generator_options_summary">Establecer los caracteres permitidos del generador de contraseñas</string>
<string name="clipboard">Portapapeles</string> <string name="clipboard">Portapapeles</string>
<string name="clipboard_notifications_title">Notificaciones del portapapeles</string> <string name="clipboard_notifications_title">Notificaciones del portapapeles</string>
<string name="clipboard_notifications_summary">Mostrar notificaciones del portapapeles para copiar campos al examinar una entrada</string> <string name="clipboard_notifications_summary">Mostrar las notificaciones del portapapeles para copiar campos al examinar una entrada</string>
<string name="lock">Bloquear</string> <string name="lock">Bloquear</string>
<string name="lock_database_screen_off_title">Bloqueo de pantalla</string> <string name="lock_database_screen_off_title">Bloqueo de pantalla</string>
<string name="lock_database_screen_off_summary">Bloquear la base de datos cuando la pantalla esté apagada</string> <string name="lock_database_screen_off_summary">Bloquear la base de datos cuando la pantalla esté apagada</string>
<string name="advanced_unlock">Desbloqueo avanzado</string> <string name="advanced_unlock">Desbloqueo avanzado</string>
<string name="biometric_unlock_enable_title">Desbloqueo biométrico</string> <string name="biometric_unlock_enable_title">Desbloqueo biométrico</string>
<string name="biometric_unlock_enable_summary">Le permite escanear su huella dactilar u otro dato biométrico para abrir la base de datos</string> <string name="biometric_unlock_enable_summary">Le permite escanear sus datos biométricos para abrir la base de datos</string>
<string name="biometric_delete_all_key_title">Eliminar claves de cifrado</string> <string name="biometric_delete_all_key_title">Eliminar claves de cifrado</string>
<string name="biometric_delete_all_key_summary">Eliminar todas las claves de cifrado relacionadas con el reconocimiento biométrico</string> <string name="biometric_delete_all_key_summary">Eliminar todas las claves de cifrado relacionadas con el reconocimiento de desbloqueo avanzado</string>
<string name="unavailable_feature_text">No se pudo iniciar esta funcionalidad.</string> <string name="unavailable_feature_text">No se pudo iniciar esta funcionalidad.</string>
<string name="unavailable_feature_version">La versión de Android del dispositivo es %1$s, pero es necesaria la %2$s o posterior.</string> <string name="unavailable_feature_version">El dispositivo funciona con Android %1$s, pero necesita %2$s o posterior.</string>
<string name="unavailable_feature_hardware">No se encontró el hardware correspondiente.</string> <string name="unavailable_feature_hardware">No se pudo encontrar el hardware correspondiente.</string>
<string name="file_name">Nombre del archivo</string> <string name="file_name">Nombre del archivo</string>
<string name="path">Ruta</string> <string name="path">Ruta</string>
<string name="assign_master_key">Asignar una clave maestra</string> <string name="assign_master_key">Asignar una clave maestra</string>
<string name="create_keepass_file">Crear base de datos nueva</string> <string name="create_keepass_file">Crear una base de datos nueva</string>
<string name="recycle_bin_title">Uso de la papelera de reciclaje</string> <string name="recycle_bin_title">Uso de la papelera de reciclaje</string>
<string name="recycle_bin_summary">Mueve los grupos y las entradas al grupo \"Papelera de reciclaje\" antes de eliminarlos</string> <string name="recycle_bin_summary">Mueve los grupos y las entradas al grupo \"Papelera de reciclaje\" antes de eliminarlos</string>
<string name="monospace_font_fields_enable_title">Fuente de los campos</string> <string name="monospace_font_fields_enable_title">Tipografía del campo</string>
<string name="monospace_font_fields_enable_summary">Cambiar la fuente de los campos para una mejor visibilidad del carácter</string> <string name="monospace_font_fields_enable_summary">Cambiar la fuente de los campos para una mejor visibilidad del carácter</string>
<string name="allow_copy_password_title">Portapapeles de confianza</string> <string name="allow_copy_password_title">Portapapeles de confianza</string>
<string name="allow_copy_password_summary">Permitir copiar la contraseña de entrada y los campos protegidos al portapapeles</string> <string name="allow_copy_password_summary">Permitir copiar la contraseña de entrada y los campos protegidos al portapapeles</string>
@@ -223,11 +225,11 @@
<string name="database_description_title">Descripción de la base de datos</string> <string name="database_description_title">Descripción de la base de datos</string>
<string name="database_version_title">Versión de la base de datos</string> <string name="database_version_title">Versión de la base de datos</string>
<string name="text_appearance">Texto</string> <string name="text_appearance">Texto</string>
<string name="application_appearance">Aplicación</string> <string name="application_appearance">Interfaz</string>
<string name="other">Otro</string> <string name="other">Otro</string>
<string name="keyboard">Teclado</string> <string name="keyboard">Teclado</string>
<string name="magic_keyboard_title">Teclado mágico</string> <string name="magic_keyboard_title">Teclado mágico</string>
<string name="magic_keyboard_explanation_summary">Active un teclado personalizado que llene sus contraseñas y todos los campos de identidad fácilmente.</string> <string name="magic_keyboard_explanation_summary">Active un teclado personalizado que llene sus contraseñas y todos los campos de identidad fácilmente</string>
<string name="reset_education_screens_title">Restablecer sugerencias didácticas</string> <string name="reset_education_screens_title">Restablecer sugerencias didácticas</string>
<string name="reset_education_screens_summary">Mostrar de nuevo toda la información didáctica</string> <string name="reset_education_screens_summary">Mostrar de nuevo toda la información didáctica</string>
<string name="reset_education_screens_text">Se restablecieron las sugerencias didácticas</string> <string name="reset_education_screens_text">Se restablecieron las sugerencias didácticas</string>
@@ -259,28 +261,28 @@
<string name="education_donation_title">Participar</string> <string name="education_donation_title">Participar</string>
<string name="education_donation_summary">Participe para aumentar la estabilidad, la seguridad y agregar más funciones.</string> <string name="education_donation_summary">Participe para aumentar la estabilidad, la seguridad y agregar más funciones.</string>
<string name="html_text_ad_free">A diferencia de muchas aplicaciones de administración de contraseñas, esta es &lt;strong&gt;sin publicidad&lt;/strong&gt;, &lt;strong&gt;software libre con copyleft&lt;/strong&gt; y no recopila datos personales en sus servidores, sin importar la versión que use.</string> <string name="html_text_ad_free">A diferencia de muchas aplicaciones de administración de contraseñas, esta es &lt;strong&gt;sin publicidad&lt;/strong&gt;, &lt;strong&gt;software libre con copyleft&lt;/strong&gt; y no recopila datos personales en sus servidores, sin importar la versión que use.</string>
<string name="html_text_buy_pro">Al comprar la versión pro, tendrá acceso a &lt;strong&gt;la característica visual &lt;/strong&gt;y usted ayudará especialmente a &lt;strong&gt;la realización de proyectos comunitarios.&lt;/strong&gt;</string> <string name="html_text_buy_pro">Al comprar la versión pro, tendrá acceso al &lt;strong&gt;estilo visual &lt;/strong&gt;y ayudará especialmente a &lt;strong&gt;la realización de proyectos comunitarios.&lt;/strong&gt;</string>
<string name="html_text_feature_generosity">Esta &lt;strong&gt;característica visual &lt;/strong&gt;está disponible gracias a tu generosidad.</string> <string name="html_text_feature_generosity">Este &lt;strong&gt;estilo visual &lt;/strong&gt;está disponible gracias a tu generosidad.</string>
<string name="html_text_donation">Para mantener nuestra libertad y estar siempre vigente, contamos con tu &lt;strong&gt;contribución.&lt;/strong&gt;</string> <string name="html_text_donation">Para mantener nuestra libertad y estar siempre vigente, contamos con tu &lt;strong&gt;contribución.&lt;/strong&gt;</string>
<string name="html_text_dev_feature">Esta función está &lt;strong&gt;en desarrollo&lt;/strong&gt; y requiere de tu &lt;strong&gt;contribución&lt;/strong&gt; para estar disponible dentro de poco.</string> <string name="html_text_dev_feature">Esta función está &lt;strong&gt;en desarrollo&lt;/strong&gt; y requiere de tu &lt;strong&gt;contribución&lt;/strong&gt; para estar disponible dentro de poco.</string>
<string name="html_text_dev_feature_buy_pro">Al comprar la versión &lt;strong&gt;pro&lt;/strong&gt;,</string> <string name="html_text_dev_feature_buy_pro">Al comprar la versión &lt;strong&gt;pro&lt;/strong&gt;,</string>
<string name="html_text_dev_feature_contibute">Al &lt;strong&gt;contribuir&lt;/strong&gt;,</string> <string name="html_text_dev_feature_contibute">Al &lt;strong&gt;contribuir&lt;/strong&gt;,</string>
<string name="html_text_dev_feature_encourage">usted alienta a los desarrolladores a crear &lt;strong&gt;nuevas funciones&lt;/strong&gt; y a &lt;strong&gt;errores de configuración&lt;/strong&gt; de acuerdo con tus comentarios.</string> <string name="html_text_dev_feature_encourage">anima a los desarrolladores a crear &lt;strong&gt;nuevas funciones&lt;/strong&gt; y a &lt;strong&gt;corregir errores&lt;/strong&gt; de acuerdo con sus comentarios.</string>
<string name="html_text_dev_feature_thanks">Muchas gracias por tu contribución.</string> <string name="html_text_dev_feature_thanks">Muchas gracias por tu contribución.</string>
<string name="html_text_dev_feature_work_hard">Estamos trabajando duro para lanzar esta característica rápidamente.</string> <string name="html_text_dev_feature_work_hard">Estamos trabajando duro para lanzar esta característica rápidamente.</string>
<string name="html_text_dev_feature_upgrade">No olvide mantener su aplicación actualizada.</string> <string name="html_text_dev_feature_upgrade">Recuerde mantener su aplicación actualizada instalando nuevas versiones.</string>
<string name="download">Descargar</string> <string name="download">Descargar</string>
<string name="contribute">Contribuir</string> <string name="contribute">Contribuir</string>
<string name="encryption_chacha20">ChaCha20</string> <string name="encryption_chacha20">ChaCha20</string>
<string name="kdf_AES">AES-KDF</string> <string name="kdf_AES">AES</string>
<string name="style_choose_title">Tema de aplicación</string> <string name="style_choose_title">Tema de la aplicación</string>
<string name="style_choose_summary">Tema utilizado en la aplicación</string> <string name="style_choose_summary">Tema utilizado en la aplicación</string>
<string name="icon_pack_choose_title">Seleccione un paquete de iconos</string> <string name="icon_pack_choose_title">Seleccione un paquete de iconos</string>
<string name="icon_pack_choose_summary">Cambiar el paquete de iconos en la aplicación</string> <string name="icon_pack_choose_summary">Cambiar el paquete de iconos en la aplicación</string>
<string name="edit_entry">Editar entrada</string> <string name="edit_entry">Editar entrada</string>
<string name="error_load_database">No se pudo cargar la base de datos.</string> <string name="error_load_database">No se pudo cargar la base de datos.</string>
<string name="error_load_database_KDF_memory">No se pudo cargar la clave. Intente disminuir el uso de memoria de KDF.</string> <string name="error_load_database_KDF_memory">No se pudo cargar la clave. Intente disminuir el uso de memoria de KDF.</string>
<string name="error_move_folder_in_itself">No se puede mover un grupo dentro de sí mismo.</string> <string name="error_move_folder_in_itself">No puede mover un grupo dentro de sí mismo.</string>
<string name="list_entries_show_username_title">Enseña nombres de usuario</string> <string name="list_entries_show_username_title">Enseña nombres de usuario</string>
<string name="list_entries_show_username_summary">Enseña nombres de usuador en las listras de entradas</string> <string name="list_entries_show_username_summary">Enseña nombres de usuador en las listras de entradas</string>
<string name="menu_copy">Copiar</string> <string name="menu_copy">Copiar</string>
@@ -291,13 +293,13 @@
<string name="menu_open_file_read_and_write">Modificable</string> <string name="menu_open_file_read_and_write">Modificable</string>
<string name="build_label">Compilación %1$s</string> <string name="build_label">Compilación %1$s</string>
<string name="clipboard_warning">Si la eliminación del cortapapeles falla, elimine su historial manualmente.</string> <string name="clipboard_warning">Si la eliminación del cortapapeles falla, elimine su historial manualmente.</string>
<string name="allow_copy_password_warning">ATENCIÓN: todas las aplicaciones comparten el portapapeles. Si copia datos confidenciales, otros programas podrían recuperarlos.</string> <string name="allow_copy_password_warning">Advertencia: El portapapeles es compartido por todas las aplicaciones. Si se copian datos sensibles, otros programas pueden recuperarlos.</string>
<string name="allow_no_password_title">No permitir claves maestras</string> <string name="allow_no_password_title">No permitir claves maestras</string>
<string name="allow_no_password_summary">Activar el botón «Abrir» si no se selecciona ninguna credencial</string> <string name="allow_no_password_summary">Permite pulsar el botón \"Abrir\" si no se seleccionan credenciales</string>
<string name="enable_education_screens_title">Pantallas didácticas</string> <string name="enable_education_screens_title">Sugerencias educativas</string>
<string name="enable_education_screens_summary">Resaltar los elementos para conocer cómo funciona la aplicación</string> <string name="enable_education_screens_summary">Destacar elementos para aprender cómo funciona la aplicación</string>
<string name="enable_read_only_title">Protegida contra escritura</string> <string name="enable_read_only_title">Protegida contra escritura</string>
<string name="enable_read_only_summary">Abrir su base de datos protegida contra escritura de manera predeterminada</string> <string name="enable_read_only_summary">Abrir la base de datos de solo lectura por defecto</string>
<string name="education_read_only_title">Proteja la base de datos contra escritura</string> <string name="education_read_only_title">Proteja la base de datos contra escritura</string>
<string name="keyboard_name">Teclado mágico</string> <string name="keyboard_name">Teclado mágico</string>
<string name="keyboard_label">Teclado mágico (KeePassDX)</string> <string name="keyboard_label">Teclado mágico (KeePassDX)</string>
@@ -315,56 +317,56 @@
<string name="keyboard_appearance_category">Apariencia</string> <string name="keyboard_appearance_category">Apariencia</string>
<string name="keyboard_theme_title">Tema del teclado</string> <string name="keyboard_theme_title">Tema del teclado</string>
<string name="keyboard_keys_category">Teclas</string> <string name="keyboard_keys_category">Teclas</string>
<string name="keyboard_key_vibrate_title">Vibrar al pulsar</string> <string name="keyboard_key_vibrate_title">Vibrar al pulsar tecla</string>
<string name="keyboard_key_sound_title">Emitir sonido al pulsar</string> <string name="keyboard_key_sound_title">Sonar al pulsar tecla</string>
<string name="selection_mode">Modo de selección</string> <string name="selection_mode">Modo de selección</string>
<string name="do_not_kill_app">No cierre la aplicación…</string> <string name="do_not_kill_app">No cierre la aplicación…</string>
<string name="lock_database_back_root_summary">Bloquear la base de datos cuando se pulsa el botón Atrás en la pantalla inicial</string> <string name="lock_database_back_root_summary">Bloquear la base de datos cuando el usuario pulse el botón atrás en la pantalla inicial</string>
<string name="clear_clipboard_notification_title">Vaciar al cerrar</string> <string name="clear_clipboard_notification_title">Vaciar al cerrar</string>
<string name="clear_clipboard_notification_summary">Bloquear la base de datos cuando la duración del portapapeles expire o la notificación sea cerrada</string> <string name="clear_clipboard_notification_summary">Bloquear la base de datos cuando expire la duración del portapapeles o cuando se cierre la notificación después de empezar a utilizarla</string>
<string name="recycle_bin">Papelera de reciclaje</string> <string name="recycle_bin">Papelera de reciclaje</string>
<string name="keyboard_selection_entry_title">Selección de entrada</string> <string name="keyboard_selection_entry_title">Selección de entrada</string>
<string name="keyboard_selection_entry_summary">Mostrar campos de entrada en el Teclado mágico al ver una entrada</string> <string name="keyboard_selection_entry_summary">Mostrar campos de entrada en el Teclado mágico al ver una entrada</string>
<string name="delete_entered_password_title">Eliminar contraseña</string> <string name="delete_entered_password_title">Eliminar contraseña</string>
<string name="delete_entered_password_summary">Elimina la contraseña proporcionada tras un intento de conexión</string> <string name="delete_entered_password_summary">Elimina la contraseña introducida tras un intento de conexión a una bse de datos</string>
<string name="content_description_open_file">Abrir archivo</string> <string name="content_description_open_file">Abrir archivo</string>
<string name="content_description_node_children">Elementos secundarios del nodo</string> <string name="content_description_node_children">Nodos hijo</string>
<string name="content_description_add_node">Añadir nodo</string> <string name="content_description_add_node">Añadir nodo</string>
<string name="content_description_add_entry">Añadir entrada</string> <string name="content_description_add_entry">Añadir entrada</string>
<string name="content_description_add_group">Añadir grupo</string> <string name="content_description_add_group">Añadir grupo</string>
<string name="content_description_file_information">Información de archivo</string> <string name="content_description_file_information">Información del archivo</string>
<string name="content_description_password_checkbox">Casilla de contraseña</string> <string name="content_description_password_checkbox">Casilla de verificación de la contraseña</string>
<string name="content_description_keyfile_checkbox">Casilla de archivo de clave</string> <string name="content_description_keyfile_checkbox">Casilla de verificación del archivo de clave</string>
<string name="content_description_entry_icon">Icono de entrada</string> <string name="content_description_entry_icon">Icono de entrada</string>
<string name="entry_password_generator">Generador de contraseñas</string> <string name="entry_password_generator">Generador de contraseñas</string>
<string name="content_description_password_length">Longitud de contraseña</string> <string name="content_description_password_length">Longitud de contraseña</string>
<string name="entry_add_field">Añadir campo</string> <string name="entry_add_field">Añadir campo</string>
<string name="content_description_remove_field">Eliminar campo</string> <string name="content_description_remove_field">Eliminar el campo</string>
<string name="entry_UUID">UUID</string> <string name="entry_UUID">UUID</string>
<string name="error_move_entry_here">No se puede mover una entrada aquí.</string> <string name="error_move_entry_here">No puede mover una entrada aquí.</string>
<string name="error_copy_entry_here">No se puede copiar una entrada aquí.</string> <string name="error_copy_entry_here">No puede copiar una entrada aquí.</string>
<string name="list_groups_show_number_entries_title">Mostrar cantidad de entradas</string> <string name="list_groups_show_number_entries_title">Mostrar el número de entradas</string>
<string name="list_groups_show_number_entries_summary">Mostrar la cantidad de entradas en un grupo</string> <string name="list_groups_show_number_entries_summary">Mostrar el número de entradas en un grupo</string>
<string name="content_description_background">Fondo</string> <string name="content_description_background">Fondo</string>
<string name="content_description_update_from_list">Actualizar</string> <string name="content_description_update_from_list">Actualizar</string>
<string name="content_description_keyboard_close_fields">Cerrar campos</string> <string name="content_description_keyboard_close_fields">Cerrar campos</string>
<string name="error_create_database_file">No se puede crear la base de datos con esta contraseña y este archivo de clave.</string> <string name="error_create_database_file">No se puede crear la base de datos con esta contraseña y este archivo de clave.</string>
<string name="menu_advanced_unlock_settings">Desbloqueo avanzado</string> <string name="menu_advanced_unlock_settings">Desbloqueo avanzado</string>
<string name="biometric">Biometría</string> <string name="biometric">Biometría</string>
<string name="biometric_auto_open_prompt_title">Abrir petición de datos biométricos automáticamente</string> <string name="biometric_auto_open_prompt_title">Abrir petición automáticamente</string>
<string name="biometric_auto_open_prompt_summary">Abrir automáticamente la petición de datos biométricos cuando se define una clave biométrica para una base de datos</string> <string name="biometric_auto_open_prompt_summary">Solicitar automáticamente el desbloqueo avanzado si la base de datos está configurada para utilizarlo</string>
<string name="enable">Activar</string> <string name="enable">Activar</string>
<string name="disable">Desactivar</string> <string name="disable">Desactivar</string>
<string name="education_read_only_summary">Cambiar el modo de apertura de la sesión. <string name="education_read_only_summary">Cambiar el modo de apertura de la sesión.
\n \n
\n\"Protegido contra escritura\" evita cambios no deseados en la base de datos. \n\"Protegido contra escritura\" evita cambios no deseados en la base de datos.
\n\"Modificable\" le permite agregar, eliminar o modificar todos los elementos.</string> \n\"Modificable\" le permite agregar, eliminar o modificar todos los elementos como desee.</string>
<string name="lock_database_back_root_title">Presione hacia atrás en la raíz para bloquear</string> <string name="lock_database_back_root_title">Presione \'Atrás\' para bloquear</string>
<string name="content_description_repeat_toggle_password_visibility">Repetir la visibilidad de la contraseña</string> <string name="content_description_repeat_toggle_password_visibility">Repetir la visibilidad de la contraseña</string>
<string name="master_key">Llave maestra</string> <string name="master_key">Clave maestra</string>
<string name="security">Seguridad</string> <string name="security">Seguridad</string>
<string name="entry_history">Historial</string> <string name="entry_history">Historial</string>
<string name="entry_setup_otp">Configuración de contraseña de un solo uso</string> <string name="entry_setup_otp">Establecer una contraseña de un solo uso</string>
<string name="otp_type">Tipo de contraseña de un solo uso</string> <string name="otp_type">Tipo de contraseña de un solo uso</string>
<string name="otp_secret">Secreto</string> <string name="otp_secret">Secreto</string>
<string name="otp_period">Período (segundos)</string> <string name="otp_period">Período (segundos)</string>
@@ -377,16 +379,16 @@
<string name="error_otp_secret_key">Clave secreta debe estar en formato Base32.</string> <string name="error_otp_secret_key">Clave secreta debe estar en formato Base32.</string>
<string name="error_otp_counter">Contador debe estar entre %1$d y %2$d.</string> <string name="error_otp_counter">Contador debe estar entre %1$d y %2$d.</string>
<string name="error_save_database">No se puede guardar la base de datos.</string> <string name="error_save_database">No se puede guardar la base de datos.</string>
<string name="error_string_type">Este texto no encaja con la información requerida.</string> <string name="error_string_type">Este texto no coincide con el elemento requerido.</string>
<string name="error_create_database">No fue posible crear el archivo de base de datos.</string> <string name="error_create_database">No fue posible crear el archivo de base de datos.</string>
<string name="html_about_contribution">Parar lograr &lt;strong&gt;mantener nuestra libertad&lt;/strong&gt;, &lt;strong&gt;corregir errores&lt;/strong&gt;, &lt;strong&gt;agregar características&lt;/strong&gt; y &lt;strong&gt;siempre estar activos&lt;/strong&gt;, contamos con tu &lt;strong&gt;contribución&lt;/strong&gt;.</string> <string name="html_about_contribution">Parar lograr &lt;strong&gt;mantener nuestra libertad&lt;/strong&gt;, &lt;strong&gt;corregir errores&lt;/strong&gt;, &lt;strong&gt;agregar características&lt;/strong&gt; y &lt;strong&gt;estar siempre activos&lt;/strong&gt;, contamos con tu &lt;strong&gt;contribución&lt;/strong&gt;.</string>
<string name="content_description_add_item">Añadir elemento</string> <string name="content_description_add_item">Añadir elemento</string>
<string name="download_complete">Descarga completa!</string> <string name="download_complete">¡Completado!</string>
<string name="download_finalization">Finalizando…</string> <string name="download_finalization">Finalizando…</string>
<string name="download_progression">En progreso: %1$d%%</string> <string name="download_progression">En progreso: %1$d%%</string>
<string name="download_initialization">Inicializando…</string> <string name="download_initialization">Inicializando…</string>
<string name="download_attachment">Descargar %1$s</string> <string name="download_attachment">Descargar %1$s</string>
<string name="enable_auto_save_database_summary">Guardar la base de datos tras acciones importantes (en modo \"Modificable\")</string> <string name="enable_auto_save_database_summary">Guardar la base de datos después de cada acción importante (en modo \"Modificable\")</string>
<string name="enable_auto_save_database_title">Guardar base de datos automáticamente</string> <string name="enable_auto_save_database_title">Guardar base de datos automáticamente</string>
<string name="autofill_block">Bloquear autocompletado</string> <string name="autofill_block">Bloquear autocompletado</string>
<string name="keyboard_change">Cambiar teclado</string> <string name="keyboard_change">Cambiar teclado</string>
@@ -395,12 +397,12 @@
<string name="compression_none">Ninguna</string> <string name="compression_none">Ninguna</string>
<string name="compression">Compresión</string> <string name="compression">Compresión</string>
<string name="database_default_username_title">Nombre de usuario predeterminado</string> <string name="database_default_username_title">Nombre de usuario predeterminado</string>
<string name="settings_database_force_changing_master_key_next_time_summary">Requerir cambiar la contraseña maestra la próxima vez (una sola vez)</string> <string name="settings_database_force_changing_master_key_next_time_summary">Requerir cambiar la contraseña maestra la próxima vez (una vez)</string>
<string name="settings_database_force_changing_master_key_next_time_title">Forzar renovación la próxima vez</string> <string name="settings_database_force_changing_master_key_next_time_title">Forzar renovación la próxima vez</string>
<string name="settings_database_force_changing_master_key_summary">Requerir un cambio de contraseña maestra (días)</string> <string name="settings_database_force_changing_master_key_summary">Requerir un cambio de la contraseña maestra (días)</string>
<string name="settings_database_force_changing_master_key_title">Forzar renovación</string> <string name="settings_database_force_changing_master_key_title">Forzar renovación</string>
<string name="max_history_size_title">Tamaño máximo</string> <string name="max_history_size_title">Tamaño máximo</string>
<string name="advanced_unlock_explanation_summary">Usar desbloqueo avanzado para abrir una base de datos de manera más fácil</string> <string name="advanced_unlock_explanation_summary">Usar el desbloqueo avanzado para abrir una base de datos más fácilmente</string>
<string name="lock_database_show_button_summary">Muestra el botón de bloqueo en la interfaz</string> <string name="lock_database_show_button_summary">Muestra el botón de bloqueo en la interfaz</string>
<string name="lock_database_show_button_title">Mostrar botón de bloqueo</string> <string name="lock_database_show_button_title">Mostrar botón de bloqueo</string>
<string name="autofill_preference_title">Configuración de autocompletado</string> <string name="autofill_preference_title">Configuración de autocompletado</string>
@@ -409,8 +411,8 @@
<string name="warning_database_read_only">Otorga acceso de escritura para guardar cambios en la base de datos</string> <string name="warning_database_read_only">Otorga acceso de escritura para guardar cambios en la base de datos</string>
<string name="show_recent_files_summary">Mostrar ubicaciones de bases de datos recientes</string> <string name="show_recent_files_summary">Mostrar ubicaciones de bases de datos recientes</string>
<string name="show_recent_files_title">Mostrar archivos recientes</string> <string name="show_recent_files_title">Mostrar archivos recientes</string>
<string name="remember_database_locations_summary">Recordar la ubicación de las bases de datos</string> <string name="remember_database_locations_summary">Lleva un registro de los lugares donde se almacenan las bases de datos</string>
<string name="remember_database_locations_title">Guardar ubicaciones de bases de datos</string> <string name="remember_database_locations_title">Recordar las ubicaciones de las bases de datos</string>
<string name="contains_duplicate_uuid">La base de datos contiene UUIDs duplicados.</string> <string name="contains_duplicate_uuid">La base de datos contiene UUIDs duplicados.</string>
<string name="menu_restore_entry_history">Restaurar historial</string> <string name="menu_restore_entry_history">Restaurar historial</string>
<string name="menu_empty_recycle_bin">Vaciar papelera de reciclaje</string> <string name="menu_empty_recycle_bin">Vaciar papelera de reciclaje</string>
@@ -419,12 +421,12 @@
<string name="invalid_db_same_uuid">%1$s con la misma UUID %2$s ya existe.</string> <string name="invalid_db_same_uuid">%1$s con la misma UUID %2$s ya existe.</string>
<string name="error_label_exists">Esta etiqueta ya existe.</string> <string name="error_label_exists">Esta etiqueta ya existe.</string>
<string name="discard">Descartar</string> <string name="discard">Descartar</string>
<string name="discard_changes">¿Descartar cambios\?</string> <string name="discard_changes">¿Descartar los cambios\?</string>
<string name="validate">Validar</string> <string name="validate">Validar</string>
<string name="contribution">Contribución</string> <string name="contribution">Contribución</string>
<string name="contact">Contactar</string> <string name="contact">Contacto</string>
<string name="error_otp_period">Período debe estar entre %1$d y %2$d segundos.</string> <string name="error_otp_period">El período debe estar entre %1$d y %2$d segundos.</string>
<string name="error_copy_group_here">No se puede copiar un grupo aquí.</string> <string name="error_copy_group_here">No puede copiar un grupo aquí.</string>
<string name="database_data_compression_summary">La compresión de datos reduce el tamaño de la base de datos</string> <string name="database_data_compression_summary">La compresión de datos reduce el tamaño de la base de datos</string>
<string name="database_data_compression_title">Compresión de datos</string> <string name="database_data_compression_title">Compresión de datos</string>
<string name="warning_empty_keyfile">No se recomienda agregar un archivo de claves vacío.</string> <string name="warning_empty_keyfile">No se recomienda agregar un archivo de claves vacío.</string>
@@ -433,19 +435,19 @@
<string name="warning_replace_file">Al cargar este archivo reemplazará el existente.</string> <string name="warning_replace_file">Al cargar este archivo reemplazará el existente.</string>
<string name="warning_permanently_delete_nodes">¿Borrar los nodos seleccionados de forma permanente\?</string> <string name="warning_permanently_delete_nodes">¿Borrar los nodos seleccionados de forma permanente\?</string>
<string name="warning_database_link_revoked">El acceso al archivo fue revocado por el administrador de archivos</string> <string name="warning_database_link_revoked">El acceso al archivo fue revocado por el administrador de archivos</string>
<string name="command_execution">Ejecutando el comando </string> <string name="command_execution">Ejecutando el comando…</string>
<string name="hide_broken_locations_summary">Ocultar enlaces rotos en la lista de bases de datos recientes</string> <string name="hide_broken_locations_summary">Ocultar enlaces rotos en la lista de bases de datos recientes</string>
<string name="hide_broken_locations_title">Ocultar enlaces de bases de datos rotos</string> <string name="hide_broken_locations_title">Ocultar enlaces de bases de datos rotos</string>
<string name="remember_keyfile_locations_summary">Realiza un seguimiento de dónde se almacenan los archivos de claves</string> <string name="remember_keyfile_locations_summary">Realiza un seguimiento de dónde se almacenan los archivos de claves</string>
<string name="remember_keyfile_locations_title">Recuerdar las ubicaciones de los archivos de claves</string> <string name="remember_keyfile_locations_title">Recordar las ubicaciones de los archivos de clave</string>
<string name="subdomain_search_summary">Buscar dominios web con restricciones de subdominios</string> <string name="subdomain_search_summary">Buscar dominios web con restricciones de subdominios</string>
<string name="subdomain_search_title">Búsqueda de subdominio</string> <string name="subdomain_search_title">Búsqueda de subdominio</string>
<string name="auto_focus_search_summary">Solicite una búsqueda al abrir una base de datos</string> <string name="auto_focus_search_summary">Solicite una búsqueda al abrir una base de datos</string>
<string name="auto_focus_search_title">Búsqueda rápida</string> <string name="auto_focus_search_title">Búsqueda rápida</string>
<string name="menu_delete_entry_history">Borrar historial</string> <string name="menu_delete_entry_history">Eliminar el historial</string>
<string name="error_otp_digits">El token debe tener de %1$d a %2$d dígitos.</string> <string name="error_otp_digits">El token debe contener de %1$d a %2$d dígitos.</string>
<string name="entry_attachments">Archivos adjuntos</string> <string name="entry_attachments">Archivos adjuntos</string>
<string name="entry_add_attachment">Adjuntar</string> <string name="entry_add_attachment">Añadir el archivo adjunto</string>
<string name="content_description_credentials_information">Información de credenciales</string> <string name="content_description_credentials_information">Información de credenciales</string>
<string name="database_opened">Base de datos abierta</string> <string name="database_opened">Base de datos abierta</string>
<string name="education_add_attachment_title">Adjuntar</string> <string name="education_add_attachment_title">Adjuntar</string>
@@ -455,4 +457,94 @@
<string name="warning_file_too_big">Se supone que una base de datos KeePass contiene solo pequeños archivos de utilidad (como archivos de clave PGP). <string name="warning_file_too_big">Se supone que una base de datos KeePass contiene solo pequeños archivos de utilidad (como archivos de clave PGP).
\n \n
\nLa base de datos puede volverse muy grande y reducir su rendimiento con esta subida.</string> \nLa base de datos puede volverse muy grande y reducir su rendimiento con esta subida.</string>
<string name="recycle_bin_group_title">Grupo de la papelera de reciclaje</string>
<string name="filter">Filtrar</string>
<string name="database_data_remove_unlinked_attachments_summary">Elimina los archivos adjuntos contenidos en la base de datos pero no vinculados a una entrada</string>
<string name="database_data_remove_unlinked_attachments_title">Eliminar los datos no vinculados</string>
<string name="data">Datos</string>
<string name="advanced_unlock_delete_all_key_warning">¿Borrar todas las claves de encriptación relacionadas con el reconocimiento de desbloqueo avanzado\?</string>
<string name="advanced_unlock_timeout">Tiempo límite de desbloqueo avanzado</string>
<string name="temp_advanced_unlock_timeout_summary">Duración del uso de desbloqueo avanzado antes de borrar su contenido</string>
<string name="temp_advanced_unlock_timeout_title">Expiración de desbloqueo avanzado</string>
<string name="temp_advanced_unlock_enable_summary">No almacenar ningún contenido encriptado para utilizar el desbloqueo avanzado</string>
<string name="temp_advanced_unlock_enable_title">Desbloqueo avanzado temporal</string>
<string name="device_credential_unlock_enable_summary">Le permite usar la credenciales de su dispositivo para abrir la base de datos</string>
<string name="device_credential_unlock_enable_title">Desbloqueo de las credenciales del dispositivo</string>
<string name="advanced_unlock_tap_delete">Toque para eliminar las teclas de desbloqueo avanzadas</string>
<string name="content">Contenido</string>
<string name="clipboard_explanation_summary">Copiar los campos de entrada usando el portapapeles de su dispositivo</string>
<string name="device_credential">Credenciales del dispositivo</string>
<string name="credential_before_click_advanced_unlock_button">Introduzca la contraseña y luego haga clic en el botón \"Desbloqueo avanzado\".</string>
<string name="advanced_unlock_prompt_not_initialized">No se pudo inicializar el indicador de desbloqueo avanzado.</string>
<string name="advanced_unlock_scanning_error">Error de desbloqueo avanzado: %1$s</string>
<string name="advanced_unlock_not_recognized">No se pudo reconocer la impresión de desbloqueo avanzado</string>
<string name="advanced_unlock_invalid_key">No se pudo leer la llave de desbloqueo avanzada. Por favor, bórrela y repita el procedimiento de reconocimiento de desbloqueo.</string>
<string name="advanced_unlock_prompt_extract_credential_message">Extraer la credencial de la base de datos con datos de desbloqueo avanzado</string>
<string name="advanced_unlock_prompt_extract_credential_title">Abrir la base de datos con reconocimiento de desbloqueo avanzado</string>
<string name="advanced_unlock_prompt_store_credential_message">Advertencia: Aún debes recordar tu contraseña maestra si usas el reconocimiento de desbloqueo avanzado.</string>
<string name="advanced_unlock_prompt_store_credential_title">Reconocimiento de desbloqueo avanzado</string>
<string name="open_advanced_unlock_prompt_store_credential">Abrir el indicador de desbloqueo avanzado para almacenar las credenciales</string>
<string name="open_advanced_unlock_prompt_unlock_database">Abrir el aviso de desbloqueo avanzado para desbloquear la base de datos</string>
<string name="keystore_not_accessible">El almacén de claves no está debidamente inicializado.</string>
<string name="biometric_security_update_required">Se requiere una actualización de la seguridad biométrica.</string>
<string name="configure_biometric">No se ha inscrito ninguna credencial biométrica o del dispositivo.</string>
<string name="warning_empty_keyfile_explanation">El contenido del archivo clave nunca debe modificarse y, en el mejor de los casos, debe contener datos generados al azar.</string>
<string name="warning_empty_recycle_bin">¿Borrar permanentemente todos los nodos de la papelera de reciclaje\?</string>
<string name="registration_mode">Modo de registro</string>
<string name="save_mode">Modo de guardado</string>
<string name="search_mode">Modo de búsqueda</string>
<string name="contains_duplicate_uuid_procedure">¿Resolver el problema generando nuevos UUID para que los duplicados continúen\?</string>
<string name="menu_keystore_remove_key">Borrar la clave de desbloqueo avanzado</string>
<string name="error_field_name_already_exists">El nombre del campo ya existe.</string>
<string name="error_registration_read_only">Guardar un nuevo elemento no está permitido en una base de datos de sólo lectura</string>
<string name="settings_database_recommend_changing_master_key_title">Recomendar la renovación</string>
<string name="max_history_size_summary">Limitar el tamaño del historial por entrada</string>
<string name="max_history_items_summary">Limitar el número de elementos del historial por entrada</string>
<string name="max_history_items_title">Número máximo</string>
<string name="device_keyboard_setting_title">Configuración del teclado del dispositivo</string>
<string name="database_custom_color_title">Color personalizado de la base de datos</string>
<string name="settings_database_recommend_changing_master_key_summary">Recomendar cambiar la contraseña maestra (días)</string>
<string name="notification">Notificación</string>
<string name="hide_expired_entries_title">Ocultar las entradas expiradas</string>
<string name="keyboard_search_share_title">Buscar información compartida</string>
<string name="kdf_Argon2id">Argon2id</string>
<string name="kdf_Argon2d">Argon2d</string>
<string name="upload_attachment">Subir %1$s</string>
<string name="education_setup_OTP_summary">Configurar la gestión de contraseñas de una sola vez (HOTP / TOTP) para generar un token solicitado para la autenticación de dos factores (2FA).</string>
<string name="education_setup_OTP_title">Establecer la contaseña de un solo uso</string>
<string name="education_advanced_unlock_summary">Vincule su contraseña con su credencial biométrica o del dispositivo escaneada para desbloquear rápidamente su base de datos.</string>
<string name="education_advanced_unlock_title">Desbloqueo avanzado de la base de datos</string>
<string name="autofill_read_only_save">No se permite guardar datos en una base de datos abierta como de sólo lectura.</string>
<string name="autofill_block_restart">Reiniciar la aplicación que contiene el formulario para activar el bloqueo.</string>
<string name="autofill_web_domain_blocklist_summary">Lista de bloqueo que impide el llenado automático de los dominios web</string>
<string name="autofill_web_domain_blocklist_title">Lista de bloqueo de los dominios web</string>
<string name="autofill_application_id_blocklist_summary">Lista de bloqueo que impide el llenado automático de las aplicaciones</string>
<string name="autofill_application_id_blocklist_title">Lista de bloqueo de las aplicaciones</string>
<string name="autofill_ask_to_save_data_summary">Pedir que se guarden los datos cuando se valide un formulario</string>
<string name="autofill_ask_to_save_data_title">Pedir que se guarden los datos</string>
<string name="autofill_save_search_info_summary">Intente guardar la información de la búsqueda cuando haga una selección de entrada manual</string>
<string name="autofill_save_search_info_title">Guardar la información de la búsqueda</string>
<string name="autofill_auto_search_summary">Sugerir automáticamente los resultados de la búsqueda desde el dominio web o el ID de la aplicación</string>
<string name="autofill_auto_search_title">Búsqueda automática</string>
<string name="autofill_close_database_summary">Cerrar la base de datos después de una selección de autocompletado</string>
<string name="autofill_close_database_title">Cerrar la base de datos</string>
<string name="enter">Entrar</string>
<string name="backspace">Retroceder</string>
<string name="select_entry">Seleccionar la entrada</string>
<string name="back_to_previous_keyboard">Volver al teclado anterior</string>
<string name="custom_fields">Campos personalizados</string>
<string name="keyboard_previous_lock_summary">Cambiar automáticamente al teclado anterior después de bloquear la base de datos</string>
<string name="keyboard_previous_lock_title">Bloquear la base de datos</string>
<string name="keyboard_previous_fill_in_summary">Cambiar automáticamente al teclado anterior después de ejecutar \"Acción de la tecla automática\"</string>
<string name="keyboard_previous_fill_in_title">Acción de la tecla automática</string>
<string name="keyboard_previous_database_credentials_summary">Cambiar automáticamente al teclado anterior en la pantalla de credenciales de la base de datos</string>
<string name="keyboard_previous_database_credentials_title">Pantalla de credenciales de la base de datos</string>
<string name="keyboard_auto_go_action_title">Acción de la tecla automática</string>
<string name="keyboard_save_search_info_summary">Intentar guardar la información compartida cuando hagas una selección de entrada manual</string>
<string name="keyboard_save_search_info_title">Guardar información compartida</string>
<string name="keyboard_search_share_summary">Buscar automáticamente la información compartida para llenar el teclado</string>
<string name="show_uuid_summary">Muestra el UUID vinculado a una entrada</string>
<string name="show_uuid_title">Mostrar UUID</string>
<string name="error_rebuild_list">No es posible reconstruir adecuadamente la lista.</string>
<string name="error_database_uri_null">La URI de la base de datos no puede ser recuperada.</string>
</resources> </resources>

View File

@@ -37,7 +37,7 @@
<string name="saving_database">در حال ذخیره پایگاه داده</string> <string name="saving_database">در حال ذخیره پایگاه داده</string>
<string name="parallelism_explanation">درجه موازی سازی (یعنی تعداد موضوعات) که توسط عملکرد مشتق کلیدی استفاده می شود.</string> <string name="parallelism_explanation">درجه موازی سازی (یعنی تعداد موضوعات) که توسط عملکرد مشتق کلیدی استفاده می شود.</string>
<string name="parallelism">موازی کاری</string> <string name="parallelism">موازی کاری</string>
<string name="memory_usage_explanation">مقدار حافظه (در بایت) که توسط تابع مشتق کلید مورد استفاده قرار گیرد.</string> <string name="memory_usage_explanation">مقدار حافظه که توسط تابع مشتق کلید مورد استفاده قرار گیرد.</string>
<string name="memory_usage">استفاده از حافظه</string> <string name="memory_usage">استفاده از حافظه</string>
<string name="rounds_explanation">دورهای رمزگذاری اضافی محافظت بالاتری در برابر حملات نیروی وحشی ایجاد می کنند ، اما در واقع می توانند سرعت و بارگذاری را کاهش دهند.</string> <string name="rounds_explanation">دورهای رمزگذاری اضافی محافظت بالاتری در برابر حملات نیروی وحشی ایجاد می کنند ، اما در واقع می توانند سرعت و بارگذاری را کاهش دهند.</string>
<string name="rounds">دور تحول</string> <string name="rounds">دور تحول</string>

View File

@@ -249,7 +249,7 @@
<string name="list_password_generator_options_title">Salasanamerkit</string> <string name="list_password_generator_options_title">Salasanamerkit</string>
<string name="password_size_summary">Asettaa oletuspituuden generoiduille salasanoille</string> <string name="password_size_summary">Asettaa oletuspituuden generoiduille salasanoille</string>
<string name="parallelism">Rinnakkaisuus</string> <string name="parallelism">Rinnakkaisuus</string>
<string name="memory_usage_explanation">Avaimen johtamisfunktion käyttämän muistin (tavuina) määrä.</string> <string name="memory_usage_explanation">Avaimen johtamisfunktion käyttämän muistin määrä.</string>
<string name="memory_usage">Muistin käyttö</string> <string name="memory_usage">Muistin käyttö</string>
<string name="kdf_explanation">Pääavain muunnetaan käyttäen satunnaista suolattua avaimen johtamisfunktiota, jotta salausalgoritmin avain voidaan generoida.</string> <string name="kdf_explanation">Pääavain muunnetaan käyttäen satunnaista suolattua avaimen johtamisfunktiota, jotta salausalgoritmin avain voidaan generoida.</string>
<string name="encryption_explanation">Salasanatietokannan salausalgoritmi, jota käytetään kaikelle datalle.</string> <string name="encryption_explanation">Salasanatietokannan salausalgoritmi, jota käytetään kaikelle datalle.</string>

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