Compare commits

..

187 Commits

Author SHA1 Message Date
J-Jamet
d560c3e8de Merge branch 'release/2.10.1' 2021-05-10 07:24:42 +02:00
J-Jamet
4f8e8e6669 Upgrade version code for deployment 2021-05-10 06:05:22 +02:00
J-Jamet
7cdc2e0915 Fix custom data item #986 2021-05-10 06:03:12 +02:00
J-Jamet
74b236b317 Fix class cast exception #986 2021-05-09 22:29:46 +02:00
J-Jamet
89ffeaf03b Fix crash with parcelable cast 2021-05-08 12:52:56 +02:00
J-Jamet
8b779a0fca Upgrade version 2021-05-07 17:45:12 +02:00
J-Jamet
4b71dc8445 Merge tag '2.10.0' into develop
2.10.0
2021-05-07 17:36:12 +02:00
J-Jamet
780875d5f2 Merge branch 'release/2.10.0' 2021-05-07 17:36:06 +02:00
J-Jamet
7356d4b0e2 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-05-07 17:08:31 +02:00
J-Jamet
654dea6b7e Manage new database format 4.1 #956 2021-05-07 17:06:41 +02:00
J-Jamet
1204374637 Merge branch 'feature/Database_4.1' into develop 2021-05-07 17:03:12 +02:00
J-Jamet
880fde2148 Change version to 2.10.0 2021-05-07 17:02:57 +02:00
J-Jamet
d776d76100 Fix null context with advanced unlocking 2021-05-07 17:01:54 +02:00
J-Jamet
2a7af826a8 Update CHANGELOG 2021-05-07 10:41:24 +02:00
J-Jamet
9d2fd53073 Fix lock 2021-05-07 08:31:05 +02:00
J-Jamet
edc5985a7e Fix show button consistency #980 2021-05-05 11:01:53 +02:00
C. Rüdinger
96fc79103b Translated using Weblate (German)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-05-04 00:18:35 +02:00
VfBFan
76879f3a73 Translated using Weblate (German)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-05-04 00:18:34 +02:00
J-Jamet
328629fe88 Fix writing previous parent group in 4.0 2021-05-03 15:41:39 +02:00
ssantos
9754535055 Translated using Weblate (Portuguese)
Currently translated at 85.9% (454 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2021-05-02 22:32:18 +02:00
J. Lavoie
26f701d890 Translated using Weblate (Italian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-05-02 22:32:17 +02:00
VfBFan
f3df8024e6 Translated using Weblate (German)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-05-02 22:32:17 +02:00
J-Jamet
bc9fdfc7a4 Fix header custom data condition 2021-05-01 18:24:11 +02:00
J-Jamet
e1e37989e4 Fix custom icon condition 2021-05-01 18:06:47 +02:00
J-Jamet
dc67cb1807 Set previous parent group during a move or a deletion 2021-05-01 17:48:48 +02:00
J-Jamet
2e8a2457bc Show custom icon name #976 2021-05-01 14:36:11 +02:00
J-Jamet
6d4398c6fd Remove custom data last modification time recognition in Entry and Group 2021-05-01 13:03:44 +02:00
J-Jamet
9f67ad872d Remove contains parent group as condition to 4.1 migration 2021-05-01 12:58:28 +02:00
J-Jamet
aba396f274 Fix parcelable UUID 2021-04-30 21:57:59 +02:00
J-Jamet
4315f34398 Add previous parent group as condition to migrate to 4.1 2021-04-30 21:46:01 +02:00
J-Jamet
47b456e1ee Custom data refactoring 2021-04-30 21:24:37 +02:00
J-Jamet
529810b2dc Fix tags and implements previous parent group 2021-04-30 21:03:29 +02:00
J-Jamet
6f67b8e788 Fix tags string length representation 2021-04-30 20:10:49 +02:00
J-Jamet
a9c9d12444 Better write XML nodes implementation 2021-04-30 20:03:59 +02:00
J-Jamet
f8428cec61 Better date XML implementation and DeletedObject as parcelable 2021-04-30 19:58:21 +02:00
J-Jamet
ac46bce807 Fix custom data writing 2021-04-30 19:39:57 +02:00
J-Jamet
23e899042d Add custom data objects 2021-04-30 19:23:56 +02:00
J-Jamet
80c4a3c06d Fix custom icon parser 2021-04-30 17:19:39 +02:00
Hosted Weblate
d2a31601ba Merge branch 'origin/develop' into Weblate. 2021-04-30 17:04:40 +02:00
J-Jamet
7ddb4f3486 Fix saving custom icons 2021-04-30 14:57:48 +02:00
J-Jamet
e56da87e0e Add last modification to custom icons 2021-04-30 14:11:48 +02:00
J-Jamet
2e4ebecf67 Add name to custom icons #956 2021-04-30 12:49:36 +02:00
J-Jamet
1b4ccaed91 Manage quality check parameter #956 2021-04-30 12:08:21 +02:00
J-Jamet
1e2d41c7fb Merge branch 'develop' into feature/Database_4.1 2021-04-30 11:41:38 +02:00
J-Jamet
1b2ead054a Upgrade to version 3.0.0 2021-04-30 11:37:32 +02:00
J-Jamet
468c1b95b7 Fix CHANGELOG version 2021-04-30 11:29:58 +02:00
J-Jamet
60d8eff71f Merge tag '2.9.20' into develop
2.9.20
2021-04-30 11:28:16 +02:00
J-Jamet
8baae8b801 Merge branch 'release/2.9.20' 2021-04-30 11:28:03 +02:00
J-Jamet
9f16f26347 skip unchanged fastlane elements to deploy 2021-04-29 22:23:51 +02:00
J-Jamet
69b4cacab4 Upgrade version code to deploy beta 2021-04-29 22:23:13 +02:00
J-Jamet
b74b5040b1 Better timeout setting integration #974 2021-04-29 21:57:25 +02:00
J-Jamet
a28decc854 Fix timeout with 0s #974 2021-04-29 21:45:39 +02:00
J-Jamet
cb59cef1b8 Remove unused dependency 2021-04-29 12:37:19 +02:00
J-Jamet
d9b600466c Rollback ignore accents #945 2021-04-29 12:09:53 +02:00
J-Jamet
4edf2f8cd1 Upgrade CHANGELOG 2021-04-29 11:04:33 +02:00
J-Jamet
c60dfdf0d7 Fix search during a node action #972 2021-04-29 10:58:16 +02:00
J-Jamet
006afc6841 Fix search with non-latin chars #971 2021-04-29 10:44:12 +02:00
J-Jamet
f70879581d Change version to fix bugs 2021-04-29 10:35:34 +02:00
J-Jamet
7a2d2b0376 Add database version 4.1 2021-04-28 16:26:11 +02:00
J-Jamet
b5368fa239 First commit to allow tags in groups 2021-04-28 13:16:09 +02:00
J-Jamet
db1d71af9f Upgrade version 2021-04-28 12:04:22 +02:00
J-Jamet
6b03ef35a6 Merge tag '2.9.19' into develop
2.9.19
2021-04-28 10:01:22 +02:00
J-Jamet
2afd02d86f Merge branch 'release/2.9.19' 2021-04-28 10:01:14 +02:00
Oymate
b32c00f455 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 5.3% (28 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn_BD/
2021-04-27 15:32:17 +02:00
Milo Ivir
fbe9fb41ed Translated using Weblate (Croatian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-04-27 15:32:17 +02:00
gnu-ewm
cc0a7f7d76 Translated using Weblate (Polish)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-04-27 15:32:17 +02:00
Y. Sakamoto
db33fc60b9 Translated using Weblate (Japanese)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-04-27 15:32:16 +02:00
VfBFan
6feaee4f86 Translated using Weblate (German)
Currently translated at 99.8% (527 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-04-27 15:32:16 +02:00
C. Rüdinger
1a27a31a32 Translated using Weblate (German)
Currently translated at 99.8% (527 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-04-27 15:32:15 +02:00
zeritti
5b611e71d5 Translated using Weblate (Czech)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-04-27 15:32:15 +02:00
J-Jamet
6de88bfe11 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-04-27 10:40:30 +02:00
J-Jamet
6d7236249f Fix field reference in search preview #964 2021-04-27 10:36:36 +02:00
J-Jamet
69fbaba8a6 Fix field reference engine 2021-04-26 21:46:45 +02:00
J-Jamet
6d88737505 Better field reference engine implementation 2021-04-26 16:28:56 +02:00
C. Rüdinger
9869cfc736 Translated using Weblate (German)
Currently translated at 96.9% (512 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-04-26 13:40:04 +02:00
VfBFan
8505326a68 Translated using Weblate (German)
Currently translated at 97.1% (513 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-04-26 13:32:54 +02:00
C. Rüdinger
3a4af88384 Translated using Weblate (German)
Currently translated at 97.1% (513 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-04-26 13:32:53 +02:00
solokot
5b2e7d0f70 Translated using Weblate (Russian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-04-25 22:57:45 +02:00
J-Jamet
ddeea6bee3 Remove field reference in search 2021-04-25 21:28:23 +02:00
J-Jamet
0cfe3a7634 Check flatten 2021-04-25 16:52:37 +02:00
J-Jamet
727463e4d1 Better field reference engine implementation 2021-04-25 16:48:37 +02:00
J-Jamet
d42abfdc56 Remove unused search code 2021-04-25 15:08:36 +02:00
J-Jamet
e01ea1df4c Fix OTP token generation #967 2021-04-23 21:43:38 +02:00
J-Jamet
078bfac5f5 Upgrade CHANGELOG 2021-04-23 15:33:57 +02:00
J-Jamet
111b07b9e6 Better temp advanced unlocking implementation 2021-04-23 15:30:00 +02:00
J-Jamet
dfbc89addc Fix database notification #965 2021-04-23 14:24:23 +02:00
J-Jamet
bf44da9a14 Update version and CHANGELOG 2021-04-23 14:24:23 +02:00
J-Jamet
d75d13965b Faster accent replacement method implementation #964 2021-04-23 14:24:23 +02:00
Oğuz Ersen
8aedebdc94 Translated using Weblate (Turkish)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-04-22 14:32:18 +02:00
Eric
9388c4bb0d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-04-22 14:32:17 +02:00
Ihor Hordiichuk
77d4f601af Translated using Weblate (Ukrainian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-04-22 14:32:16 +02:00
solokot
7fae590848 Translated using Weblate (Russian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-04-22 14:32:16 +02:00
Stephan Paternotte
bc41558a26 Translated using Weblate (Dutch)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-04-22 14:32:16 +02:00
Oliver Cervera
f6651face4 Translated using Weblate (Italian)
Currently translated at 99.8% (527 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-04-22 14:32:16 +02:00
Kunzisoft
345f00f7f2 Translated using Weblate (French)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-04-22 14:32:15 +02:00
Retrial
e876d02118 Translated using Weblate (Greek)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-04-22 14:32:15 +02:00
J-Jamet
5b7018f71b Merge tag '2.9.18' into develop
2.9.18
2021-04-20 20:11:11 +02:00
J-Jamet
f45b3fc50a Merge branch 'release/2.9.18' 2021-04-20 20:11:05 +02:00
J-Jamet
01196be30d Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-04-20 11:57:48 +02:00
J-Jamet
0a2999bffb Change Pro description 2021-04-20 11:46:55 +02:00
J-Jamet
8f097096e7 Update CHANGELOG 2021-04-20 11:36:52 +02:00
J-Jamet
cd97fc046a Fix theme in Libre version 2021-04-20 11:35:14 +02:00
Hosted Weblate
eeb10f31a6 Merge branch 'origin/develop' into Weblate. 2021-04-20 11:34:10 +02:00
J-Jamet
9df5e116e8 Change to version 2.9.18 to deploy bugs fixes 2021-04-20 11:01:26 +02:00
J-Jamet
1228a03d39 searchInEntry as instance method 2021-04-19 18:34:44 +02:00
J-Jamet
a5e1b3096e Merge branch 'feature/Search_Fields' into develop #962 2021-04-19 18:28:17 +02:00
J-Jamet
b41ae67128 Update CHANGELOG 2021-04-19 18:28:07 +02:00
Sebastian
ddfbe20125 Translated using Weblate (Danish)
Currently translated at 98.1% (517 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2021-04-19 18:27:10 +02:00
J-Jamet
0bfe9291dd Move UUID util 2021-04-19 18:26:40 +02:00
J-Jamet
622b2e1edc Fix search in field reference 2021-04-19 18:24:30 +02:00
J-Jamet
4a40719534 Search refactoring 2021-04-19 17:54:16 +02:00
J-Jamet
384993d363 Remove diacritical marks in search string #945 2021-04-19 15:24:06 +02:00
J-Jamet
01b7d28154 Update CHANGELOG 2021-04-19 13:28:48 +02:00
J-Jamet
d7c4f5577f Merge branch 'feature/Move_Group' into develop #658 2021-04-19 13:27:02 +02:00
J-Jamet
a69d23ca64 Update CHANGELOG 2021-04-19 13:26:41 +02:00
J-Jamet
e2f8b7a6e3 Fix move group message 2021-04-19 13:16:30 +02:00
J-Jamet
171a0b012f Fix remove recycle bin 2021-04-19 12:59:02 +02:00
J-Jamet
5c04b15433 Check group name to prevent manual backup group creation 2021-04-19 11:45:53 +02:00
J-Jamet
6397feffff Better search backup group implementation 2021-04-19 11:11:07 +02:00
J-Jamet
e73b9b7f1c Move group and fix backup in KDB 2021-04-18 23:52:59 +02:00
WaldiS
0d82e40c67 Translated using Weblate (Polish)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-04-17 23:27:06 +02:00
Stephan Paternotte
b75d6d02fa Translated using Weblate (Dutch)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-04-17 23:27:06 +02:00
J. Lavoie
76d4542716 Translated using Weblate (French)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-04-17 23:27:05 +02:00
J. Lavoie
87955de849 Translated using Weblate (German)
Currently translated at 95.8% (505 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-04-17 23:27:04 +02:00
J-Jamet
6df60cf5da Upgrade biometric lib to 1.1.0 2021-04-17 12:19:14 +02:00
Oliver Cervera
3c23a314f0 Translated using Weblate (Italian)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-04-16 10:27:10 +02:00
Oğuz Ersen
8fda6b04a4 Translated using Weblate (Turkish)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-04-15 01:57:14 +02:00
Eric
9fa98e6b76 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-04-15 01:57:14 +02:00
Ihor Hordiichuk
deb685f39b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-04-15 01:57:13 +02:00
Kunzisoft
d7851d3a18 Translated using Weblate (French)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-04-15 01:57:13 +02:00
Retrial
44946fc54a Translated using Weblate (Greek)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-04-15 01:57:12 +02:00
Hosted Weblate
a033d10adc Merge branch 'origin/develop' into Weblate. 2021-04-14 10:49:40 +02:00
André Marcelo Alvarenga
5a3e599fe0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 81.2% (428 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2021-04-14 10:49:39 +02:00
Oliver Cervera
a9f645f389 Translated using Weblate (Italian)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-04-14 10:49:39 +02:00
J-Jamet
d662f0903a Workaround to autofill recognition #960 2021-04-13 12:48:33 +02:00
J-Jamet
beaa947eb7 Upgrade version 2021-04-13 11:41:03 +02:00
random r
48006b64d6 Translated using Weblate (Italian)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-04-13 11:34:57 +02:00
J-Jamet
8f195ba66f Merge tag '2.9.17' into develop
2.9.17
2021-04-13 09:11:12 +02:00
J-Jamet
123288e745 Merge branch 'release/2.9.17' 2021-04-13 09:11:05 +02:00
Milo Ivir
5866e95d49 Translated using Weblate (Croatian)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-04-10 22:27:10 +02:00
WaldiS
e79f395424 Translated using Weblate (Polish)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-04-10 22:27:09 +02:00
HARADA Hiroyuki
999ca87fec Translated using Weblate (Japanese)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-04-10 22:27:09 +02:00
Oliver Cervera
1217266d88 Translated using Weblate (Italian)
Currently translated at 98.1% (517 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-04-10 22:27:08 +02:00
Sebastian
bb262198be Translated using Weblate (Danish)
Currently translated at 93.1% (491 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2021-04-10 22:27:08 +02:00
J-Jamet
11aae77caf Update CHANGELOG 2021-04-10 20:56:25 +02:00
J-Jamet
8212cede6e Merge branch 'feature/Duration_Preference' into develop #579 2021-04-10 20:53:35 +02:00
J-Jamet
a3c51884f4 Fix timeout strings 2021-04-10 20:53:24 +02:00
J-Jamet
b8890aca7f Better notification timer implementation 2021-04-10 20:30:48 +02:00
J-Jamet
014b0cce14 Add duration preference with number picker 2021-04-10 19:29:19 +02:00
Oğuz Ersen
6d860c5cb7 Translated using Weblate (Turkish)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-04-08 20:55:17 +02:00
Eric
d8be832858 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-04-08 20:55:17 +02:00
Ihor Hordiichuk
afcb9fcf41 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-04-08 20:55:16 +02:00
solokot
3c7ae0aaf0 Translated using Weblate (Russian)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-04-08 20:55:15 +02:00
Retrial
6b7f93dbfe Translated using Weblate (Greek)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-04-08 20:55:15 +02:00
J-Jamet
c40b255022 Check properties 2021-04-08 16:40:01 +02:00
J-Jamet
1742d265f3 Better stylish implementation 2021-04-08 16:26:14 +02:00
Nikita Epifanov
3240e0bcae Translated using Weblate (Russian)
Currently translated at 100.0% (527 of 527 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-04-08 13:59:42 +02:00
J-Jamet
ff185f6505 Upgrade version and CHANGELOG 2021-04-08 12:58:39 +02:00
J-Jamet
346b517c9d Force twofish padding compatibility #955 2021-04-08 12:55:23 +02:00
Hosted Weblate
80f00aba0a Merge branch 'origin/develop' into Weblate. 2021-04-08 12:07:37 +02:00
J-Jamet
949905f6e2 Merge branch 'feature/Import_Export_App_Properties' into develop 2021-04-08 12:06:52 +02:00
J-Jamet
b9e26fecfd Export and import properties 2021-04-08 11:45:23 +02:00
solokot
232682f4a8 Translated using Weblate (Russian)
Currently translated at 100.0% (516 of 516 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-04-08 08:27:03 +02:00
C. Rüdinger
de3b690d60 Translated using Weblate (German)
Currently translated at 97.0% (501 of 516 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-04-08 08:27:03 +02:00
J-Jamet
de69a78a98 First commit to import and export app properties 2021-04-07 16:13:40 +02:00
J-Jamet
1c341c34a3 Merge tag '2.9.16' into develop
2.9.16
2021-04-07 09:53:13 +02:00
J-Jamet
33beb57e9d Merge branch 'release/2.9.16' 2021-04-07 09:53:07 +02:00
J-Jamet
66eeadca0b Upgrade version code 2021-04-06 17:39:27 +02:00
J-Jamet
a10d1c98a8 Fix KDB parcelable 2021-04-06 17:32:40 +02:00
J-Jamet
59ead4986f Move Parent Parcelable 2021-04-06 15:05:11 +02:00
J-Jamet
09f6c18189 Small changes 2021-04-06 15:02:24 +02:00
Timur Seber
a5cd6d5ac0 Translated using Weblate (Tatar)
Currently translated at 8.5% (44 of 516 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tt/
2021-04-06 06:26:46 +02:00
J-Jamet
0f3ad7c8b1 Fix select custom icon 2021-04-05 19:06:54 +02:00
J-Jamet
0487dea7fc Fix null cache directory 2021-04-05 11:32:07 +02:00
Timur Seber
a6803bf0e3 Added translation using Weblate (Tatar) 2021-04-05 05:18:42 +02:00
J-Jamet
8cac1ee284 Merge branch 'feature/ExternalFileHelper' into develop 2021-04-05 00:06:03 +02:00
J-Jamet
196620e1bd Remove unused methods 2021-04-05 00:04:25 +02:00
J-Jamet
43d6c76873 Refactor open document click listener 2021-04-04 23:44:25 +02:00
J-Jamet
b864c39a0d Fix add database workflow in some devices 2021-04-04 23:13:24 +02:00
J-Jamet
818b975111 Change default type verification to create document 2021-04-04 09:56:09 +02:00
J-Jamet
d5fbc8393f Change file creation methods 2021-04-03 19:40:55 +02:00
Oliver Cervera
df9a71a63d Translated using Weblate (Italian)
Currently translated at 100.0% (516 of 516 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-04-02 12:26:46 +02:00
J-Jamet
7b5e9d2344 Better parcelable entry CREATOR implementation #948 2021-04-02 09:37:18 +02:00
J-Jamet
7fc2d95886 Fix document file retrievment 2021-04-01 10:52:00 +02:00
J-Jamet
78d3b369bb Move Parcelable inheritance 2021-03-31 19:39:07 +02:00
J-Jamet
bb3620680b Upgrade version 2021-03-31 17:58:49 +02:00
J-Jamet
d4a45655ca Merge tag '2.9.15' into develop
2.9.15
2021-03-29 22:01:52 +02:00
162 changed files with 3350 additions and 2520 deletions

View File

@@ -1,3 +1,37 @@
KeePassDX(2.10.1)
* Fix parcelable with custom data #986
KeePassDX(2.10.0)
* Manage new database format 4.1 #956
* Fix show button consistency #980
* Fix persistent notification #979
KeePassDX(2.9.20)
* Fix search with non-latin chars #971
* Fix action mode with search #972 (rollback ignore accents #945)
* Fix timeout with 0s #974
KeePassDX(2.9.19)
* Fix search slowdown #964
* Fix closing notification after lock request #965
* Better temp advanced unlocking code implementation #965
* Fix OTP token generation #967
KeePassDX(2.9.18)
* Move groups #658
* Improve autofill recognition #960
* Remove diacritical marks in search string #945
* Fix search in references #962
* Fix themes in Libre version
KeePassDX(2.9.17)
* Import / Export app properties #839
* Force twofish padding compatibility #955
* Better timeout preference #579
KeePassDX(2.9.16)
* Fix small bugs #948
KeePassDX(2.9.15)
* Fix themes #935 #926
* Decrease default clipboard time #934

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 30
versionCode = 68
versionName = "2.9.15"
versionCode = 78
versionName = "2.10.1"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -109,7 +109,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0-rc01'
implementation 'androidx.biometric:biometric:1.1.0'
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.2"
implementation 'androidx.fragment:fragment-ktx:1.2.5'

View File

@@ -0,0 +1,15 @@
package com.kunzisoft.keepass.tests.utils
import com.kunzisoft.keepass.utils.UuidUtil
import junit.framework.TestCase
import java.util.*
class UUIDTest: TestCase() {
fun testUUID() {
val randomUUID = UUID.randomUUID()
val hexStringUUID = UuidUtil.toHexString(randomUUID)
val retrievedUUID = UuidUtil.fromHexString(hexStringUUID)
assertEquals(randomUUID, retrievedUUID)
}
}

View File

@@ -39,6 +39,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
@@ -93,6 +94,8 @@ class EntryActivity : LockingActivity() {
private var clipboardHelper: ClipboardHelper? = null
private var mFirstLaunchOfActivity: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null
private var iconColor: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
@@ -140,6 +143,9 @@ class EntryActivity : LockingActivity() {
clipboardHelper = ClipboardHelper(this)
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
// Init SAF manager
mExternalFileHelper = ExternalFileHelper(this)
// Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
@@ -344,7 +350,7 @@ class EntryActivity : LockingActivity() {
// Manage attachments
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
createDocument(this, attachmentItem.name)?.let { requestCode ->
mExternalFileHelper?.createDocument(attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
@@ -380,7 +386,7 @@ class EntryActivity : LockingActivity() {
}
}
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
if (createdFileUri != null) {
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
mAttachmentFileBinderManager

View File

@@ -45,7 +45,7 @@ import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.autofill.AutofillComponent
@@ -103,7 +103,7 @@ class EntryEditActivity : LockingActivity(),
private var lockView: View? = null
// To manage attachments
private var mSelectFileHelper: SelectFileHelper? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAllowMultipleAttachments: Boolean = false
private var mTempAttachments = ArrayList<EntryAttachmentState>()
@@ -241,7 +241,7 @@ class EntryEditActivity : LockingActivity(),
}
// To retrieve attachment
mSelectFileHelper = SelectFileHelper(this)
mExternalFileHelper = ExternalFileHelper(this)
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Save button
@@ -458,8 +458,8 @@ class EntryEditActivity : LockingActivity(),
/**
* Add a new attachment
*/
private fun addNewAttachment(item: MenuItem) {
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
private fun addNewAttachment() {
mExternalFileHelper?.openDocument()
}
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
@@ -505,7 +505,7 @@ class EntryEditActivity : LockingActivity(),
entryEditFragment?.icon = icon
}
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
uri?.let { attachmentToUploadUri ->
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName ->
@@ -655,7 +655,7 @@ class EntryEditActivity : LockingActivity(),
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView,
{
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
mExternalFileHelper?.openDocument()
},
{
performedNextEducation(entryEditActivityEducation)
@@ -683,7 +683,7 @@ class EntryEditActivity : LockingActivity(),
return true
}
R.id.menu_add_attachment -> {
addNewAttachment(item)
addNewAttachment()
return true
}
R.id.menu_add_otp -> {

View File

@@ -42,8 +42,9 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
@@ -82,7 +83,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private var mDatabaseFileUri: Uri? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
@@ -103,14 +104,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
createDatabaseButtonView?.setOnClickListener { createNewFile() }
// Open database button
mSelectFileHelper = SelectFileHelper(this)
mExternalFileHelper = ExternalFileHelper(this)
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.apply {
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
// History list
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
@@ -171,8 +167,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
}
GroupActivity.launch(this@FileDatabaseSelectActivity,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
}
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
@@ -185,10 +179,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
}
}
}
databaseFilesViewModel.consumeAction()
} catch (e: Exception) {
Log.e(TAG, "Unable to observe database action", e)
}
databaseFilesViewModel.consumeAction()
}
// Observe default database
@@ -206,6 +200,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
}
GroupActivity.launch(this@FileDatabaseSelectActivity,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
}
ACTION_DATABASE_LOAD_TASK -> {
val database = Database.getInstance()
@@ -234,7 +230,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
* Create a new file by calling the content provider
*/
private fun createNewFile() {
createDocument(this, getString(R.string.database_file_name_default) +
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default), "application/x-keepass")
}
@@ -286,7 +282,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Show open and create button or special mode
when (mSpecialMode) {
SpecialMode.DEFAULT -> {
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
// There is an activity which can handle this intent.
createDatabaseButtonView?.visibility = View.VISIBLE
} else{
@@ -359,14 +355,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
if (uri != null) {
launchPasswordActivityWithPath(uri)
}
}
// Retrieve the created URI from the file manager
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
@@ -412,9 +408,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
openDatabaseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
openDatabaseButtonView!!,
{tapTargetView ->
{ tapTargetView ->
tapTargetView?.let {
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
mExternalFileHelper?.openDocument()
}
},
{}

View File

@@ -382,6 +382,7 @@ class GroupActivity : LockingActivity(),
Log.d(TAG, "setNewIntent: $intentNotNull")
setIntent(intentNotNull)
if (Intent.ACTION_SEARCH == intentNotNull.action) {
finishNodeAction()
// only one instance of search in backstack
deletePreviousSearchGroup()
openGroup(retrieveCurrentGroup(intentNotNull, null), true)
@@ -812,74 +813,75 @@ class GroupActivity : LockingActivity(),
override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
nodes: List<Node>): Boolean {
// Move or copy only if allowed (in root if allowed)
if (mCurrentGroup != mDatabase?.rootGroup
|| mDatabase?.rootCanContainsEntry() == true) {
when (pasteMode) {
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
// Copy
mCurrentGroup?.let { newParent ->
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
)
}
}
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
// Move
mCurrentGroup?.let { newParent ->
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
)
}
}
else -> {
when (pasteMode) {
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
// Copy
mCurrentGroup?.let { newParent ->
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
)
}
}
} else {
coordinatorLayout?.let { coordinatorLayout ->
Snackbar.make(coordinatorLayout,
R.string.error_copy_entry_here,
Snackbar.LENGTH_LONG).asError().show()
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
// Move
mCurrentGroup?.let { newParent ->
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
)
}
}
else -> {}
}
finishNodeAction()
return true
}
private fun eachNodeRecyclable(nodes: List<Node>): Boolean {
mDatabase?.let { database ->
return nodes.find { node ->
var cannotRecycle = true
if (node is Entry) {
cannotRecycle = !database.canRecycle(node)
} else if (node is Group) {
cannotRecycle = !database.canRecycle(node)
}
cannotRecycle
} == null
}
return false
}
private fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false): Boolean {
val database = mDatabase
mDatabase?.let { database ->
// If recycle bin enabled, ensure it exists
if (database != null && database.isRecycleBinEnabled) {
database.ensureRecycleBinExists(resources)
}
// If recycle bin enabled and not in recycle bin, move in recycle bin
if (database != null
&& database.isRecycleBinEnabled
&& database.recycleBin != mCurrentGroup) {
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
nodes,
!mReadOnly && mAutoSaveEnable
)
}
// else open the dialog to confirm deletion
else {
val deleteNodesDialogFragment: DeleteNodesDialogFragment =
if (recycleBin) {
EmptyRecycleBinDialogFragment.getInstance(nodes)
} else {
DeleteNodesDialogFragment.getInstance(nodes)
// If recycle bin enabled, ensure it exists
if (database.isRecycleBinEnabled) {
database.ensureRecycleBinExists(resources)
}
deleteNodesDialogFragment.show(supportFragmentManager, "deleteNodesDialogFragment")
// If recycle bin enabled and not in recycle bin, move in recycle bin
if (eachNodeRecyclable(nodes)) {
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
nodes,
!mReadOnly && mAutoSaveEnable
)
}
// else open the dialog to confirm deletion
else {
val deleteNodesDialogFragment: DeleteNodesDialogFragment =
if (recycleBin) {
EmptyRecycleBinDialogFragment.getInstance(nodes)
} else {
DeleteNodesDialogFragment.getInstance(nodes)
}
deleteNodesDialogFragment.show(supportFragmentManager, "deleteNodesDialogFragment")
}
finishNodeAction()
}
finishNodeAction()
return true
}
@@ -1076,6 +1078,16 @@ class GroupActivity : LockingActivity(),
}
}
override fun isValidGroupName(name: String): GroupEditDialogFragment.Error {
if (name.isEmpty()) {
return GroupEditDialogFragment.Error(true, R.string.error_no_name)
}
if (mDatabase?.groupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null) {
return GroupEditDialogFragment.Error(true, R.string.error_word_reserved)
}
return GroupEditDialogFragment.Error(false, null)
}
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
groupInfo: GroupInfo) {

View File

@@ -34,7 +34,8 @@ import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database
@@ -66,7 +67,7 @@ class IconPickerActivity : LockingActivity() {
private var mDatabase: Database? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mExternalFileHelper: ExternalFileHelper? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -84,15 +85,11 @@ class IconPickerActivity : LockingActivity() {
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
mExternalFileHelper = ExternalFileHelper(this)
uploadButton = findViewById(R.id.icon_picker_upload)
if (mDatabase?.allowCustomIcons == true) {
uploadButton.setOnClickListener {
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
}
uploadButton.setOnLongClickListener {
mSelectFileHelper?.selectFileOnClickViewListener?.onLongClick(it)
true
}
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else {
uploadButton.visibility = View.GONE
}
@@ -124,8 +121,6 @@ class IconPickerActivity : LockingActivity() {
// Focus view to reinitialize timeout
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
mSelectFileHelper = SelectFileHelper(this)
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
mIconImage.standard = iconStandard
// Remove the custom icon if a standard one is selected
@@ -281,7 +276,7 @@ class IconPickerActivity : LockingActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
addCustomIcon(uri)
}
}

View File

@@ -42,10 +42,7 @@ import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.*
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
@@ -95,7 +92,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
private var mDatabaseKeyFileUri: Uri? = null
private var mRememberKeyFile: Boolean = false
private var mSelectFileHelper: SelectFileHelper? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mPermissionAsked = false
private var readOnly: Boolean = false
@@ -138,13 +135,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
keyFileSelectionView?.apply {
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
passwordView?.setOnEditorActionListener(onEditorActionListener)
passwordView?.addTextChangedListener(object : TextWatcher {
@@ -702,8 +694,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
}
var keyFileResult = false
mSelectFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
mExternalFileHelper?.let {
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data
) { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri

View File

@@ -30,13 +30,13 @@ import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.view.View
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView
@@ -60,7 +60,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private var mListener: AssignPasswordDialogListener? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
private var mNoKeyConfirmationDialog: AlertDialog? = null
@@ -133,11 +133,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mSelectFileHelper = SelectFileHelper(this)
keyFileSelectionView?.apply {
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
}
mExternalFileHelper = ExternalFileHelper(this)
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
val dialog = builder.create()
@@ -289,7 +286,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null

View File

@@ -219,14 +219,19 @@ class GroupEditDialogFragment : DialogFragment() {
}
private fun isValid(): Boolean {
if (nameTextView.text.toString().isEmpty()) {
nameTextLayoutView.error = getString(R.string.error_no_name)
return false
val error = mEditGroupListener?.isValidGroupName(nameTextView.text.toString()) ?: Error(false, null)
error.messageId?.let { messageId ->
nameTextLayoutView.error = getString(messageId)
} ?: kotlin.run {
nameTextLayoutView.error = null
}
return true
return !error.isError
}
data class Error(val isError: Boolean, val messageId: Int?)
interface EditGroupListener {
fun isValidGroupName(name: String): Error
fun approveEditGroup(action: EditGroupDialogAction,
groupInfo: GroupInfo)
fun cancelEditGroup(action: EditGroupDialogAction,

View File

@@ -311,13 +311,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
menu?.removeItem(R.id.menu_edit)
}
// Copy and Move (not for groups)
// Move
if (readOnly
|| isASearchResult) {
menu?.removeItem(R.id.menu_move)
}
// Copy (not allowed for group)
if (readOnly
|| isASearchResult
|| nodes.any { it.type == Type.GROUP }) {
// TODO Copy For Group
menu?.removeItem(R.id.menu_copy)
menu?.removeItem(R.id.menu_move)
}
// Deletion

View File

@@ -0,0 +1,254 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.helpers
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.View
import androidx.annotation.RequiresApi
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
class ExternalFileHelper {
private var activity: FragmentActivity? = null
private var fragment: Fragment? = null
constructor(context: FragmentActivity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
fun openDocument(getContent: Boolean = false,
typeString: String = "*/*") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
if (getContent) {
openActivityWithActionGetContent(typeString)
} else {
openActivityWithActionOpenDocument(typeString)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to open document", e)
showFileManagerDialogFragment()
}
} else {
showFileManagerDialogFragment()
}
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun openActivityWithActionOpenDocument(typeString: String) {
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
else
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun openActivityWithActionGetContent(typeString: String) {
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
else
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param onFileSelected Callback retrieve from data
* @return true if requestCode was captured, false elsewhere
*/
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
when (requestCode) {
FILE_BROWSE -> {
if (resultCode == RESULT_OK) {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parse(filename)
}
onFileSelected?.invoke(keyUri)
}
return true
}
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
val uri = data.data
if (uri != null) {
try {
// try to persist read and write permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity?.contentResolver?.apply {
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
} catch (e: Exception) {
// nop
}
onFileSelected?.invoke(uri)
}
}
}
return true
}
}
return false
}
/**
* Show Browser dialog to select file picker app
*/
private fun showFileManagerDialogFragment() {
try {
if (fragment != null) {
fragment?.parentFragmentManager
} else {
activity?.supportFragmentManager
}?.let { fragmentManager ->
FileManagerDialogFragment().show(fragmentManager, "browserDialog")
}
} catch (e: Exception) {
Log.e(TAG, "Can't open BrowserDialog", e)
}
}
fun createDocument(titleString: String,
typeString: String = "application/octet-stream"): Int? {
val idCode = getUnusedCreateFileRequestCode()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
putExtra(Intent.EXTRA_TITLE, titleString)
}
if (fragment != null)
fragment?.startActivityForResult(intent, idCode)
else
activity?.startActivityForResult(intent, idCode)
return idCode
} catch (e: Exception) {
Log.e(TAG, "Unable to create document", e)
showFileManagerDialogFragment()
}
} else {
showFileManagerDialogFragment()
}
return null
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param onFileCreated Callback retrieve from data
* @return true if requestCode was captured, false elsewhere
*/
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
onFileCreated: (fileCreated: Uri?)->Unit) {
// Retrieve the created URI from the file manager
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
onFileCreated.invoke(data?.data)
fileRequestCodes.remove(requestCode)
}
}
companion object {
private const val TAG = "OpenFileHelper"
private const val GET_CONTENT = 25745
private const val OPEN_DOC = 25845
private const val FILE_BROWSE = 25645
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
private var fileRequestCodes = ArrayList<Int>()
private fun getUnusedCreateFileRequestCode(): Int {
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
fileRequestCodes.add(newCreateFileRequestCode)
return newCreateFileRequestCode
}
@SuppressLint("InlinedApi")
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
typeString: String = "application/octet-stream"): Boolean {
return when {
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
}
else -> true
}
}
}
}
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
externalFileHelper?.let { fileHelper ->
setOnClickListener {
fileHelper.openDocument()
}
setOnLongClickListener {
fileHelper.openDocument(true)
true
}
} ?: kotlin.run {
setOnClickListener(null)
setOnLongClickListener(null)
}
}

View File

@@ -1,244 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.helpers
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
class SelectFileHelper {
private var activity: Activity? = null
private var fragment: Fragment? = null
val selectFileOnClickViewListener: SelectFileOnClickViewListener
get() = SelectFileOnClickViewListener()
constructor(context: Activity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
inner class SelectFileOnClickViewListener :
View.OnClickListener,
View.OnLongClickListener,
MenuItem.OnMenuItemClickListener {
private fun onAbstractClick(longClick: Boolean = false) {
try {
if (longClick) {
try {
openActivityWithActionGetContent()
} catch (e: Exception) {
openActivityWithActionOpenDocument()
}
} else {
try {
openActivityWithActionOpenDocument()
} catch (e: Exception) {
openActivityWithActionGetContent()
}
}
} catch (e: Exception) {
Log.e(TAG, "Enable to start the file picker activity", e)
// Open browser dialog
if (lookForOpenIntentsFilePicker())
showBrowserDialog()
}
}
override fun onClick(v: View) {
onAbstractClick()
}
override fun onLongClick(v: View?): Boolean {
onAbstractClick(true)
return true
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
onAbstractClick()
return true
}
}
@SuppressLint("InlinedApi")
private fun openActivityWithActionOpenDocument() {
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
else
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
}
@SuppressLint("InlinedApi")
private fun openActivityWithActionGetContent() {
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
else
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
}
private fun lookForOpenIntentsFilePicker(): Boolean {
var showBrowser = false
try {
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
if (fragment != null)
fragment?.startActivityForResult(intent, FILE_BROWSE)
else
activity?.startActivityForResult(intent, FILE_BROWSE)
} else {
showBrowser = true
}
} catch (e: Exception) {
Log.w(TAG, "Enable to start OPEN_INTENTS_FILE_BROWSE", e)
showBrowser = true
}
return showBrowser
}
/**
* Indicates whether the specified action can be used as an intent. This
* method queries the package manager for installed packages that can
* respond to an intent with the specified action. If no suitable package is
* found, this method returns false.
*
* @param context The application's environment.
* @param action The Intent action to check for availability.
*
* @return True if an Intent with the specified action can be sent and
* responded to, false otherwise.
*/
private fun isIntentAvailable(context: Context, action: String): Boolean {
val packageManager = context.packageManager
val intent = Intent(action)
val list = packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY)
return list.size > 0
}
/**
* Show Browser dialog to select file picker app
*/
private fun showBrowserDialog() {
try {
val fileManagerDialogFragment = FileManagerDialogFragment()
fragment?.let {
fileManagerDialogFragment.show(it.parentFragmentManager, "browserDialog")
} ?: fileManagerDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
} catch (e: Exception) {
Log.e(TAG, "Can't open BrowserDialog", e)
}
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param keyFileCallback Callback retrieve from data
* @return true if requestCode was captured, false elsechere
*/
fun onActivityResultCallback(
requestCode: Int,
resultCode: Int,
data: Intent?,
keyFileCallback: ((uri: Uri?) -> Unit)?): Boolean {
when (requestCode) {
FILE_BROWSE -> {
if (resultCode == RESULT_OK) {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parse(filename)
}
keyFileCallback?.invoke(keyUri)
}
return true
}
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
val uri = data.data
if (uri != null) {
try {
// try to persist read and write permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity?.contentResolver?.apply {
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
} catch (e: Exception) {
// nop
}
keyFileCallback?.invoke(uri)
}
}
}
return true
}
}
return false
}
companion object {
private const val TAG = "OpenFileHelper"
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
private const val GET_CONTENT = 25745
private const val OPEN_DOC = 25845
private const val FILE_BROWSE = 25645
}
}

View File

@@ -37,12 +37,12 @@ object Stylish {
* Initialize the class with a theme preference
* @param context Context to retrieve the theme preference
*/
fun init(context: Context) {
fun load(context: Context) {
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
themeString = PreferencesUtil.getStyle(context)
}
private fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
context.getString(R.string.list_style_brightness_light) -> false
context.getString(R.string.list_style_brightness_night) -> true
@@ -84,12 +84,16 @@ object Stylish {
}
}
fun defaultStyle(context: Context): String {
return context.getString(R.string.list_style_name_light)
}
/**
* Assign the style to the class attribute
* @param styleString Style id String
*/
fun assignStyle(context: Context, styleString: String) {
themeString = retrieveEquivalentSystemStyle(context, styleString)
PreferencesUtil.setStyle(context, styleString)
}
/**

View File

@@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
@@ -95,6 +96,12 @@ class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tint
override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) {
val icon = iconList[position]
iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon)
icon.getIconImageToDraw().custom.name.let { iconName ->
holder.iconTextView.apply {
text = iconName
visibility = if (iconName.isNotEmpty()) View.VISIBLE else View.GONE
}
}
holder.iconContainerView.isSelected = icon.selected
holder.itemView.setOnClickListener {
iconPickerListener?.onIconClickListener(icon)
@@ -117,5 +124,6 @@ class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tint
inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container)
var iconImageView: ImageView = itemView.findViewById(R.id.icon_image)
var iconTextView: TextView = itemView.findViewById(R.id.icon_name)
}
}

View File

@@ -106,7 +106,6 @@ class SearchEntryCursorAdapter(private val context: Context,
private fun getEntryFrom(cursor: Cursor): Entry? {
return database.createEntry()?.apply {
database.startManageEntry(this)
entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB,
{ standardIconId ->
@@ -127,7 +126,6 @@ class SearchEntryCursorAdapter(private val context: Context,
}
)
}
database.stopManageEntry(this)
}
}
@@ -150,12 +148,14 @@ class SearchEntryCursorAdapter(private val context: Context,
if (searchGroup != null) {
// Search in hide entries but not meta-stream
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
database.startManageEntry(entry)
entry.entryKDB?.let {
cursorKDB?.addEntry(it)
}
entry.entryKDBX?.let {
cursorKDBX?.addEntry(it)
}
database.stopManageEntry(entry)
}
}

View File

@@ -29,7 +29,7 @@ class App : MultiDexApplication() {
override fun onCreate() {
super.onCreate()
Stylish.init(this)
Stylish.load(this)
PRNGFixes.apply()
}

View File

@@ -19,10 +19,7 @@
*/
package com.kunzisoft.keepass.app.database
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.*
import android.net.Uri
import android.os.IBinder
import android.util.Log
@@ -42,66 +39,95 @@ class CipherDatabaseAction(context: Context) {
// Temp DAO to easily remove content if object no longer in memory
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
private val mIntentAdvancedUnlockService = Intent(applicationContext,
AdvancedUnlockNotificationService::class.java)
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
private var mServiceConnection: ServiceConnection? = null
private var mDatabaseListeners = LinkedList<DatabaseListener>()
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
deleteAll()
removeAllDataAndDetach()
}
fun reloadPreferences() {
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
}
@Synchronized
private fun attachService(performedAction: () -> Unit) {
// Check if a service is currently running else do nothing
if (mBinder != null) {
private fun serviceActionTask(startService: Boolean = false, performedAction: () -> Unit) {
// Check if a service is currently running else call action without info
if (startService && mServiceConnection == null) {
attachService(performedAction)
} else {
performedAction.invoke()
} else if (mServiceConnection == null) {
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
performedAction.invoke()
}
override fun onServiceDisconnected(name: ComponentName?) {
mBinder = null
mServiceConnection = null
mDatabaseListeners.forEach {
it.onDatabaseCleared()
}
}
}
applicationContext.bindService(mIntentAdvancedUnlockService,
mServiceConnection!!,
Context.BIND_ABOVE_CLIENT)
if (mBinder == null) {
try {
applicationContext.startService(mIntentAdvancedUnlockService)
} catch (e: Exception) {
Log.e(TAG, "Unable to start cipher action", e)
}
}
}
}
fun registerDatabaseListener(listener: DatabaseListener) {
mDatabaseListeners.add(listener)
@Synchronized
private fun attachService(performedAction: () -> Unit) {
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
})
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
performedAction.invoke()
}
override fun onServiceDisconnected(name: ComponentName?) {
onClear()
}
}
try {
AdvancedUnlockNotificationService.bindService(applicationContext,
mServiceConnection!!,
Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
Log.e(TAG, "Unable to start cipher action", e)
performedAction.invoke()
}
}
fun unregisterDatabaseListener(listener: DatabaseListener) {
mDatabaseListeners.remove(listener)
@Synchronized
private fun detachService() {
try {
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
} catch (e: Exception) {}
mServiceConnection?.let {
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
}
}
interface DatabaseListener {
fun onDatabaseCleared()
private fun removeAllDataAndDetach() {
detachService()
onClear()
}
fun registerDatabaseListener(listenerCipher: CipherDatabaseListener) {
mDatabaseListeners.add(listenerCipher)
}
fun unregisterDatabaseListener(listenerCipher: CipherDatabaseListener) {
mDatabaseListeners.remove(listenerCipher)
}
private fun onClear() {
mBinder = null
mServiceConnection = null
mDatabaseListeners.forEach {
it.onCipherDatabaseCleared()
}
}
interface CipherDatabaseListener {
fun onCipherDatabaseCleared()
}
fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
if (useTempDao) {
attachService {
serviceActionTask {
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
}
} else {
@@ -126,7 +152,8 @@ class CipherDatabaseAction(context: Context) {
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) {
attachService {
// The only case to create service (not needed to get an info)
serviceActionTask(true) {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke()
}
@@ -151,7 +178,7 @@ class CipherDatabaseAction(context: Context) {
fun deleteByDatabaseUri(databaseUri: Uri,
cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) {
attachService {
serviceActionTask {
mBinder?.deleteByDatabaseUri(databaseUri)
cipherDatabaseResultListener?.invoke()
}
@@ -168,14 +195,19 @@ class CipherDatabaseAction(context: Context) {
}
fun deleteAll() {
attachService {
mBinder?.deleteAll()
if (useTempDao) {
serviceActionTask {
mBinder?.deleteAll()
}
}
// To erase the residues
IOActionTask(
{
cipherDatabaseDao.deleteAll()
}
).execute()
// Unbind
removeAllDataAndDetach()
}
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {

View File

@@ -223,9 +223,22 @@ class StructureParser(private val structure: AssistStructure) {
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
// Some forms used visible password as username
if (usernameCandidate == null && usernameValueCandidate == null) {
usernameCandidate = autofillId
usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
} else if (result?.passwordId == null && result?.passwordValue == null) {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
usernameNeeded = false
}
}
inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
result?.passwordId = autofillId
result?.passwordValue = node.autofillValue

View File

@@ -36,7 +36,6 @@ 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.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
@@ -68,7 +67,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private lateinit var cipherDatabaseAction : CipherDatabaseAction
private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
@@ -125,8 +124,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
override fun onResume() {
super.onResume()
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext())
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())
context?.let {
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it)
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
}
keepConnection = false
}
@@ -176,34 +177,36 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
* 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)
context?.let { context ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true
if (PreferencesUtil.isBiometricUnlockEnable(context)) {
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)
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context)
if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else {
selectMode()
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
} else {
selectMode()
}
}
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
if (AdvancedUnlockManager.isDeviceSecure(context)) {
selectMode()
} else {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
}
}
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(requireContext())) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
if (AdvancedUnlockManager.isDeviceSecure(requireContext())) {
selectMode()
} else {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
}
}
}
@@ -261,7 +264,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private fun openBiometricSetting() {
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
requireContext().startActivity(Intent(Settings.ACTION_SETTINGS))
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
@@ -296,9 +299,11 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
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))
context?.let { context ->
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
context.getString(R.string.credential_before_click_advanced_unlock_button))
}
}
}
@@ -402,9 +407,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
fun connect(databaseUri: Uri) {
showViews(true)
this.databaseFileUri = databaseUri
cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
override fun onDatabaseCleared() {
deleteEncryptedDatabaseKey()
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
override fun onCipherDatabaseCleared() {
advancedUnlockManager?.closeBiometricPrompt()
checkUnlockAvailability()
}
}
cipherDatabaseAction.apply {
@@ -435,14 +441,12 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() {
allowOpenBiometricPrompt = false
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
checkUnlockAvailability()
}
}
} ?: checkUnlockAvailability()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
@@ -479,7 +483,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
advancedUnlockManager?.encryptData(credential)
}
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
}
Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences

View File

@@ -31,41 +31,47 @@ class DeleteNodesRunnable(context: Context,
afterActionNodesFinish: AfterActionNodesFinish)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
private var mParent: Group? = null
private var mOldParent: Group? = null
private var mCanRecycle: Boolean = false
private var mNodesToDeleteBackup = ArrayList<Node>()
override fun nodeAction() {
foreachNode@ for(currentNode in mNodesToDelete) {
mParent = currentNode.parent
mParent?.touch(modified = false, touchParents = true)
foreachNode@ for(nodeToDelete in mNodesToDelete) {
mOldParent = nodeToDelete.parent
mOldParent?.touch(modified = false, touchParents = true)
when (currentNode.type) {
when (nodeToDelete.type) {
Type.GROUP -> {
val groupToDelete = nodeToDelete as Group
// Create a copy to keep the old ref and remove it visually
mNodesToDeleteBackup.add(Group(currentNode as Group))
mNodesToDeleteBackup.add(Group(groupToDelete))
// Remove Node from parent
mCanRecycle = database.canRecycle(currentNode)
mCanRecycle = database.canRecycle(groupToDelete)
if (mCanRecycle) {
database.recycle(currentNode, context.resources)
groupToDelete.touch(modified = false, touchParents = true)
database.recycle(groupToDelete, context.resources)
groupToDelete.setPreviousParentGroup(mOldParent)
} else {
database.deleteGroup(currentNode)
database.deleteGroup(groupToDelete)
}
}
Type.ENTRY -> {
val entryToDelete = nodeToDelete as Entry
// Create a copy to keep the old ref and remove it visually
mNodesToDeleteBackup.add(Entry(currentNode as Entry))
mNodesToDeleteBackup.add(Entry(entryToDelete))
// Remove Node from parent
mCanRecycle = database.canRecycle(currentNode)
mCanRecycle = database.canRecycle(entryToDelete)
if (mCanRecycle) {
database.recycle(currentNode, context.resources)
entryToDelete.touch(modified = false, touchParents = true)
database.recycle(entryToDelete, context.resources)
entryToDelete.setPreviousParentGroup(mOldParent)
} else {
database.deleteEntry(currentNode)
database.deleteEntry(entryToDelete)
}
// Remove the oldest attachments
currentNode.getAttachments(database.attachmentPool).forEach {
entryToDelete.getAttachments(database.attachmentPool).forEach {
database.removeAttachmentIfNotUsed(it)
}
}
@@ -76,7 +82,7 @@ class DeleteNodesRunnable(context: Context,
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
if (mCanRecycle) {
mParent?.let {
mOldParent?.let {
mNodesToDeleteBackup.forEach { backupNode ->
when (backupNode.type) {
Type.GROUP -> {

View File

@@ -24,7 +24,7 @@ import android.util.Log
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.EntryDatabaseException
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
class MoveNodesRunnable constructor(
@@ -47,11 +47,14 @@ class MoveNodesRunnable constructor(
when (nodeToMove.type) {
Type.GROUP -> {
val groupToMove = nodeToMove as Group
// Move group in new parent if not in the current group
if (groupToMove != mNewParent
// Move group if the parent change
if (mOldParent != mNewParent
// and if not in the current group
&& groupToMove != mNewParent
&& !mNewParent.isContainedIn(groupToMove)) {
nodeToMove.touch(modified = true, touchParents = true)
groupToMove.touch(modified = true, touchParents = true)
database.moveGroupTo(groupToMove, mNewParent)
groupToMove.setPreviousParentGroup(mOldParent)
} else {
// Only finish thread
setError(MoveGroupDatabaseException())
@@ -64,11 +67,12 @@ class MoveNodesRunnable constructor(
if (mOldParent != mNewParent
// and root can contains entry
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
nodeToMove.touch(modified = true, touchParents = true)
entryToMove.touch(modified = true, touchParents = true)
database.moveEntryTo(entryToMove, mNewParent)
entryToMove.setPreviousParentGroup(mOldParent)
} else {
// Only finish thread
setError(EntryDatabaseException())
setError(MoveEntryDatabaseException())
break@foreachNode
}
}

View File

@@ -36,6 +36,9 @@ abstract class CipherEngine {
return 16
}
// Used only with padding workaround
var forcePaddingCompatibility = false
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher

View File

@@ -30,7 +30,7 @@ class TwofishEngine : CipherEngine() {
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
return CipherFactory.getTwofish(opmode, key, IV)
return CipherFactory.getTwofish(opmode, key, IV, forcePaddingCompatibility)
}
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {

View File

@@ -0,0 +1,66 @@
package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.utils.ParcelableUtil
import java.util.*
class CustomData : Parcelable {
private val mCustomDataItems = HashMap<String, CustomDataItem>()
constructor()
constructor(toCopy: CustomData) {
mCustomDataItems.clear()
mCustomDataItems.putAll(toCopy.mCustomDataItems)
}
constructor(parcel: Parcel) {
ParcelableUtil.readStringParcelableMap(parcel, CustomDataItem::class.java)
}
fun get(key: String): CustomDataItem? {
return mCustomDataItems[key]
}
fun put(customDataItem: CustomDataItem) {
mCustomDataItems[customDataItem.key] = customDataItem
}
fun containsItemWithValue(value: String): Boolean {
return mCustomDataItems.any { mapEntry -> mapEntry.value.value.equals(value, true) }
}
fun containsItemWithLastModificationTime(): Boolean {
return mCustomDataItems.any { mapEntry -> mapEntry.value.lastModificationTime != null }
}
fun isNotEmpty(): Boolean {
return mCustomDataItems.isNotEmpty()
}
fun doForEachItems(action: (CustomDataItem) -> Unit) {
for ((_, value) in mCustomDataItems) {
action.invoke(value)
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CustomData> {
override fun createFromParcel(parcel: Parcel): CustomData {
return CustomData(parcel)
}
override fun newArray(size: Int): Array<CustomData?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,43 @@
package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
class CustomDataItem : Parcelable {
val key: String
var value: String
var lastModificationTime: DateInstant? = null
constructor(parcel: Parcel) {
key = parcel.readString() ?: ""
value = parcel.readString() ?: ""
lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader)
}
constructor(key: String, value: String, lastModificationTime: DateInstant? = null) {
this.key = key
this.value = value
this.lastModificationTime = lastModificationTime
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(key)
parcel.writeString(value)
parcel.writeParcelable(lastModificationTime, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CustomDataItem> {
override fun createFromParcel(parcel: Parcel): CustomDataItem {
return CustomDataItem(parcel)
}
override fun newArray(size: Int): Array<CustomDataItem?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -23,7 +23,6 @@ import android.content.ContentResolver
import android.content.res.Resources
import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -43,7 +42,7 @@ import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
@@ -55,6 +54,7 @@ import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import java.io.*
import java.util.*
import kotlin.collections.ArrayList
@@ -105,10 +105,6 @@ class Database {
return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache()
}
fun setCacheDirectory(cacheDirectory: File) {
binaryCache.cacheDirectory = cacheDirectory
}
private val iconsManager: IconsManager
get() {
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
@@ -226,7 +222,7 @@ class Database {
// Default compression not necessary if stored in header
mDatabaseKDBX?.let {
return it.compressionAlgorithm == CompressionAlgorithm.GZip
&& it.kdbxVersion.isBefore(FILE_VERSION_32_4)
&& it.kdbxVersion.isBefore(FILE_VERSION_40)
}
return false
}
@@ -363,15 +359,10 @@ class Database {
return null
}
fun ensureRecycleBinExists(resources: Resources) {
mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources)
}
fun removeRecycleBin() {
// Don't allow remove backup in KDB
mDatabaseKDBX?.removeRecycleBin()
}
val groupNamesNotAllowed: List<String>
get() {
return mDatabaseKDB?.groupNamesNotAllowed ?: ArrayList()
}
private fun setDatabaseKDB(databaseKDB: DatabaseKDB) {
this.mDatabaseKDB = databaseKDB
@@ -546,25 +537,27 @@ class Database {
omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
searchQuery, SearchParameters(), omitBackup, max)
SearchParameters().apply {
this.searchQuery = searchQuery
}, omitBackup, max)
}
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
searchInfoString, SearchParameters().apply {
searchInTitles = true
searchInUserNames = false
searchInPasswords = false
searchInUrls = true
searchInNotes = true
searchInOTP = false
searchInOther = true
searchInUUIDs = false
searchInTags = false
ignoreCase = true
}, omitBackup, max)
SearchParameters().apply {
searchQuery = searchInfoString
searchInTitles = true
searchInUserNames = false
searchInPasswords = false
searchInUrls = true
searchInNotes = true
searchInOTP = false
searchInOther = true
searchInUUIDs = false
searchInTags = false
}, omitBackup, max)
}
val attachmentPool: AttachmentPool
@@ -794,11 +787,11 @@ class Database {
}
fun addGroupTo(group: Group, parent: Group) {
group.groupKDB?.let { entryKDB ->
mDatabaseKDB?.addGroupTo(entryKDB, parent.groupKDB)
group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
}
group.groupKDBX?.let { entryKDBX ->
mDatabaseKDBX?.addGroupTo(entryKDBX, parent.groupKDBX)
group.groupKDBX?.let { groupKDBX ->
mDatabaseKDBX?.addGroupTo(groupKDBX, parent.groupKDBX)
}
group.afterAssignNewParent()
}
@@ -813,11 +806,11 @@ class Database {
}
fun removeGroupFrom(group: Group, parent: Group) {
group.groupKDB?.let { entryKDB ->
mDatabaseKDB?.removeGroupFrom(entryKDB, parent.groupKDB)
group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
}
group.groupKDBX?.let { entryKDBX ->
mDatabaseKDBX?.removeGroupFrom(entryKDBX, parent.groupKDBX)
group.groupKDBX?.let { groupKDBX ->
mDatabaseKDBX?.removeGroupFrom(groupKDBX, parent.groupKDBX)
}
group.afterAssignNewParent()
}
@@ -892,6 +885,16 @@ class Database {
}
}
fun ensureRecycleBinExists(resources: Resources) {
mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources)
}
fun removeRecycleBin() {
// Don't allow remove backup in KDB
mDatabaseKDBX?.removeRecycleBin()
}
fun canRecycle(entry: Entry): Boolean {
var canRecycle: Boolean? = null
entry.entryKDB?.let {

View File

@@ -55,7 +55,7 @@ class DateInstant : Parcelable {
jDate = Date()
}
protected constructor(parcel: Parcel) {
constructor(parcel: Parcel) {
jDate = parcel.readSerializable() as Date
}

View File

@@ -19,30 +19,37 @@
*/
package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import java.util.Date
import java.util.UUID
import java.util.*
class DeletedObject {
class DeletedObject : Parcelable {
var uuid: UUID = DatabaseVersioned.UUID_ZERO
private var mDeletionTime: Date? = null
private var mDeletionTime: DateInstant? = null
fun getDeletionTime(): Date {
constructor()
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
this.uuid = uuid
this.mDeletionTime = deletionTime
}
constructor(parcel: Parcel) {
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader)
}
fun getDeletionTime(): DateInstant {
if (mDeletionTime == null) {
mDeletionTime = Date(System.currentTimeMillis())
mDeletionTime = DateInstant(System.currentTimeMillis())
}
return mDeletionTime!!
}
fun setDeletionTime(deletionTime: Date) {
this.mDeletionTime = deletionTime
}
constructor()
constructor(uuid: UUID, deletionTime: Date = Date()) {
this.uuid = uuid
fun setDeletionTime(deletionTime: DateInstant) {
this.mDeletionTime = deletionTime
}
@@ -59,4 +66,23 @@ class DeletedObject {
override fun hashCode(): Int {
return uuid.hashCode()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(ParcelUuid(uuid), flags)
parcel.writeParcelable(mDeletionTime, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<DeletedObject> {
override fun createFromParcel(parcel: Parcel): DeletedObject {
return DeletedObject(parcel)
}
override fun newArray(size: Int): Array<DeletedObject?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -23,6 +23,7 @@ import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
@@ -114,6 +115,20 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.icon = value
}
var tags: Tags
get() = entryKDBX?.tags ?: Tags()
set(value) {
entryKDBX?.tags = value
}
var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
get() = entryKDBX?.previousParentGroup ?: DatabaseVersioned.UUID_ZERO
private set
fun setPreviousParentGroup(previousParent: Group?) {
entryKDBX?.previousParentGroup = previousParent?.groupKDBX?.id ?: DatabaseVersioned.UUID_ZERO
}
override val type: Type
get() = Type.ENTRY
@@ -373,10 +388,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
return entryKDBX?.getSize(attachmentPool) ?: 0L
}
fun containsCustomData(): Boolean {
return entryKDBX?.containsCustomData() ?: false
}
/*
------------
Converter
@@ -466,16 +477,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
return result
}
companion object CREATOR : Parcelable.Creator<Entry> {
override fun createFromParcel(parcel: Parcel): Entry {
return Entry(parcel)
}
override fun newArray(size: Int): Array<Entry?> {
return arrayOfNulls(size)
}
companion object {
const val PMS_TAN_ENTRY = "<TAN>"
/**
@@ -484,5 +486,16 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun newExtraFieldNameAllowed(field: Field): Boolean {
return EntryKDBX.newCustomNameAllowed(field.name)
}
@JvmField
val CREATOR: Parcelable.Creator<Entry> = object : Parcelable.Creator<Entry> {
override fun createFromParcel(parcel: Parcel): Entry {
return Entry(parcel)
}
override fun newArray(size: Int): Array<Entry?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.element
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
@@ -134,6 +135,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.icon = value
}
var tags: Tags
get() = groupKDBX?.tags ?: Tags()
set(value) {
groupKDBX?.tags = value
}
var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
get() = groupKDBX?.previousParentGroup ?: DatabaseVersioned.UUID_ZERO
private set
fun setPreviousParentGroup(previousParent: Group?) {
groupKDBX?.previousParentGroup = previousParent?.groupKDBX?.id ?: DatabaseVersioned.UUID_ZERO
}
override val type: Type
get() = Type.GROUP
@@ -368,14 +383,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDB?.nodeId = id
}
fun getLevel(): Int {
return groupKDB?.level ?: -1
}
fun setLevel(level: Int) {
groupKDB?.level = level
}
/*
------------
KDBX Methods
@@ -402,10 +409,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.isExpanded = expanded
}
fun containsCustomData(): Boolean {
return groupKDBX?.containsCustomData() ?: false
}
/*
------------
Converter

View File

@@ -0,0 +1,45 @@
package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
class Tags: Parcelable {
private val mTags = ArrayList<String>()
constructor()
constructor(values: String): this() {
mTags.addAll(values.split(';'))
}
constructor(parcel: Parcel) : this() {
parcel.readStringList(mTags)
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeStringList(mTags)
}
override fun describeContents(): Int {
return 0
}
fun isEmpty(): Boolean {
return mTags.isEmpty()
}
override fun toString(): String {
return mTags.joinToString(";")
}
companion object CREATOR : Parcelable.Creator<Tags> {
override fun createFromParcel(parcel: Parcel): Tags {
return Tags(parcel)
}
override fun newArray(size: Int): Array<Tags?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -11,7 +11,7 @@ class BinaryCache {
*/
var loadedCipherKey: LoadedKey = LoadedKey.generateNewCipherKey()
lateinit var cacheDirectory: File
var cacheDirectory: File? = null
private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0))
@@ -19,11 +19,12 @@ class BinaryCache {
smallSize: Boolean = false,
compression: Boolean = false,
protection: Boolean = false): BinaryData {
return if (smallSize) {
val cacheDir = cacheDirectory
return if (smallSize || cacheDir == null) {
BinaryByte(binaryId, compression, protection)
} else {
val fileInCache = File(cacheDirectory, binaryId)
return BinaryFile(fileInCache, compression, protection)
val fileInCache = File(cacheDir, binaryId)
BinaryFile(fileInCache, compression, protection)
}
}

View File

@@ -1,8 +1,27 @@
package com.kunzisoft.keepass.database.element.binary
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import java.util.*
class CustomIconPool(binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
private val customIcons = HashMap<UUID, IconImageCustom>()
fun put(key: UUID? = null,
name: String,
lastModificationTime: DateInstant?,
smallSize: Boolean,
result: (IconImageCustom, BinaryData?) -> Unit) {
val keyBinary = super.put(key) { uniqueBinaryId ->
// Create a byte array for better performance with small data
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
}
val uuid = keyBinary.keys.first()
val customIcon = IconImageCustom(uuid, name, lastModificationTime)
customIcons[uuid] = customIcon
result.invoke(customIcon, keyBinary.binary)
}
override fun findUnusedKey(): UUID {
var newUUID = UUID.randomUUID()
@@ -11,4 +30,14 @@ class CustomIconPool(binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
}
return newUUID
}
fun any(predicate: (IconImageCustom)-> Boolean): Boolean {
return customIcons.any { predicate(it.value) }
}
fun doForEachCustomIcon(action: (customIcon: IconImageCustom, binary: BinaryData) -> Unit) {
doForEachBinary { key, binary ->
action.invoke(customIcons[key] ?: IconImageCustom(key), binary)
}
}
}

View File

@@ -38,8 +38,6 @@ import kotlin.collections.ArrayList
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
override val version: String
@@ -55,13 +53,14 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return getGroupById(NodeIdInt(groupId))
}
// Retrieve backup group in index
val backupGroup: GroupKDB?
get() {
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
null
else
getGroupById(backupGroupId)
return retrieveBackup()
}
val groupNamesNotAllowed: List<String>
get() {
return listOf(BACKUP_FOLDER_TITLE)
}
override val kdfEngine: KdfEngine
@@ -80,12 +79,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
val rootGroups: List<GroupKDB>
get() {
val kids = ArrayList<GroupKDB>()
doForEachGroupInIndex { group ->
if (group.level == 0)
kids.add(group)
}
return kids
return rootGroup?.getChildGroups() ?: ArrayList()
}
override val passwordEncoding: String
@@ -163,27 +157,16 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return this.iconsManager.getIcon(iconId)
}
override fun containsCustomData(): Boolean {
return false
}
override fun isInRecycleBin(group: GroupKDB): Boolean {
var currentGroup: GroupKDB? = group
val currentBackupGroup = backupGroup ?: return false
// Init backup group variable
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
findBackupGroupId()
if (backupGroup == null)
return false
if (currentGroup == backupGroup)
if (currentGroup == currentBackupGroup)
return true
val backupGroupId = currentBackupGroup.id
while (currentGroup != null) {
if (currentGroup.level == 0
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
backupGroupId = currentGroup.id
if (backupGroupId == currentGroup.id) {
return true
}
currentGroup = currentGroup.parent
@@ -191,12 +174,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return false
}
private fun findBackupGroupId() {
rootGroups.forEach { currentGroup ->
if (currentGroup.level == 0
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
backupGroupId = currentGroup.id
}
/**
* Retrieve backup group with his name
*/
private fun retrieveBackup(): GroupKDB? {
return rootGroup?.searchChildGroup {
it.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)
}
}
@@ -205,8 +188,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
* if it doesn't exist
*/
fun ensureBackupExists() {
findBackupGroupId()
if (backupGroup == null) {
// Create recycle bin
val recycleBinGroup = createGroup().apply {
@@ -214,7 +195,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
}
addGroupTo(recycleBinGroup, rootGroup)
backupGroupId = recycleBinGroup.id
}
}
@@ -268,6 +248,5 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
val TYPE = DatabaseKDB::class.java
const val BACKUP_FOLDER_TITLE = "Backup"
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
}
}

View File

@@ -23,8 +23,6 @@ import android.content.res.Resources
import android.util.Base64
import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.longTo8Bytes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.AesEngine
@@ -34,11 +32,13 @@ import com.kunzisoft.keepass.database.crypto.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
@@ -46,10 +46,13 @@ import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
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_4
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
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.longTo8Bytes
import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node
import java.io.IOException
@@ -75,6 +78,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private var kdfList: MutableList<KdfEngine> = ArrayList()
private var numKeyEncRounds: Long = 0
var publicCustomData = VariantDictionary()
private val mFieldReferenceEngine = FieldReferencesEngine(this)
var kdbxVersion = UnsignedInt(0)
var name = ""
@@ -100,7 +104,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
*/
var isRecycleBinEnabled = true
var recycleBinUUID: UUID = UUID_ZERO
var recycleBinChanged = Date()
var recycleBinChanged = DateInstant()
var entryTemplatesGroup = UUID_ZERO
var entryTemplatesGroupChanged = DateInstant()
var historyMaxItems = DEFAULT_HISTORY_MAX_ITEMS
@@ -109,7 +113,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var lastTopVisibleGroupUUID = UUID_ZERO
var memoryProtection = MemoryProtectionConfig()
val deletedObjects = ArrayList<DeletedObject>()
val customData = HashMap<String, String>()
val customData = CustomData()
var localizedAppName = "KeePassDX"
@@ -126,20 +130,20 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
*/
constructor(databaseName: String, rootName: String) {
name = databaseName
kdbxVersion = FILE_VERSION_32_3
kdbxVersion = FILE_VERSION_31
val group = createGroup().apply {
title = rootName
icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
}
rootGroup = group
addGroupIndex(group)
}
override val version: String
get() {
val kdbxStringVersion = when(kdbxVersion) {
FILE_VERSION_32_3 -> "3.1"
FILE_VERSION_32_4 -> "4.0"
FILE_VERSION_31 -> "3.1"
FILE_VERSION_40 -> "4.0"
FILE_VERSION_41 -> "4.1"
else -> "UNKNOWN"
}
return "KeePass 2 - KDBX$kdbxStringVersion"
@@ -187,7 +191,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
CompressionAlgorithm.GZip -> {
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
if (kdbxVersion.isBefore(FILE_VERSION_32_4)) {
if (kdbxVersion.isBefore(FILE_VERSION_40)) {
compressAllBinaries()
}
}
@@ -195,7 +199,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
CompressionAlgorithm.GZip -> {
// In databaseV4 the header is zipped during the save, so not necessary here
if (kdbxVersion.isBefore(FILE_VERSION_32_4)) {
if (kdbxVersion.isBefore(FILE_VERSION_40)) {
when (newCompression) {
CompressionAlgorithm.None -> {
decompressAllBinaries()
@@ -313,9 +317,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
fun addCustomIcon(customIconId: UUID? = null,
name: String,
lastModificationTime: DateInstant?,
smallSize: Boolean,
result: (IconImageCustom, BinaryData?) -> Unit) {
iconsManager.addCustomIcon(customIconId, smallSize, result)
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, smallSize, result)
}
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
@@ -326,12 +332,20 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return this.iconsManager.getIcon(iconUuid)
}
fun putCustomData(label: String, value: String) {
this.customData[label] = value
/**
* To perform a search in entry custom data
*/
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
return entryIndexes.values.find { entry ->
entry.customData.containsItemWithValue(customDataValue)
}
}
override fun containsCustomData(): Boolean {
return customData.isNotEmpty()
/**
* Retrieve the value of a field reference
*/
fun getFieldReferenceValue(textReference: String, recursionLevel: Int): String {
return mFieldReferenceEngine.compile(textReference, recursionLevel)
}
@Throws(IOException::class)
@@ -594,14 +608,14 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
addGroupTo(recycleBinGroup, rootGroup)
recycleBinUUID = recycleBinGroup.id
recycleBinChanged = recycleBinGroup.lastModificationTime.date
recycleBinChanged = recycleBinGroup.lastModificationTime
}
}
fun removeRecycleBin() {
if (recycleBin != null) {
recycleBinUUID = UUID_ZERO
recycleBinChanged = DateInstant().date
recycleBinChanged = DateInstant()
}
}
@@ -615,6 +629,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return false
if (recycleBin == null)
return false
if (node is GroupKDBX
&& recycleBin!!.isContainedIn(node))
return false
if (!node.isContainedIn(recycleBin!!))
return true
return false
@@ -652,9 +669,20 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
this.deletedObjects.add(deletedObject)
}
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
super.addEntryTo(newEntry, parent)
mFieldReferenceEngine.clear()
}
override fun updateEntry(entry: EntryKDBX) {
super.updateEntry(entry)
mFieldReferenceEngine.clear()
}
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
super.removeEntryFrom(entryToRemove, parent)
deletedObjects.add(DeletedObject(entryToRemove.id))
mFieldReferenceEngine.clear()
}
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
@@ -725,6 +753,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
override fun clearCache() {
try {
super.clearCache()
mFieldReferenceEngine.clear()
attachmentPool.clear()
} catch (e: Exception) {
Log.e(TAG, "Unable to clear cache", e)

View File

@@ -35,7 +35,6 @@ import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.security.MessageDigest
import java.util.*
abstract class DatabaseVersioned<
@@ -68,7 +67,7 @@ abstract class DatabaseVersioned<
var changeDuplicateId = false
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
abstract val version: String
@@ -87,6 +86,12 @@ abstract class DatabaseVersioned<
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
var rootGroup: Group? = null
set(value) {
field = value
value?.let {
addGroupIndex(it)
}
}
@Throws(IOException::class)
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@@ -266,6 +271,26 @@ abstract class DatabaseVersioned<
return this.entryIndexes[id]
}
fun getEntryByTitle(title: String): Entry? {
return this.entryIndexes.values.find { entry -> entry.title.equals(title, true) }
}
fun getEntryByUsername(username: String): Entry? {
return this.entryIndexes.values.find { entry -> entry.username.equals(username, true) }
}
fun getEntryByURL(url: String): Entry? {
return this.entryIndexes.values.find { entry -> entry.url.equals(url, true) }
}
fun getEntryByPassword(password: String): Entry? {
return this.entryIndexes.values.find { entry -> entry.password.equals(password, true) }
}
fun getEntryByNotes(notes: String): Entry? {
return this.entryIndexes.values.find { entry -> entry.notes.equals(notes, true) }
}
fun addEntryIndex(entry: Entry) {
val entryId = entry.nodeId
if (entryIndexes.containsKey(entryId)) {
@@ -312,8 +337,6 @@ abstract class DatabaseVersioned<
abstract fun getStandardIcon(iconId: Int): IconImageStandard
abstract fun containsCustomData(): Boolean
fun addGroupTo(newGroup: Group, parent: Group?) {
// Add tree to parent tree
parent?.addChildGroup(newGroup)
@@ -331,14 +354,14 @@ abstract class DatabaseVersioned<
removeGroupIndex(groupToRemove)
}
fun addEntryTo(newEntry: Entry, parent: Group?) {
open fun addEntryTo(newEntry: Entry, parent: Group?) {
// Add entry to parent
parent?.addChildEntry(newEntry)
newEntry.parent = parent
addEntryIndex(newEntry)
}
fun updateEntry(entry: Entry) {
open fun updateEntry(entry: Entry) {
updateEntryIndex(entry)
}

View File

@@ -90,7 +90,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
url = parcel.readString() ?: url
notes = parcel.readString() ?: notes
binaryDescription = parcel.readString() ?: binaryDescription
binaryDataId = parcel.readInt()
val rawBinaryDataId = parcel.readInt()
binaryDataId = if (rawBinaryDataId == -1) null else rawBinaryDataId
}
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
@@ -109,9 +110,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
dest.writeString(url)
dest.writeString(notes)
dest.writeString(binaryDescription)
binaryDataId?.let {
dest.writeInt(it)
}
dest.writeInt(binaryDataId ?: -1)
}
fun updateWith(source: EntryKDB) {

View File

@@ -20,12 +20,15 @@
package com.kunzisoft.keepass.database.element.entry
import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.utils.UnsignedLong
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
@@ -33,6 +36,7 @@ import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashMap
@@ -45,16 +49,183 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
@Transient
private var mDecodeRef = false
var customData = LinkedHashMap<String, String>()
override var usageCount = UnsignedLong(0)
override var locationChanged = DateInstant()
override var customData = CustomData()
var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
var foregroundColor = ""
var backgroundColor = ""
var overrideURL = ""
override var tags = Tags()
override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
var qualityCheck = true
var autoType = AutoType()
var history = ArrayList<EntryKDBX>()
var additional = ""
var tags = ""
override var expires: Boolean = false
constructor() : super()
constructor(parcel: Parcel) : super(parcel) {
usageCount = UnsignedLong(parcel.readLong())
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData()
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
binaries = ParcelableUtil.readStringIntMap(parcel)
foregroundColor = parcel.readString() ?: foregroundColor
backgroundColor = parcel.readString() ?: backgroundColor
overrideURL = parcel.readString() ?: overrideURL
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType
parcel.readTypedList(history, CREATOR)
url = parcel.readString() ?: url
additional = parcel.readString() ?: additional
}
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
return parcel.readParcelable(GroupKDBX::class.java.classLoader)
}
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
parcel.writeParcelable(parent, flags)
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags)
dest.writeParcelable(customData, flags)
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
ParcelableUtil.writeStringIntMap(dest, binaries)
dest.writeString(foregroundColor)
dest.writeString(backgroundColor)
dest.writeString(overrideURL)
dest.writeParcelable(tags, flags)
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
dest.writeParcelable(autoType, flags)
dest.writeTypedList(history)
dest.writeString(url)
dest.writeString(additional)
}
/**
* Update with deep copy of each entry element
* @param source
*/
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
super.updateWith(source)
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
customData = CustomData(source.customData)
fields.clear()
fields.putAll(source.fields)
binaries.clear()
binaries.putAll(source.binaries)
foregroundColor = source.foregroundColor
backgroundColor = source.backgroundColor
overrideURL = source.overrideURL
tags = source.tags
previousParentGroup = source.previousParentGroup
autoType = AutoType(source.autoType)
history.clear()
if (copyHistory)
history.addAll(source.history)
url = source.url
additional = source.additional
}
fun startToManageFieldReferences(database: DatabaseKDBX) {
this.mDatabase = database
this.mDecodeRef = true
}
fun stopToManageFieldReferences() {
this.mDatabase = null
this.mDecodeRef = false
}
override fun initNodeId(): NodeId<UUID> {
return NodeIdUUID()
}
override fun copyNodeId(nodeId: NodeId<UUID>): NodeId<UUID> {
return NodeIdUUID(nodeId.id)
}
override val type: Type
get() = Type.ENTRY
/**
* Decode a reference key with the FieldReferencesEngine
* @param decodeRef
* @param key
* @return
*/
private fun decodeRefKey(decodeRef: Boolean, key: String, recursionLevel: Int): String {
return fields[key]?.toString()?.let { text ->
return if (decodeRef) {
mDatabase?.getFieldReferenceValue(text, recursionLevel) ?: text
} else text
} ?: ""
}
fun decodeTitleKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_TITLE, recursionLevel)
}
override var title: String
get() = decodeTitleKey(0)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectTitle
fields[STR_TITLE] = ProtectedString(protect, value)
}
fun decodeUsernameKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_USERNAME, recursionLevel)
}
override var username: String
get() = decodeUsernameKey(0)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUserName
fields[STR_USERNAME] = ProtectedString(protect, value)
}
fun decodePasswordKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_PASSWORD, recursionLevel)
}
override var password: String
get() = decodePasswordKey(0)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectPassword
fields[STR_PASSWORD] = ProtectedString(protect, value)
}
fun decodeUrlKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_URL, recursionLevel)
}
override var url
get() = decodeUrlKey(0)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUrl
fields[STR_URL] = ProtectedString(protect, value)
}
fun decodeNotesKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_NOTES, recursionLevel)
}
override var notes: String
get() = decodeNotesKey(0)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectNotes
fields[STR_NOTES] = ProtectedString(protect, value)
}
fun getSize(attachmentPool: AttachmentPool): Long {
var size = FIXED_LENGTH_SIZE
@@ -77,157 +248,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
}
size += overrideURL.length.toLong()
size += tags.length.toLong()
size += tags.toString().length
return size
}
override var expires: Boolean = false
constructor() : super()
constructor(parcel: Parcel) : super(parcel) {
usageCount = UnsignedLong(parcel.readLong())
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
customData = ParcelableUtil.readStringParcelableMap(parcel)
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
binaries = ParcelableUtil.readStringIntMap(parcel)
foregroundColor = parcel.readString() ?: foregroundColor
backgroundColor = parcel.readString() ?: backgroundColor
overrideURL = parcel.readString() ?: overrideURL
autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType
parcel.readTypedList(history, CREATOR)
url = parcel.readString() ?: url
additional = parcel.readString() ?: additional
tags = parcel.readString() ?: tags
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags)
ParcelableUtil.writeStringParcelableMap(dest, customData)
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
ParcelableUtil.writeStringIntMap(dest, binaries)
dest.writeString(foregroundColor)
dest.writeString(backgroundColor)
dest.writeString(overrideURL)
dest.writeParcelable(autoType, flags)
dest.writeTypedList(history)
dest.writeString(url)
dest.writeString(additional)
dest.writeString(tags)
}
/**
* Update with deep copy of each entry element
* @param source
*/
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
super.updateWith(source)
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map
customData.clear()
customData.putAll(source.customData)
fields.clear()
fields.putAll(source.fields)
binaries.clear()
binaries.putAll(source.binaries)
foregroundColor = source.foregroundColor
backgroundColor = source.backgroundColor
overrideURL = source.overrideURL
autoType = AutoType(source.autoType)
history.clear()
if (copyHistory)
history.addAll(source.history)
url = source.url
additional = source.additional
tags = source.tags
}
fun startToManageFieldReferences(database: DatabaseKDBX) {
this.mDatabase = database
this.mDecodeRef = true
}
fun stopToManageFieldReferences() {
this.mDatabase = null
this.mDecodeRef = false
}
override fun initNodeId(): NodeId<UUID> {
return NodeIdUUID()
}
override fun copyNodeId(nodeId: NodeId<UUID>): NodeId<UUID> {
return NodeIdUUID(nodeId.id)
}
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
return parcel.readParcelable(GroupKDBX::class.java.classLoader)
}
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
parcel.writeParcelable(parent, flags)
}
/**
* Decode a reference key with the FieldReferencesEngine
* @param decodeRef
* @param key
* @return
*/
private fun decodeRefKey(decodeRef: Boolean, key: String): String {
return fields[key]?.toString()?.let { text ->
return if (decodeRef) {
if (mDatabase == null) text else FieldReferencesEngine().compile(text, this, mDatabase!!)
} else text
} ?: ""
}
override var title: String
get() = decodeRefKey(mDecodeRef, STR_TITLE)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectTitle
fields[STR_TITLE] = ProtectedString(protect, value)
}
override val type: Type
get() = Type.ENTRY
override var username: String
get() = decodeRefKey(mDecodeRef, STR_USERNAME)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUserName
fields[STR_USERNAME] = ProtectedString(protect, value)
}
override var password: String
get() = decodeRefKey(mDecodeRef, STR_PASSWORD)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectPassword
fields[STR_PASSWORD] = ProtectedString(protect, value)
}
override var url
get() = decodeRefKey(mDecodeRef, STR_URL)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUrl
fields[STR_URL] = ProtectedString(protect, value)
}
override var notes: String
get() = decodeRefKey(mDecodeRef, STR_NOTES)
set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectNotes
fields[STR_NOTES] = ProtectedString(protect, value)
}
override var usageCount = UnsignedLong(0)
override var locationChanged = DateInstant()
fun afterChangeParent() {
locationChanged = DateInstant()
}
@@ -245,7 +270,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
field.clear()
for ((key, value) in fields) {
if (!isStandardField(key)) {
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key, 0))
}
}
return field
@@ -302,14 +327,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return size
}
override fun putCustomData(key: String, value: String) {
customData[key] = value
}
override fun containsCustomData(): Boolean {
return customData.isNotEmpty()
}
fun addEntryToHistory(entry: EntryKDBX) {
history.add(entry)
}
@@ -349,6 +366,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
const val STR_URL = "URL"
const val STR_NOTES = "Notes"
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
fun newCustomNameAllowed(name: String): Boolean {
return !(name.equals(STR_TITLE, true)
|| name.equals(STR_USERNAME, true)
@@ -367,7 +386,5 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return arrayOfNulls(size)
}
}
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
}
}

View File

@@ -36,6 +36,10 @@ abstract class EntryVersioned
constructor(parcel: Parcel) : super(parcel)
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
}
override fun nodeIndexInParentForNaturalOrder(): Int {
if (nodeIndexInParentForNaturalOrder == -1) {
val numberOfGroups = parent?.getChildGroups()?.size

View File

@@ -20,267 +20,123 @@
package com.kunzisoft.keepass.database.element.entry
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.search.EntryKDBXSearchHandler
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.utils.UuidUtil
import java.util.*
class FieldReferencesEngine {
class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
inner class TargetResult(var entry: EntryKDBX?, var wanted: Char)
// Key : <WantedField>@<SearchIn>:<Text>
// Value : content
private var refsCache: MutableMap<String, String?> = HashMap()
private inner class SprContextV4 {
var databaseV4: DatabaseKDBX? = null
var entry: EntryKDBX
var refsCache: MutableMap<String, String> = HashMap()
internal constructor(db: DatabaseKDBX, entry: EntryKDBX) {
this.databaseV4 = db
this.entry = entry
}
internal constructor(source: SprContextV4) {
this.databaseV4 = source.databaseV4
this.entry = source.entry
this.refsCache = source.refsCache
}
fun clear() {
refsCache.clear()
}
fun compile(text: String, entry: EntryKDBX, database: DatabaseKDBX): String {
return compileInternal(text, SprContextV4(database, entry), 0)
}
private fun compileInternal(text: String?, sprContextV4: SprContextV4?, recursionLevel: Int): String {
if (text == null) {
return ""
}
if (sprContextV4 == null) {
return ""
}
fun compile(textReference: String, recursionLevel: Int): String {
return if (recursionLevel >= MAX_RECURSION_DEPTH) {
""
} else fillRefPlaceholders(text, sprContextV4, recursionLevel)
} else
fillReferencesPlaceholders(textReference, recursionLevel)
}
private fun fillRefPlaceholders(textReference: String, contextV4: SprContextV4, recursionLevel: Int): String {
var text = textReference
if (contextV4.databaseV4 == null) {
return text
}
/**
* Manage placeholders with {REF:<WantedField>@<SearchIn>:<Text>}
*/
private fun fillReferencesPlaceholders(textReference: String, recursionLevel: Int): String {
var textValue = textReference
var offset = 0
for (i in 0..19) {
text = fillRefsUsingCache(text, contextV4)
var numberInlineRef = 0
while (textValue.contains(STR_REF_START)
&& numberInlineRef <= MAX_INLINE_REF) {
numberInlineRef++
val start = text.indexOf(STR_REF_START, offset, true)
textValue = fillReferencesUsingCache(textValue)
val start = textValue.indexOf(STR_REF_START, offset, true)
if (start < 0) {
break
}
val end = text.indexOf(STR_REF_END, start + 1, true)
val end = textValue.indexOf(STR_REF_END, offset, true)
if (end <= start) {
break
}
val fullRef = text.substring(start, end + 1)
val result = findRefTarget(fullRef, contextV4)
val reference = textValue.substring(start + STR_REF_START.length, end)
val fullReference = "$STR_REF_START$reference$STR_REF_END"
if (result != null) {
val found = result.entry
found?.stopToManageFieldReferences()
val wanted = result.wanted
var data: String? = null
when (wanted) {
'T' -> data = found?.title
'U' -> data = found?.username
'A' -> data = found?.url
'P' -> data = found?.password
'N' -> data = found?.notes
'I' -> data = found?.nodeId.toString()
}
if (data != null && found != null) {
val subCtx = SprContextV4(contextV4)
subCtx.entry = found
val innerContent = compileInternal(data, subCtx, recursionLevel + 1)
addRefsToCache(fullRef, innerContent, contextV4)
text = fillRefsUsingCache(text, contextV4)
} else {
offset = start + 1
if (!refsCache.containsKey(fullReference)) {
val result = findReferenceTarget(reference)
val entryFound = result.entry
val newRecursionLevel = recursionLevel + 1
val data: String? = when (result.wanted) {
'T' -> entryFound?.decodeTitleKey(newRecursionLevel)
'U' -> entryFound?.decodeUsernameKey(newRecursionLevel)
'A' -> entryFound?.decodeUrlKey(newRecursionLevel)
'P' -> entryFound?.decodePasswordKey(newRecursionLevel)
'N' -> entryFound?.decodeNotesKey(newRecursionLevel)
'I' -> UuidUtil.toHexString(entryFound?.nodeId?.id)
else -> null
}
refsCache[fullReference] = data
textValue = fillReferencesUsingCache(textValue)
}
offset = end
}
return text
return textValue
}
private fun findRefTarget(fullReference: String?, contextV4: SprContextV4): TargetResult? {
var fullRef: String? = fullReference ?: return null
fullRef = fullRef!!.toUpperCase(Locale.ENGLISH)
if (!fullRef.startsWith(STR_REF_START) || !fullRef.endsWith(STR_REF_END)) {
return null
}
val ref = fullRef.substring(STR_REF_START.length, fullRef.length - STR_REF_END.length)
if (ref.length <= 4) {
return null
}
if (ref[1] != '@') {
return null
}
if (ref[3] != ':') {
return null
}
val scan = Character.toUpperCase(ref[2])
val wanted = Character.toUpperCase(ref[0])
val searchParameters = SearchParameters()
searchParameters.setupNone()
searchParameters.searchString = ref.substring(4)
when (scan) {
'T' -> searchParameters.searchInTitles = true
'U' -> searchParameters.searchInUserNames = true
'A' -> searchParameters.searchInUrls = true
'P' -> searchParameters.searchInPasswords = true
'N' -> searchParameters.searchInNotes = true
'I' -> searchParameters.searchInUUIDs = true
'O' -> searchParameters.searchInOther = true
else -> return null
}
val list = ArrayList<EntryKDBX>()
searchEntries(contextV4.databaseV4?.rootGroup, searchParameters, list)
return if (list.size > 0) {
TargetResult(list[0], wanted)
} else null
}
private fun addRefsToCache(ref: String?, value: String?, ctx: SprContextV4?) {
if (ref == null) {
return
}
if (value == null) {
return
}
if (ctx == null) {
return
}
if (!ctx.refsCache.containsKey(ref)) {
ctx.refsCache[ref] = value
}
}
private fun fillRefsUsingCache(text: String, sprContextV4: SprContextV4): String {
private fun fillReferencesUsingCache(text: String): String {
var newText = text
for ((key, value) in sprContextV4.refsCache) {
newText = text.replace(key, value, true)
for ((key, value) in refsCache) {
// Replace by key if value not found
newText = newText.replace(key, value ?: key, true)
}
return newText
}
private fun searchEntries(root: GroupKDBX?, searchParameters: SearchParameters?, listStorage: MutableList<EntryKDBX>?) {
if (searchParameters == null) {
return
private fun findReferenceTarget(reference: String): TargetResult {
val targetResult = TargetResult(null, 'J')
if (reference.length <= 4) {
return targetResult
}
if (listStorage == null) {
return
if (reference[1] != '@') {
return targetResult
}
if (reference[3] != ':') {
return targetResult
}
val terms = splitStringTerms(searchParameters.searchString)
if (terms.size <= 1 || searchParameters.regularExpression) {
root!!.doForEachChild(EntryKDBXSearchHandler(searchParameters, listStorage), null)
return
}
// Search longest term first
val stringLengthComparator = Comparator<String> { lhs, rhs -> lhs.length - rhs.length }
Collections.sort(terms, stringLengthComparator)
val fullSearch = searchParameters.searchString
var childEntries: List<EntryKDBX>? = root!!.getChildEntries()
for (i in terms.indices) {
val pgNew = ArrayList<EntryKDBX>()
searchParameters.searchString = terms[i]
var negate = false
if (searchParameters.searchString.startsWith("-")) {
searchParameters.searchString = searchParameters.searchString.substring(1)
negate = searchParameters.searchString.isNotEmpty()
}
if (!root.doForEachChild(EntryKDBXSearchHandler(searchParameters, pgNew), null)) {
childEntries = null
break
}
childEntries = if (negate) {
val complement = ArrayList<EntryKDBX>()
for (entry in childEntries!!) {
if (!pgNew.contains(entry)) {
complement.add(entry)
}
}
complement
} else {
pgNew
}
}
if (childEntries != null) {
listStorage.addAll(childEntries)
}
searchParameters.searchString = fullSearch
}
/**
* Create a list of String by split text when ' ', '\t', '\r' or '\n' is found
*/
private fun splitStringTerms(text: String?): List<String> {
val list = ArrayList<String>()
if (text == null) {
return list
}
val stringBuilder = StringBuilder()
var quoted = false
for (element in text) {
if ((element == ' ' || element == '\t' || element == '\r' || element == '\n') && !quoted) {
val len = stringBuilder.length
when {
len > 0 -> {
list.add(stringBuilder.toString())
stringBuilder.delete(0, len)
}
element == '\"' -> quoted = !quoted
else -> stringBuilder.append(element)
targetResult.wanted = Character.toUpperCase(reference[0])
val searchIn = Character.toUpperCase(reference[2])
val searchQuery = reference.substring(4)
targetResult.entry = when (searchIn) {
'T' -> mDatabase.getEntryByTitle(searchQuery)
'U' -> mDatabase.getEntryByUsername(searchQuery)
'A' -> mDatabase.getEntryByURL(searchQuery)
'P' -> mDatabase.getEntryByPassword(searchQuery)
'N' -> mDatabase.getEntryByNotes(searchQuery)
'I' -> {
UuidUtil.fromHexString(searchQuery)?.let { uuid ->
mDatabase.getEntryById(NodeIdUUID(uuid))
}
}
'O' -> mDatabase.getEntryByCustomData(searchQuery)
else -> null
}
if (stringBuilder.isNotEmpty()) {
list.add(stringBuilder.toString())
}
return list
return targetResult
}
private data class TargetResult(var entry: EntryKDBX?, var wanted: Char)
companion object {
private const val MAX_RECURSION_DEPTH = 12
private const val MAX_RECURSION_DEPTH = 10
private const val MAX_INLINE_REF = 10
private const val STR_REF_START = "{REF:"
private const val STR_REF_END = "}"
}

View File

@@ -31,14 +31,12 @@ import java.util.*
class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface {
var level = 0 // short
// Used by KeePass internally, don't use
var groupFlags = 0
constructor() : super()
constructor(parcel: Parcel) : super(parcel) {
level = parcel.readInt()
groupFlags = parcel.readInt()
}
@@ -52,13 +50,11 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeInt(level)
dest.writeInt(groupFlags)
}
fun updateWith(source: GroupKDB) {
super.updateWith(source)
level = source.level
groupFlags = source.groupFlags
}
@@ -73,15 +69,12 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
return NodeIdInt(nodeId.id)
}
override fun afterAssignNewParent() {
if (parent != null)
level = parent!!.level + 1
}
fun setGroupId(groupId: Int) {
this.nodeId = NodeIdInt(groupId)
}
override fun afterAssignNewParent() {}
companion object {
@JvmField

View File

@@ -20,8 +20,11 @@
package com.kunzisoft.keepass.database.element.group
import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.node.NodeId
@@ -33,14 +36,17 @@ import java.util.*
class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
private val customData = HashMap<String, String>()
override var usageCount = UnsignedLong(0)
override var locationChanged = DateInstant()
override var customData = CustomData()
var notes = ""
var isExpanded = true
var defaultAutoTypeSequence = ""
var enableAutoType: Boolean? = null
var enableSearching: Boolean? = null
var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO
override var tags = Tags()
override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
override var expires: Boolean = false
@@ -60,7 +66,7 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
constructor(parcel: Parcel) : super(parcel) {
usageCount = UnsignedLong(parcel.readLong())
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
// TODO customData = ParcelableUtil.readStringParcelableMap(parcel);
customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData()
notes = parcel.readString() ?: notes
isExpanded = parcel.readByte().toInt() != 0
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
@@ -69,6 +75,8 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
val isSearchingEnabled = parcel.readInt()
enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1
lastTopVisibleEntry = parcel.readSerializable() as UUID
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
}
override fun readParentParcelable(parcel: Parcel): GroupKDBX? {
@@ -83,13 +91,15 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
super.writeToParcel(dest, flags)
dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags)
// TODO ParcelableUtil.writeStringParcelableMap(dest, customData);
dest.writeParcelable(customData, flags)
dest.writeString(notes)
dest.writeByte((if (isExpanded) 1 else 0).toByte())
dest.writeString(defaultAutoTypeSequence)
dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0)
dest.writeSerializable(lastTopVisibleEntry)
dest.writeParcelable(tags, flags)
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
}
fun updateWith(source: GroupKDBX) {
@@ -97,34 +107,21 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map
customData.clear()
for ((key, value) in source.customData) {
customData[key] = value
}
customData = CustomData(source.customData)
notes = source.notes
isExpanded = source.isExpanded
defaultAutoTypeSequence = source.defaultAutoTypeSequence
enableAutoType = source.enableAutoType
enableSearching = source.enableSearching
lastTopVisibleEntry = source.lastTopVisibleEntry
tags = source.tags
previousParentGroup = source.previousParentGroup
}
override var usageCount = UnsignedLong(0)
override var locationChanged = DateInstant()
override fun afterAssignNewParent() {
locationChanged = DateInstant()
}
override fun putCustomData(key: String, value: String) {
customData[key] = value
}
override fun containsCustomData(): Boolean {
return customData.isNotEmpty()
}
companion object {
@JvmField

View File

@@ -63,6 +63,17 @@ abstract class GroupVersioned
get() = titleGroup
set(value) { titleGroup = value }
/**
* To determine the level from the root group (root group level is -1)
*/
fun getLevel(): Int {
var level = -1
parent?.let { parent ->
level = parent.getLevel() + 1
}
return level
}
override fun getChildGroups(): List<Group> {
return childGroups
}

View File

@@ -45,23 +45,64 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
groupHandler.operate(this as Group)
}
fun doForEachChild(entryHandler: NodeHandler<Entry>,
fun doForEachChild(entryHandler: NodeHandler<Entry>?,
groupHandler: NodeHandler<Group>?,
stopIterationWhenGroupHandlerFails: Boolean = true): Boolean {
for (entry in this.getChildEntries()) {
if (!entryHandler.operate(entry))
return false
stopIterationWhenGroupHandlerOperateFalse: Boolean = true): Boolean {
if (entryHandler != null) {
for (entry in this.getChildEntries()) {
if (!entryHandler.operate(entry))
return false
}
}
for (group in this.getChildGroups()) {
var doActionForChild = true
if (groupHandler != null && !groupHandler.operate(group)) {
doActionForChild = false
if (stopIterationWhenGroupHandlerFails)
if (stopIterationWhenGroupHandlerOperateFalse)
return false
}
if (doActionForChild)
group.doForEachChild(entryHandler, groupHandler)
group.doForEachChild(entryHandler, groupHandler, stopIterationWhenGroupHandlerOperateFalse)
}
return true
}
fun searchChildEntry(criteria: (entry: Entry) -> Boolean): Entry? {
return searchChildEntry(this, criteria)
}
private fun searchChildEntry(rootGroup: GroupVersionedInterface<Group, Entry>,
criteria: (entry: Entry) -> Boolean): Entry? {
for (childEntry in rootGroup.getChildEntries()) {
if (criteria.invoke(childEntry)) {
return childEntry
}
}
for (group in rootGroup.getChildGroups()) {
val searchChildEntry = searchChildEntry(group, criteria)
if (searchChildEntry != null) {
return searchChildEntry
}
}
return null
}
fun searchChildGroup(criteria: (group: Group) -> Boolean): Group? {
return searchChildGroup(this, criteria)
}
private fun searchChildGroup(rootGroup: GroupVersionedInterface<Group, Entry>,
criteria: (group: Group) -> Boolean): Group? {
for (childGroup in rootGroup.getChildGroups()) {
if (criteria.invoke(childGroup)) {
return childGroup
} else {
val subGroup = searchChildGroup(childGroup, criteria)
if (subGroup != null) {
return subGroup
}
}
}
return null
}
}

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.element.icon
import android.os.Parcel
import android.os.Parcelable
class IconImage() : IconImageDraw(), Parcelable {
class IconImage() : IconImageDraw() {
var standard: IconImageStandard = IconImageStandard()
var custom: IconImageCustom = IconImageCustom()

View File

@@ -22,27 +22,38 @@ package com.kunzisoft.keepass.database.element.icon
import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import java.util.*
class IconImageCustom : Parcelable, IconImageDraw {
class IconImageCustom : IconImageDraw {
var uuid: UUID
val uuid: UUID
var name: String = ""
var lastModificationTime: DateInstant? = null
constructor() {
uuid = DatabaseVersioned.UUID_ZERO
constructor(name: String = "", lastModificationTime: DateInstant? = null) {
this.uuid = DatabaseVersioned.UUID_ZERO
this.name = name
this.lastModificationTime = lastModificationTime
}
constructor(uuid: UUID) {
constructor(uuid: UUID, name: String = "", lastModificationTime: DateInstant? = null) {
this.uuid = uuid
this.name = name
this.lastModificationTime = lastModificationTime
}
constructor(parcel: Parcel) {
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
name = parcel.readString() ?: name
lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader)
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeParcelable(ParcelUuid(uuid), flags)
dest.writeString(name)
dest.writeParcelable(lastModificationTime, flags)
}
override fun describeContents(): Int {

View File

@@ -19,7 +19,9 @@
*/
package com.kunzisoft.keepass.database.element.icon
abstract class IconImageDraw {
import android.os.Parcelable
abstract class IconImageDraw : Parcelable {
var selected = false
/**

View File

@@ -23,7 +23,7 @@ import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
class IconImageStandard : Parcelable, IconImageDraw {
class IconImageStandard : IconImageDraw {
val id: Int

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.element.icon
import android.util.Log
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.CustomIconPool
@@ -27,7 +28,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.K
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
import java.util.*
class IconsManager(private val binaryCache: BinaryCache) {
class IconsManager(binaryCache: BinaryCache) {
private val standardCache = List(NB_ICONS) {
IconImageStandard(it)
@@ -52,17 +53,15 @@ class IconsManager(private val binaryCache: BinaryCache) {
fun buildNewCustomIcon(key: UUID? = null,
result: (IconImageCustom, BinaryData?) -> Unit) {
// Create a binary file for a brand new custom icon
addCustomIcon(key, false, result)
addCustomIcon(key, "", null, false, result)
}
fun addCustomIcon(key: UUID? = null,
name: String,
lastModificationTime: DateInstant?,
smallSize: Boolean,
result: (IconImageCustom, BinaryData?) -> Unit) {
val keyBinary = customCache.put(key) { uniqueBinaryId ->
// Create a byte array for better performance with small data
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
}
result.invoke(IconImageCustom(keyBinary.keys.first()), keyBinary.binary)
customCache.put(key, name, lastModificationTime, smallSize, result)
}
fun getIcon(iconUuid: UUID): IconImageCustom {
@@ -88,8 +87,12 @@ class IconsManager(private val binaryCache: BinaryCache) {
}
fun doForEachCustomIcon(action: (IconImageCustom, BinaryData) -> Unit) {
customCache.doForEachBinary { key, binary ->
action.invoke(IconImageCustom(key), binary)
customCache.doForEachCustomIcon(action)
}
fun containsCustomIconWithNameOrLastModificationTime(): Boolean {
return customCache.any { customIcon ->
customIcon.name.isNotEmpty() || customIcon.lastModificationTime != null
}
}

View File

@@ -19,17 +19,17 @@
*/
package com.kunzisoft.keepass.database.element.node
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.*
interface NodeKDBXInterface : NodeTimeInterface {
var usageCount: UnsignedLong
var locationChanged: DateInstant
fun putCustomData(key: String, value: String)
fun containsCustomData(): Boolean
var customData: CustomData
var tags: Tags
var previousParentGroup: UUID
}

View File

@@ -25,15 +25,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
interface NodeVersionedInterface<ParentGroup> : NodeTimeInterface, Parcelable {
var title: String
/**
* @return Visual icon
*/
var icon: IconImage
/**
* @return Type of Node
*/
val type: Type
/**

View File

@@ -114,7 +114,7 @@ class NoMemoryDatabaseException: LoadDatabaseException {
constructor(exception: Throwable) : super(exception)
}
class EntryDatabaseException: LoadDatabaseException {
class MoveEntryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_move_entry_here
constructor() : super()
@@ -123,7 +123,7 @@ class EntryDatabaseException: LoadDatabaseException {
class MoveGroupDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_move_folder_in_itself
override var errorId: Int = R.string.error_move_group_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}

View File

@@ -19,9 +19,9 @@
*/
package com.kunzisoft.keepass.database.file
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
import com.kunzisoft.keepass.database.crypto.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
@@ -91,41 +91,64 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
this.masterSeed = ByteArray(32)
}
private inner class NodeHasCustomData<T: NodeKDBXInterface> : NodeHandler<T>() {
internal var containsCustomData = false
private open class NodeOperationHandler<T: NodeKDBXInterface> : NodeHandler<T>() {
var containsCustomData = false
override fun operate(node: T): Boolean {
if (node.containsCustomData()) {
if (node.customData.isNotEmpty()) {
containsCustomData = true
return false
}
return true
}
}
private fun getMinKdbxVersion(databaseV4: DatabaseKDBX): UnsignedInt {
private inner class EntryOperationHandler: NodeOperationHandler<EntryKDBX>() {
var passwordQualityEstimationDisabled = false
override fun operate(node: EntryKDBX): Boolean {
if (!node.qualityCheck) {
passwordQualityEstimationDisabled = true
}
return super.operate(node)
}
}
private inner class GroupOperationHandler: NodeOperationHandler<GroupKDBX>() {
var containsTags = false
override fun operate(node: GroupKDBX): Boolean {
if (!node.tags.isEmpty())
containsTags = true
return super.operate(node)
}
}
private fun getMinKdbxVersion(databaseKDBX: DatabaseKDBX): UnsignedInt {
val entryHandler = EntryOperationHandler()
val groupHandler = GroupOperationHandler()
databaseKDBX.rootGroup?.doForEachChildAndForIt(entryHandler, groupHandler)
// https://keepass.info/help/kb/kdbx_4.1.html
val containsGroupWithTag = groupHandler.containsTags
val containsEntryWithPasswordQualityEstimationDisabled = entryHandler.passwordQualityEstimationDisabled
val containsCustomIconWithNameOrLastModificationTime = databaseKDBX.iconsManager.containsCustomIconWithNameOrLastModificationTime()
val containsHeaderCustomDataWithLastModificationTime = databaseKDBX.customData.containsItemWithLastModificationTime()
// https://keepass.info/help/kb/kdbx_4.html
// If AES is not use, it's at least 4.0
val kdfIsNotAes = databaseKDBX.kdfParameters?.uuid != AesKdf.CIPHER_UUID
val containsHeaderCustomData = databaseKDBX.customData.isNotEmpty()
val containsNodeCustomData = entryHandler.containsCustomData || groupHandler.containsCustomData
// Return v4 if AES is not use
if (databaseV4.kdfParameters != null
&& databaseV4.kdfParameters!!.uuid != AesKdf.CIPHER_UUID) {
return FILE_VERSION_32_4
}
if (databaseV4.rootGroup == null) {
return FILE_VERSION_32_3
}
val entryHandler = NodeHasCustomData<EntryKDBX>()
val groupHandler = NodeHasCustomData<GroupKDBX>()
databaseV4.rootGroup?.doForEachChildAndForIt(entryHandler, groupHandler)
return if (databaseV4.containsCustomData()
|| entryHandler.containsCustomData
|| groupHandler.containsCustomData) {
FILE_VERSION_32_4
// Check each condition to determine version
return if (containsGroupWithTag
|| containsEntryWithPasswordQualityEstimationDisabled
|| containsCustomIconWithNameOrLastModificationTime
|| containsHeaderCustomDataWithLastModificationTime) {
FILE_VERSION_41
} else if (kdfIsNotAes
|| containsHeaderCustomData
|| containsNodeCustomData) {
FILE_VERSION_40
} else {
FILE_VERSION_32_3
FILE_VERSION_31
}
}
@@ -167,7 +190,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
private fun readHeaderField(dis: InputStream): Boolean {
val fieldID = dis.read().toByte()
val fieldSize: Int = if (version.isBefore(FILE_VERSION_32_4)) {
val fieldSize: Int = if (version.isBefore(FILE_VERSION_40)) {
dis.readBytes2ToUShort()
} else {
dis.readBytes4ToUInt().toKotlinInt()
@@ -194,20 +217,20 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
PwDbHeaderV4Fields.MasterSeed -> masterSeed = fieldData
PwDbHeaderV4Fields.TransformSeed -> if (version.isBefore(FILE_VERSION_32_4))
PwDbHeaderV4Fields.TransformSeed -> if (version.isBefore(FILE_VERSION_40))
transformSeed = fieldData
PwDbHeaderV4Fields.TransformRounds -> if (version.isBefore(FILE_VERSION_32_4))
PwDbHeaderV4Fields.TransformRounds -> if (version.isBefore(FILE_VERSION_40))
setTransformRound(fieldData)
PwDbHeaderV4Fields.EncryptionIV -> encryptionIV = fieldData
PwDbHeaderV4Fields.InnerRandomstreamKey -> if (version.isBefore(FILE_VERSION_32_4))
PwDbHeaderV4Fields.InnerRandomstreamKey -> if (version.isBefore(FILE_VERSION_40))
innerRandomStreamKey = fieldData
PwDbHeaderV4Fields.StreamStartBytes -> streamStartBytes = fieldData
PwDbHeaderV4Fields.InnerRandomStreamID -> if (version.isBefore(FILE_VERSION_32_4))
PwDbHeaderV4Fields.InnerRandomStreamID -> if (version.isBefore(FILE_VERSION_40))
setRandomStreamID(fieldData)
PwDbHeaderV4Fields.KdfParameters -> databaseV4.kdfParameters = KdfParameters.deserialize(fieldData)
@@ -283,7 +306,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
*/
private fun validVersion(version: UnsignedInt): Boolean {
return version.toKotlinInt() and FILE_VERSION_CRITICAL_MASK.toKotlinInt() <=
FILE_VERSION_32_4.toKotlinInt() and FILE_VERSION_CRITICAL_MASK.toKotlinInt()
FILE_VERSION_40.toKotlinInt() and FILE_VERSION_CRITICAL_MASK.toKotlinInt()
}
companion object {
@@ -292,8 +315,9 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
val DBSIG_2 = UnsignedInt(-0x4ab40499)
private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000)
val FILE_VERSION_32_3 = UnsignedInt(0x00030001)
val FILE_VERSION_32_4 = UnsignedInt(0x00040000)
val FILE_VERSION_31 = UnsignedInt(0x00030001)
val FILE_VERSION_40 = UnsignedInt(0x00040000)
val FILE_VERSION_41 = UnsignedInt(0x00040001)
fun getCompressionFromFlag(flag: UnsignedInt): CompressionAlgorithm? {
return when (flag.toKotlinInt()) {

View File

@@ -79,8 +79,10 @@ object DatabaseKDBXXML {
const val ElemFgColor = "ForegroundColor"
const val ElemBgColor = "BackgroundColor"
const val ElemOverrideUrl = "OverrideURL"
const val ElemQualityCheck = "QualityCheck"
const val ElemTimes = "Times"
const val ElemTags = "Tags"
const val ElemPreviousParentGroup = "PreviousParentGroup"
const val ElemCreationTime = "CreationTime"
const val ElemLastModTime = "LastModificationTime"

View File

@@ -40,6 +40,7 @@ import java.security.MessageDigest
import java.util.*
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import kotlin.collections.HashMap
/**
@@ -154,11 +155,10 @@ class DatabaseInputKDB(cacheDirectory: File,
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
val newRoot = mDatabase.createGroup()
newRoot.level = -1
mDatabase.rootGroup = newRoot
mDatabase.addGroupIndex(newRoot)
// Import all nodes
val groupLevelList = HashMap<GroupKDB, Int>()
var newGroup: GroupKDB? = null
var newEntry: EntryKDB? = null
var currentGroupNumber = 0
@@ -248,7 +248,7 @@ class DatabaseInputKDB(cacheDirectory: File,
}
0x0008 -> {
newGroup?.let { group ->
group.level = cipherInputStream.readBytes2ToUShort()
groupLevelList.put(group, cipherInputStream.readBytes2ToUShort())
} ?:
newEntry?.let { entry ->
entry.notes = cipherInputStream.readBytesToString(fieldSize)
@@ -318,7 +318,7 @@ class DatabaseInputKDB(cacheDirectory: File,
if (!Arrays.equals(messageDigest.digest(), header.contentsHash)) {
throw InvalidCredentialsDatabaseException()
}
constructTreeFromIndex()
constructTreeFromIndex(groupLevelList)
stopContentTimer()
@@ -339,34 +339,40 @@ class DatabaseInputKDB(cacheDirectory: File,
return mDatabase
}
private fun buildTreeGroups(previousGroup: GroupKDB, currentGroup: GroupKDB, groupIterator: Iterator<GroupKDB>) {
private fun buildTreeGroups(groupLevelList: HashMap<GroupKDB, Int>,
previousGroup: GroupKDB,
currentGroup: GroupKDB,
groupIterator: Iterator<GroupKDB>) {
if (currentGroup.parent == null && (previousGroup.level + 1) == currentGroup.level) {
val previousGroupLevel = groupLevelList[previousGroup] ?: -1
val currentGroupLevel = groupLevelList[currentGroup] ?: -1
if (currentGroup.parent == null && (previousGroupLevel + 1) == currentGroupLevel) {
// Current group has an increment level compare to the previous, current group is a child
previousGroup.addChildGroup(currentGroup)
currentGroup.parent = previousGroup
} else if (previousGroup.parent != null && previousGroup.level == currentGroup.level) {
} else if (previousGroup.parent != null && previousGroupLevel == currentGroupLevel) {
// In the same level, previous parent is the same as previous group
previousGroup.parent!!.addChildGroup(currentGroup)
currentGroup.parent = previousGroup.parent
} else if (previousGroup.parent != null) {
// Previous group has a higher level than the current group, check it's parent
buildTreeGroups(previousGroup.parent!!, currentGroup, groupIterator)
buildTreeGroups(groupLevelList, previousGroup.parent!!, currentGroup, groupIterator)
}
// Next current group
if (groupIterator.hasNext()){
buildTreeGroups(currentGroup, groupIterator.next(), groupIterator)
buildTreeGroups(groupLevelList, currentGroup, groupIterator.next(), groupIterator)
}
}
private fun constructTreeFromIndex() {
mDatabase.rootGroup?.let {
private fun constructTreeFromIndex(groupLevelList: HashMap<GroupKDB, Int>) {
mDatabase.rootGroup?.let { root ->
// add each group
val groupIterator = mDatabase.getGroupIndexes().iterator()
if (groupIterator.hasNext())
buildTreeGroups(it, groupIterator.next(), groupIterator)
buildTreeGroups(groupLevelList, root, groupIterator.next(), groupIterator)
// add each child
for (currentEntry in mDatabase.getEntryIndexes()) {

View File

@@ -26,9 +26,7 @@ import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.HmacBlock
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
@@ -42,7 +40,7 @@ import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
import com.kunzisoft.keepass.database.file.DateKDBXUtil
import com.kunzisoft.keepass.stream.HashedBlockInputStream
@@ -88,9 +86,12 @@ class DatabaseInputKDBX(cacheDirectory: File,
private var ctxHistoryBase: EntryKDBX? = null
private var ctxDeletedObject: DeletedObject? = null
private var customIconID = DatabaseVersioned.UUID_ZERO
private var customIconName: String = ""
private var customIconLastModificationTime: DateInstant? = null
private var customIconData: ByteArray? = null
private var customDataKey: String? = null
private var customDataValue: String? = null
private var customDataLastModificationTime: DateInstant? = null
private var groupCustomDataKey: String? = null
private var groupCustomDataValue: String? = null
private var entryCustomDataKey: String? = null
@@ -151,15 +152,17 @@ class DatabaseInputKDBX(cacheDirectory: File,
val cipher: Cipher
try {
engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine
engine.forcePaddingCompatibility = true
mDatabase.setDataEngine(engine)
mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
engine.forcePaddingCompatibility = false
} catch (e: Exception) {
throw InvalidAlgorithmDatabaseException(e)
}
val plainInputStream: InputStream
if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_32_4)) {
if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) {
val dataDecrypted = CipherInputStream(databaseInputStream, cipher)
val storedStartBytes: ByteArray?
@@ -208,7 +211,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
else -> plainInputStream
}
if (!mDatabase.kdbxVersion.isBefore(FILE_VERSION_32_4)) {
if (!mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) {
readInnerHeader(inputStreamXml, header)
}
@@ -384,25 +387,25 @@ class DatabaseInputKDBX(cacheDirectory: File,
}
}
} else if (name.equals(DatabaseKDBXXML.ElemSettingsChanged, ignoreCase = true)) {
mDatabase.settingsChanged = readPwTime(xpp)
mDatabase.settingsChanged = readDateInstant(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDbName, ignoreCase = true)) {
mDatabase.name = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDbNameChanged, ignoreCase = true)) {
mDatabase.nameChanged = readPwTime(xpp)
mDatabase.nameChanged = readDateInstant(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDbDesc, ignoreCase = true)) {
mDatabase.description = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDbDescChanged, ignoreCase = true)) {
mDatabase.descriptionChanged = readPwTime(xpp)
mDatabase.descriptionChanged = readDateInstant(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDbDefaultUser, ignoreCase = true)) {
mDatabase.defaultUserName = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDbDefaultUserChanged, ignoreCase = true)) {
mDatabase.defaultUserNameChanged = readPwTime(xpp)
mDatabase.defaultUserNameChanged = readDateInstant(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDbColor, ignoreCase = true)) {
mDatabase.color = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDbMntncHistoryDays, ignoreCase = true)) {
mDatabase.maintenanceHistoryDays = readUInt(xpp, DEFAULT_HISTORY_DAYS)
} else if (name.equals(DatabaseKDBXXML.ElemDbKeyChanged, ignoreCase = true)) {
mDatabase.keyLastChanged = readPwTime(xpp)
mDatabase.keyLastChanged = readDateInstant(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDbKeyChangeRec, ignoreCase = true)) {
mDatabase.keyChangeRecDays = readLong(xpp, -1)
} else if (name.equals(DatabaseKDBXXML.ElemDbKeyChangeForce, ignoreCase = true)) {
@@ -418,11 +421,11 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (name.equals(DatabaseKDBXXML.ElemRecycleBinUuid, ignoreCase = true)) {
mDatabase.recycleBinUUID = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemRecycleBinChanged, ignoreCase = true)) {
mDatabase.recycleBinChanged = readTime(xpp)
mDatabase.recycleBinChanged = readDateInstant(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemEntryTemplatesGroup, ignoreCase = true)) {
mDatabase.entryTemplatesGroup = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemEntryTemplatesGroupChanged, ignoreCase = true)) {
mDatabase.entryTemplatesGroupChanged = readPwTime(xpp)
mDatabase.entryTemplatesGroupChanged = readDateInstant(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemHistoryMaxItems, ignoreCase = true)) {
mDatabase.historyMaxItems = readInt(xpp, -1)
} else if (name.equals(DatabaseKDBXXML.ElemHistoryMaxSize, ignoreCase = true)) {
@@ -466,6 +469,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
if (strData.isNotEmpty()) {
customIconData = Base64.decode(strData, BASE_64_FLAG)
}
} else if (name.equals(DatabaseKDBXXML.ElemName, ignoreCase = true)) {
customIconName = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true)) {
customIconLastModificationTime = readDateInstant(xpp)
} else {
readUnknown(xpp)
}
@@ -486,6 +493,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
customDataKey = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true)) {
customDataValue = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true)) {
customDataLastModificationTime = readDateInstant(xpp)
} else {
readUnknown(xpp)
}
@@ -516,6 +525,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
} else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
ctxGroup?.tags = readTags(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
ctxGroup?.previousParentGroup = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
return switchContext(ctx, KdbContext.GroupTimes, xpp)
} else if (name.equals(DatabaseKDBXXML.ElemIsExpanded, ignoreCase = true)) {
@@ -560,6 +573,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
KdbContext.GroupCustomDataItem -> when {
name.equals(DatabaseKDBXXML.ElemKey, ignoreCase = true) -> groupCustomDataKey = readString(xpp)
name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true) -> groupCustomDataValue = readString(xpp)
name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> readDateInstant(xpp) // Ignore
else -> readUnknown(xpp)
}
@@ -576,8 +590,12 @@ class DatabaseInputKDBX(cacheDirectory: File,
ctxEntry?.backgroundColor = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemOverrideUrl, ignoreCase = true)) {
ctxEntry?.overrideURL = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemQualityCheck, ignoreCase = true)) {
ctxEntry?.qualityCheck = readBool(xpp, true)
} else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
ctxEntry?.tags = readString(xpp)
ctxEntry?.tags = readTags(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
ctxEntry?.previousParentGroup = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemTimes, ignoreCase = true)) {
return switchContext(ctx, KdbContext.EntryTimes, xpp)
} else if (name.equals(DatabaseKDBXXML.ElemString, ignoreCase = true)) {
@@ -606,6 +624,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
KdbContext.EntryCustomDataItem -> when {
name.equals(DatabaseKDBXXML.ElemKey, ignoreCase = true) -> entryCustomDataKey = readString(xpp)
name.equals(DatabaseKDBXXML.ElemValue, ignoreCase = true) -> entryCustomDataValue = readString(xpp)
name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> readDateInstant(xpp) // Ignore
else -> readUnknown(xpp)
}
@@ -618,13 +637,13 @@ class DatabaseInputKDBX(cacheDirectory: File,
}
when {
name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> tl?.lastModificationTime = readPwTime(xpp)
name.equals(DatabaseKDBXXML.ElemCreationTime, ignoreCase = true) -> tl?.creationTime = readPwTime(xpp)
name.equals(DatabaseKDBXXML.ElemLastAccessTime, ignoreCase = true) -> tl?.lastAccessTime = readPwTime(xpp)
name.equals(DatabaseKDBXXML.ElemExpiryTime, ignoreCase = true) -> tl?.expiryTime = readPwTime(xpp)
name.equals(DatabaseKDBXXML.ElemLastModTime, ignoreCase = true) -> tl?.lastModificationTime = readDateInstant(xpp)
name.equals(DatabaseKDBXXML.ElemCreationTime, ignoreCase = true) -> tl?.creationTime = readDateInstant(xpp)
name.equals(DatabaseKDBXXML.ElemLastAccessTime, ignoreCase = true) -> tl?.lastAccessTime = readDateInstant(xpp)
name.equals(DatabaseKDBXXML.ElemExpiryTime, ignoreCase = true) -> tl?.expiryTime = readDateInstant(xpp)
name.equals(DatabaseKDBXXML.ElemExpires, ignoreCase = true) -> tl?.expires = readBool(xpp, false)
name.equals(DatabaseKDBXXML.ElemUsageCount, ignoreCase = true) -> tl?.usageCount = readULong(xpp, UnsignedLong(0))
name.equals(DatabaseKDBXXML.ElemLocationChanged, ignoreCase = true) -> tl?.locationChanged = readPwTime(xpp)
name.equals(DatabaseKDBXXML.ElemLocationChanged, ignoreCase = true) -> tl?.locationChanged = readDateInstant(xpp)
else -> readUnknown(xpp)
}
}
@@ -685,7 +704,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
ctxDeletedObject?.uuid = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
ctxDeletedObject?.setDeletionTime(readTime(xpp))
ctxDeletedObject?.setDeletionTime(readDateInstant(xpp))
} else {
readUnknown(xpp)
}
@@ -712,29 +731,34 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (ctx == KdbContext.CustomIcon && name.equals(DatabaseKDBXXML.ElemCustomIconItem, ignoreCase = true)) {
val iconData = customIconData
if (customIconID != DatabaseVersioned.UUID_ZERO && iconData != null) {
mDatabase.addCustomIcon(customIconID, isRAMSufficient.invoke(iconData.size.toLong())) { _, binary ->
mDatabase.addCustomIcon(customIconID,
customIconName,
customIconLastModificationTime,
isRAMSufficient.invoke(iconData.size.toLong())) { _, binary ->
binary?.getOutputDataStream(mDatabase.binaryCache)?.use { outputStream ->
outputStream.write(iconData)
}
}
}
customIconID = DatabaseVersioned.UUID_ZERO
customIconName = ""
customIconLastModificationTime = null
customIconData = null
return KdbContext.CustomIcons
} else if (ctx == KdbContext.Binaries && name.equals(DatabaseKDBXXML.ElemBinaries, ignoreCase = true)) {
return KdbContext.Meta
} else if (ctx == KdbContext.CustomData && name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) {
return KdbContext.Meta
} else if (ctx == KdbContext.CustomDataItem && name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) {
if (customDataKey != null && customDataValue != null) {
mDatabase.putCustomData(customDataKey!!, customDataValue!!)
customDataKey?.let { dataKey ->
customDataValue?.let { dataValue ->
mDatabase.customData.put(CustomDataItem(dataKey,
dataValue, customDataLastModificationTime))
}
}
customDataKey = null
customDataValue = null
customDataLastModificationTime = null
return KdbContext.CustomData
} else if (ctx == KdbContext.Group && name.equals(DatabaseKDBXXML.ElemGroup, ignoreCase = true)) {
if (ctxGroup != null && ctxGroup?.id == DatabaseVersioned.UUID_ZERO) {
@@ -756,13 +780,13 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (ctx == KdbContext.GroupCustomData && name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) {
return KdbContext.Group
} else if (ctx == KdbContext.GroupCustomDataItem && name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) {
if (groupCustomDataKey != null && groupCustomDataValue != null) {
ctxGroup?.putCustomData(groupCustomDataKey!!, groupCustomDataValue!!)
groupCustomDataKey?.let { customDataKey ->
groupCustomDataValue?.let { customDataValue ->
ctxGroup?.customData?.put(CustomDataItem(customDataKey, customDataValue))
}
}
groupCustomDataKey = null
groupCustomDataValue = null
return KdbContext.GroupCustomData
} else if (ctx == KdbContext.Entry && name.equals(DatabaseKDBXXML.ElemEntry, ignoreCase = true)) {
@@ -808,13 +832,13 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (ctx == KdbContext.EntryCustomData && name.equals(DatabaseKDBXXML.ElemCustomData, ignoreCase = true)) {
return KdbContext.Entry
} else if (ctx == KdbContext.EntryCustomDataItem && name.equals(DatabaseKDBXXML.ElemStringDictExItem, ignoreCase = true)) {
if (entryCustomDataKey != null && entryCustomDataValue != null) {
ctxEntry?.putCustomData(entryCustomDataKey!!, entryCustomDataValue!!)
entryCustomDataKey?.let { customDataKey ->
entryCustomDataValue?.let { customDataValue ->
ctxEntry?.customData?.put(CustomDataItem(customDataKey, customDataValue))
}
}
entryCustomDataKey = null
entryCustomDataValue = null
return KdbContext.EntryCustomData
} else if (ctx == KdbContext.EntryHistory && name.equals(DatabaseKDBXXML.ElemHistory, ignoreCase = true)) {
entryInHistory = false
@@ -834,16 +858,11 @@ class DatabaseInputKDBX(cacheDirectory: File,
}
@Throws(IOException::class, XmlPullParserException::class)
private fun readPwTime(xpp: XmlPullParser): DateInstant {
return DateInstant(readTime(xpp))
}
@Throws(IOException::class, XmlPullParserException::class)
private fun readTime(xpp: XmlPullParser): Date {
private fun readDateInstant(xpp: XmlPullParser): DateInstant {
val sDate = readString(xpp)
var utcDate: Date? = null
if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_32_4)) {
if (mDatabase.kdbxVersion.isBefore(FILE_VERSION_40)) {
try {
utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate)
} catch (e: ParseException) {
@@ -861,7 +880,12 @@ class DatabaseInputKDBX(cacheDirectory: File,
utcDate = DateKDBXUtil.convertKDBX4Time(seconds)
}
return utcDate ?: Date(0L)
return DateInstant(utcDate ?: Date(0L))
}
@Throws(IOException::class, XmlPullParserException::class)
private fun readTags(xpp: XmlPullParser): Tags {
return Tags(readString(xpp))
}
@Throws(XmlPullParserException::class, IOException::class)

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.stream.MacOutputStream
import com.kunzisoft.keepass.utils.*
import java.io.ByteArrayOutputStream
@@ -76,7 +76,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
if (header.version.isBefore(FILE_VERSION_32_4)) {
if (header.version.isBefore(FILE_VERSION_40)) {
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformSeed, header.transformSeed)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, longTo8Bytes(databaseKDBX.numberKeyEncryptionRounds))
} else {
@@ -87,7 +87,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.EncryptionIV, header.encryptionIV)
}
if (header.version.isBefore(FILE_VERSION_32_4)) {
if (header.version.isBefore(FILE_VERSION_40)) {
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomstreamKey, header.innerRandomStreamKey)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, uIntTo4Bytes(header.innerRandomStream!!.id))
@@ -121,7 +121,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
@Throws(IOException::class)
private fun writeHeaderFieldSize(size: Int) {
if (header.version.isBefore(FILE_VERSION_32_4)) {
if (header.version.isBefore(FILE_VERSION_40)) {
mos.write2BytesUShort(size)
} else {
mos.write4BytesUInt(UnsignedInt(size))

View File

@@ -20,15 +20,15 @@
package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.write2BytesUShort
import com.kunzisoft.keepass.utils.write4BytesUInt
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.write2BytesUShort
import com.kunzisoft.keepass.utils.write4BytesUInt
import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
@@ -59,6 +59,8 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
override fun output() {
// Before we output the header, we should sort our list of groups
// and remove any orphaned nodes that are no longer part of the tree hierarchy
// also remove the virtual root not present in kdb
val rootGroup = mDatabaseKDB.rootGroup
sortGroupsForOutput()
val header = outputHeader(mOutputStream)
@@ -86,8 +88,10 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
throw DatabaseOutputException("Invalid algorithm parameter.", e)
} catch (e: IOException) {
throw DatabaseOutputException("Failed to output final encrypted part.", e)
} finally {
// Add again the virtual root group for better management
mDatabaseKDB.rootGroup = rootGroup
}
}
@Throws(DatabaseOutputException::class)
@@ -201,7 +205,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
private fun sortGroupsForOutput() {
val groupList = ArrayList<GroupKDB>()
// Rebuild list according to coalation sorting order removing any orphaned groups
// Rebuild list according to sorting order removing any orphaned groups
for (rootGroup in mDatabaseKDB.rootGroups) {
sortGroup(rootGroup, groupList)
}

View File

@@ -23,15 +23,16 @@ import android.util.Base64
import android.util.Log
import android.util.Xml
import com.kunzisoft.encrypt.StreamCipher
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
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.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.AutoType
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
@@ -41,7 +42,8 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
import com.kunzisoft.keepass.database.file.DateKDBXUtil
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
@@ -83,7 +85,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
header = outputHeader(mOutputStream)
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_32_4)) {
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
val cos = attachStreamEncryptor(header!!, mOutputStream)
cos.write(header!!.streamStartBytes)
@@ -102,7 +104,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
else -> osPlain
}
if (!header!!.version.isBefore(FILE_VERSION_32_4)) {
if (!header!!.version.isBefore(FILE_VERSION_40)) {
outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream)
}
@@ -234,40 +236,40 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun writeMeta() {
xml.startTag(null, DatabaseKDBXXML.ElemMeta)
writeObject(DatabaseKDBXXML.ElemGenerator, mDatabaseKDBX.localizedAppName)
writeString(DatabaseKDBXXML.ElemGenerator, mDatabaseKDBX.localizedAppName)
if (hashOfHeader != null) {
writeObject(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG)))
writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG)))
}
writeObject(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
writeObject(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged.date)
writeObject(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
writeObject(DatabaseKDBXXML.ElemDbDescChanged, mDatabaseKDBX.descriptionChanged.date)
writeObject(DatabaseKDBXXML.ElemDbDefaultUser, mDatabaseKDBX.defaultUserName, true)
writeObject(DatabaseKDBXXML.ElemDbDefaultUserChanged, mDatabaseKDBX.defaultUserNameChanged.date)
writeObject(DatabaseKDBXXML.ElemDbMntncHistoryDays, mDatabaseKDBX.maintenanceHistoryDays.toKotlinLong())
writeObject(DatabaseKDBXXML.ElemDbColor, mDatabaseKDBX.color)
writeObject(DatabaseKDBXXML.ElemDbKeyChanged, mDatabaseKDBX.keyLastChanged.date)
writeObject(DatabaseKDBXXML.ElemDbKeyChangeRec, mDatabaseKDBX.keyChangeRecDays)
writeObject(DatabaseKDBXXML.ElemDbKeyChangeForce, mDatabaseKDBX.keyChangeForceDays)
writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged)
writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
writeDateInstant(DatabaseKDBXXML.ElemDbDescChanged, mDatabaseKDBX.descriptionChanged)
writeString(DatabaseKDBXXML.ElemDbDefaultUser, mDatabaseKDBX.defaultUserName, true)
writeDateInstant(DatabaseKDBXXML.ElemDbDefaultUserChanged, mDatabaseKDBX.defaultUserNameChanged)
writeLong(DatabaseKDBXXML.ElemDbMntncHistoryDays, mDatabaseKDBX.maintenanceHistoryDays.toKotlinLong())
writeString(DatabaseKDBXXML.ElemDbColor, mDatabaseKDBX.color)
writeDateInstant(DatabaseKDBXXML.ElemDbKeyChanged, mDatabaseKDBX.keyLastChanged)
writeLong(DatabaseKDBXXML.ElemDbKeyChangeRec, mDatabaseKDBX.keyChangeRecDays)
writeLong(DatabaseKDBXXML.ElemDbKeyChangeForce, mDatabaseKDBX.keyChangeForceDays)
writeMemoryProtection(mDatabaseKDBX.memoryProtection)
writeCustomIconList()
writeObject(DatabaseKDBXXML.ElemRecycleBinEnabled, mDatabaseKDBX.isRecycleBinEnabled)
writeBoolean(DatabaseKDBXXML.ElemRecycleBinEnabled, mDatabaseKDBX.isRecycleBinEnabled)
writeUuid(DatabaseKDBXXML.ElemRecycleBinUuid, mDatabaseKDBX.recycleBinUUID)
writeObject(DatabaseKDBXXML.ElemRecycleBinChanged, mDatabaseKDBX.recycleBinChanged)
writeDateInstant(DatabaseKDBXXML.ElemRecycleBinChanged, mDatabaseKDBX.recycleBinChanged)
writeUuid(DatabaseKDBXXML.ElemEntryTemplatesGroup, mDatabaseKDBX.entryTemplatesGroup)
writeObject(DatabaseKDBXXML.ElemEntryTemplatesGroupChanged, mDatabaseKDBX.entryTemplatesGroupChanged.date)
writeObject(DatabaseKDBXXML.ElemHistoryMaxItems, mDatabaseKDBX.historyMaxItems.toLong())
writeObject(DatabaseKDBXXML.ElemHistoryMaxSize, mDatabaseKDBX.historyMaxSize)
writeDateInstant(DatabaseKDBXXML.ElemEntryTemplatesGroupChanged, mDatabaseKDBX.entryTemplatesGroupChanged)
writeLong(DatabaseKDBXXML.ElemHistoryMaxItems, mDatabaseKDBX.historyMaxItems.toLong())
writeLong(DatabaseKDBXXML.ElemHistoryMaxSize, mDatabaseKDBX.historyMaxSize)
writeUuid(DatabaseKDBXXML.ElemLastSelectedGroup, mDatabaseKDBX.lastSelectedGroupUUID)
writeUuid(DatabaseKDBXXML.ElemLastTopVisibleGroup, mDatabaseKDBX.lastTopVisibleGroupUUID)
// Seem to work properly if always in meta
if (header!!.version.isBefore(FILE_VERSION_32_4))
if (header!!.version.isBefore(FILE_VERSION_40))
writeMetaBinaries()
writeCustomData(mDatabaseKDBX.customData)
@@ -309,7 +311,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
Log.e(TAG, "Unable to retrieve header", unknownKDF)
}
if (header.version.isBefore(FILE_VERSION_32_4)) {
if (header.version.isBefore(FILE_VERSION_40)) {
header.innerRandomStream = CrsAlgorithm.Salsa20
header.innerRandomStreamKey = ByteArray(32)
} else {
@@ -324,7 +326,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
throw DatabaseOutputException(e)
}
if (header.version.isBefore(FILE_VERSION_32_4)) {
if (header.version.isBefore(FILE_VERSION_40)) {
random.nextBytes(header.streamStartBytes)
}
@@ -353,19 +355,21 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun startGroup(group: GroupKDBX) {
xml.startTag(null, DatabaseKDBXXML.ElemGroup)
writeUuid(DatabaseKDBXXML.ElemUuid, group.id)
writeObject(DatabaseKDBXXML.ElemName, group.title)
writeObject(DatabaseKDBXXML.ElemNotes, group.notes)
writeObject(DatabaseKDBXXML.ElemIcon, group.icon.standard.id.toLong())
writeString(DatabaseKDBXXML.ElemName, group.title)
writeString(DatabaseKDBXXML.ElemNotes, group.notes)
writeLong(DatabaseKDBXXML.ElemIcon, group.icon.standard.id.toLong())
if (!group.icon.custom.isUnknown) {
writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.icon.custom.uuid)
}
writeTags(group.tags)
writePreviousParentGroup(group.previousParentGroup)
writeTimes(group)
writeObject(DatabaseKDBXXML.ElemIsExpanded, group.isExpanded)
writeObject(DatabaseKDBXXML.ElemGroupDefaultAutoTypeSeq, group.defaultAutoTypeSequence)
writeObject(DatabaseKDBXXML.ElemEnableAutoType, group.enableAutoType)
writeObject(DatabaseKDBXXML.ElemEnableSearching, group.enableSearching)
writeBoolean(DatabaseKDBXXML.ElemIsExpanded, group.isExpanded)
writeString(DatabaseKDBXXML.ElemGroupDefaultAutoTypeSeq, group.defaultAutoTypeSequence)
writeBoolean(DatabaseKDBXXML.ElemEnableAutoType, group.enableAutoType)
writeBoolean(DatabaseKDBXXML.ElemEnableSearching, group.enableSearching)
writeUuid(DatabaseKDBXXML.ElemLastTopVisibleEntry, group.lastTopVisibleEntry)
}
@@ -380,24 +384,26 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemEntry)
writeUuid(DatabaseKDBXXML.ElemUuid, entry.id)
writeObject(DatabaseKDBXXML.ElemIcon, entry.icon.standard.id.toLong())
writeLong(DatabaseKDBXXML.ElemIcon, entry.icon.standard.id.toLong())
if (!entry.icon.custom.isUnknown) {
writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.icon.custom.uuid)
}
writeObject(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor)
writeObject(DatabaseKDBXXML.ElemBgColor, entry.backgroundColor)
writeObject(DatabaseKDBXXML.ElemOverrideUrl, entry.overrideURL)
writeObject(DatabaseKDBXXML.ElemTags, entry.tags)
writeString(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor)
writeString(DatabaseKDBXXML.ElemBgColor, entry.backgroundColor)
writeString(DatabaseKDBXXML.ElemOverrideUrl, entry.overrideURL)
// Write quality check only if false
if (!entry.qualityCheck) {
writeBoolean(DatabaseKDBXXML.ElemQualityCheck, entry.qualityCheck)
}
writeTags(entry.tags)
writePreviousParentGroup(entry.previousParentGroup)
writeTimes(entry)
writeFields(entry.fields)
writeEntryBinaries(entry.binaries)
if (entry.containsCustomData()) {
writeCustomData(entry.customData)
}
writeCustomData(entry.customData)
writeAutoType(entry.autoType)
if (!isHistory) {
@@ -408,7 +414,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeObject(name: String, value: String, filterXmlChars: Boolean = false) {
private fun writeString(name: String, value: String, filterXmlChars: Boolean = false) {
var xmlString = value
xml.startTag(null, name)
@@ -422,38 +428,37 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeObject(name: String, value: Date) {
if (header!!.version.isBefore(FILE_VERSION_32_4)) {
writeObject(name, DatabaseKDBXXML.DateFormatter.format(value))
private fun writeDateInstant(name: String, value: DateInstant) {
val date = value.date
if (header!!.version.isBefore(FILE_VERSION_40)) {
writeString(name, DatabaseKDBXXML.DateFormatter.format(date))
} else {
val dt = DateTime(value)
val seconds = DateKDBXUtil.convertDateToKDBX4Time(dt)
val buf = longTo8Bytes(seconds)
val buf = longTo8Bytes(DateKDBXUtil.convertDateToKDBX4Time(DateTime(date)))
val b64 = String(Base64.encode(buf, BASE_64_FLAG))
writeObject(name, b64)
writeString(name, b64)
}
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeObject(name: String, value: Long) {
writeObject(name, value.toString())
private fun writeLong(name: String, value: Long) {
writeString(name, value.toString())
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeObject(name: String, value: Boolean?) {
private fun writeBoolean(name: String, value: Boolean?) {
val text: String = when {
value == null -> DatabaseKDBXXML.ValNull
value -> DatabaseKDBXXML.ValTrue
else -> DatabaseKDBXXML.ValFalse
}
writeObject(name, text)
writeString(name, text)
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeUuid(name: String, uuid: UUID) {
val data = uuidTo16Bytes(uuid)
writeObject(name, String(Base64.encode(data, BASE_64_FLAG)))
writeString(name, String(Base64.encode(data, BASE_64_FLAG)))
}
/*
@@ -514,34 +519,29 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.endTag(null, DatabaseKDBXXML.ElemBinaries)
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeObject(name: String, keyName: String, keyValue: String, valueName: String, valueValue: String) {
xml.startTag(null, name)
xml.startTag(null, keyName)
xml.text(safeXmlString(keyValue))
xml.endTag(null, keyName)
xml.startTag(null, valueName)
xml.text(safeXmlString(valueValue))
xml.endTag(null, valueName)
xml.endTag(null, name)
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeAutoType(autoType: AutoType) {
xml.startTag(null, DatabaseKDBXXML.ElemAutoType)
writeObject(DatabaseKDBXXML.ElemAutoTypeEnabled, autoType.enabled)
writeObject(DatabaseKDBXXML.ElemAutoTypeObfuscation, autoType.obfuscationOptions.toKotlinLong())
writeBoolean(DatabaseKDBXXML.ElemAutoTypeEnabled, autoType.enabled)
writeLong(DatabaseKDBXXML.ElemAutoTypeObfuscation, autoType.obfuscationOptions.toKotlinLong())
if (autoType.defaultSequence.isNotEmpty()) {
writeObject(DatabaseKDBXXML.ElemAutoTypeDefaultSeq, autoType.defaultSequence, true)
writeString(DatabaseKDBXXML.ElemAutoTypeDefaultSeq, autoType.defaultSequence, true)
}
for ((key, value) in autoType.entrySet()) {
writeObject(DatabaseKDBXXML.ElemAutoTypeItem, DatabaseKDBXXML.ElemWindow, key, DatabaseKDBXXML.ElemKeystrokeSequence, value)
xml.startTag(null, DatabaseKDBXXML.ElemAutoTypeItem)
xml.startTag(null, DatabaseKDBXXML.ElemWindow)
xml.text(safeXmlString(key))
xml.endTag(null, DatabaseKDBXXML.ElemWindow)
xml.startTag(null, DatabaseKDBXXML.ElemKeystrokeSequence)
xml.text(safeXmlString(value))
xml.endTag(null, DatabaseKDBXXML.ElemKeystrokeSequence)
xml.endTag(null, DatabaseKDBXXML.ElemAutoTypeItem)
}
xml.endTag(null, DatabaseKDBXXML.ElemAutoType)
@@ -592,7 +592,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
writeObject(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
}
@@ -632,43 +632,72 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun writeMemoryProtection(value: MemoryProtectionConfig) {
xml.startTag(null, DatabaseKDBXXML.ElemMemoryProt)
writeObject(DatabaseKDBXXML.ElemProtTitle, value.protectTitle)
writeObject(DatabaseKDBXXML.ElemProtUserName, value.protectUserName)
writeObject(DatabaseKDBXXML.ElemProtPassword, value.protectPassword)
writeObject(DatabaseKDBXXML.ElemProtURL, value.protectUrl)
writeObject(DatabaseKDBXXML.ElemProtNotes, value.protectNotes)
writeBoolean(DatabaseKDBXXML.ElemProtTitle, value.protectTitle)
writeBoolean(DatabaseKDBXXML.ElemProtUserName, value.protectUserName)
writeBoolean(DatabaseKDBXXML.ElemProtPassword, value.protectPassword)
writeBoolean(DatabaseKDBXXML.ElemProtURL, value.protectUrl)
writeBoolean(DatabaseKDBXXML.ElemProtNotes, value.protectNotes)
xml.endTag(null, DatabaseKDBXXML.ElemMemoryProt)
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeCustomData(customData: Map<String, String>) {
xml.startTag(null, DatabaseKDBXXML.ElemCustomData)
private fun writeCustomData(customData: CustomData) {
if (customData.isNotEmpty()) {
xml.startTag(null, DatabaseKDBXXML.ElemCustomData)
for ((key, value) in customData) {
writeObject(
DatabaseKDBXXML.ElemStringDictExItem,
DatabaseKDBXXML.ElemKey,
key,
DatabaseKDBXXML.ElemValue,
value
)
customData.doForEachItems { customDataItem ->
writeCustomDataItem(customDataItem)
}
xml.endTag(null, DatabaseKDBXXML.ElemCustomData)
}
}
private fun writeCustomDataItem(customDataItem: CustomDataItem) {
xml.startTag(null, DatabaseKDBXXML.ElemStringDictExItem)
xml.startTag(null, DatabaseKDBXXML.ElemKey)
xml.text(safeXmlString(customDataItem.key))
xml.endTag(null, DatabaseKDBXXML.ElemKey)
xml.startTag(null, DatabaseKDBXXML.ElemValue)
xml.text(safeXmlString(customDataItem.value))
xml.endTag(null, DatabaseKDBXXML.ElemValue)
customDataItem.lastModificationTime?.let { lastModificationTime ->
writeDateInstant(DatabaseKDBXXML.ElemLastModTime, lastModificationTime)
}
xml.endTag(null, DatabaseKDBXXML.ElemCustomData)
xml.endTag(null, DatabaseKDBXXML.ElemStringDictExItem)
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeTags(tags: Tags) {
if (!tags.isEmpty()) {
writeString(DatabaseKDBXXML.ElemTags, tags.toString())
}
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writePreviousParentGroup(previousParentGroup: UUID) {
if (!header!!.version.isBefore(FILE_VERSION_41)
&& previousParentGroup != DatabaseVersioned.UUID_ZERO) {
writeUuid(DatabaseKDBXXML.ElemPreviousParentGroup, previousParentGroup)
}
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeTimes(node: NodeKDBXInterface) {
xml.startTag(null, DatabaseKDBXXML.ElemTimes)
writeObject(DatabaseKDBXXML.ElemLastModTime, node.lastModificationTime.date)
writeObject(DatabaseKDBXXML.ElemCreationTime, node.creationTime.date)
writeObject(DatabaseKDBXXML.ElemLastAccessTime, node.lastAccessTime.date)
writeObject(DatabaseKDBXXML.ElemExpiryTime, node.expiryTime.date)
writeObject(DatabaseKDBXXML.ElemExpires, node.expires)
writeObject(DatabaseKDBXXML.ElemUsageCount, node.usageCount.toKotlinLong())
writeObject(DatabaseKDBXXML.ElemLocationChanged, node.locationChanged.date)
writeDateInstant(DatabaseKDBXXML.ElemLastModTime, node.lastModificationTime)
writeDateInstant(DatabaseKDBXXML.ElemCreationTime, node.creationTime)
writeDateInstant(DatabaseKDBXXML.ElemLastAccessTime, node.lastAccessTime)
writeDateInstant(DatabaseKDBXXML.ElemExpiryTime, node.expiryTime)
writeBoolean(DatabaseKDBXXML.ElemExpires, node.expires)
writeLong(DatabaseKDBXXML.ElemUsageCount, node.usageCount.toKotlinLong())
writeDateInstant(DatabaseKDBXXML.ElemLocationChanged, node.locationChanged)
xml.endTag(null, DatabaseKDBXXML.ElemTimes)
}
@@ -709,9 +738,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} catch (e: Exception) {
Log.e(TAG, "Unable to write custom icon", e)
} finally {
writeObject(DatabaseKDBXXML.ElemCustomIconItemData,
writeString(DatabaseKDBXXML.ElemCustomIconItemData,
String(Base64.encode(customImageData, BASE_64_FLAG)))
}
if (iconCustom.name.isNotEmpty()) {
writeString(DatabaseKDBXXML.ElemName, iconCustom.name)
}
iconCustom.lastModificationTime?.let { lastModificationTime ->
writeDateInstant(DatabaseKDBXXML.ElemLastModTime, lastModificationTime)
}
xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem)
}

View File

@@ -19,13 +19,9 @@
*/
package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.dateTo5Bytes
import com.kunzisoft.keepass.utils.uIntTo4Bytes
import com.kunzisoft.keepass.utils.uShortTo2Bytes
import com.kunzisoft.keepass.utils.writeStringToStream
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.utils.*
import java.io.IOException
import java.io.OutputStream
@@ -77,7 +73,7 @@ class GroupOutputKDB(private val mGroup: GroupKDB,
// Level
mOutputStream.write(LEVEL_FIELD_TYPE)
mOutputStream.write(LEVEL_FIELD_SIZE)
mOutputStream.write(uShortTo2Bytes(mGroup.level))
mOutputStream.write(uShortTo2Bytes(mGroup.getLevel()))
// Flags
mOutputStream.write(FLAGS_FIELD_TYPE)

View File

@@ -1,89 +0,0 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.search
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
class EntryKDBXSearchHandler(private val mSearchParametersKDBX: SearchParameters,
private val mListStorage: MutableList<EntryKDBX>)
: NodeHandler<EntryKDBX>() {
override fun operate(node: EntryKDBX): Boolean {
if (mSearchParametersKDBX.excludeExpired
&& node.isCurrentlyExpires) {
return true
}
if (searchStrings(node)) {
mListStorage.add(node)
return true
}
if (searchInGroupNames(node)) {
mListStorage.add(node)
return true
}
if (searchInUUID(node)) {
mListStorage.add(node)
return true
}
return true
}
private fun searchInGroupNames(entry: EntryKDBX): Boolean {
if (mSearchParametersKDBX.searchInGroupNames) {
val parent = entry.parent
if (parent != null) {
return parent.title
.contains(mSearchParametersKDBX.searchString, mSearchParametersKDBX.ignoreCase)
}
}
return false
}
private fun searchInUUID(entry: EntryKDBX): Boolean {
if (mSearchParametersKDBX.searchInUUIDs) {
return UuidUtil.toHexString(entry.id)
.contains(mSearchParametersKDBX.searchString, true)
}
return false
}
private fun searchStrings(entry: EntryKDBX): Boolean {
val iterator = EntrySearchStringIteratorKDBX(entry, mSearchParametersKDBX)
while (iterator.hasNext()) {
val stringValue = iterator.next()
if (stringValue.isNotEmpty()) {
if (stringValue.contains(mSearchParametersKDBX.searchString, mSearchParametersKDBX.ignoreCase)) {
return true
}
}
}
return false
}
}

View File

@@ -24,15 +24,67 @@ import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDB
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UuidUtil
class SearchHelper {
private var incrementEntry = 0
fun createVirtualGroupWithSearchResult(database: Database,
searchParameters: SearchParameters,
omitBackup: Boolean,
max: Int): Group? {
val searchGroup = database.createGroup()
searchGroup?.isVirtual = true
searchGroup?.title = "\"" + searchParameters.searchQuery + "\""
// Search all entries
incrementEntry = 0
database.rootGroup?.doForEachChild(
object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean {
if (incrementEntry >= max)
return false
if (entryContainsString(database, node, searchParameters)) {
searchGroup?.addChildEntry(node)
incrementEntry++
}
// Stop searching when we have max entries
return incrementEntry < max
}
},
object : NodeHandler<Group>() {
override fun operate(node: Group): Boolean {
return when {
incrementEntry >= max -> false
database.isGroupSearchable(node, omitBackup) -> true
else -> false
}
}
},
false)
return searchGroup
}
private fun entryContainsString(database: Database,
entry: Entry,
searchParameters: SearchParameters): Boolean {
// To search in field references
database.startManageEntry(entry)
// Search all strings in the entry
val searchFound = searchInEntry(entry, searchParameters)
database.stopManageEntry(entry)
return searchFound
}
companion object {
const val MAX_SEARCH_ENTRY = 10
@@ -70,75 +122,67 @@ class SearchHelper {
onDatabaseClosed.invoke()
}
}
}
private var incrementEntry = 0
/**
* Return true if the search query in search parameters is found in available parameters
*/
fun searchInEntry(entry: Entry,
searchParameters: SearchParameters): Boolean {
val searchQuery = searchParameters.searchQuery
// Entry don't contains string if the search string is empty
if (searchQuery.isEmpty())
return false
fun createVirtualGroupWithSearchResult(database: Database,
searchQuery: String,
searchParameters: SearchParameters,
omitBackup: Boolean,
max: Int): Group? {
val searchGroup = database.createGroup()
searchGroup?.isVirtual = true
searchGroup?.title = "\"" + searchQuery + "\""
// Search all entries
incrementEntry = 0
database.rootGroup?.doForEachChild(
object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean {
if (incrementEntry >= max)
return false
if (entryContainsString(node, searchQuery, searchParameters)) {
searchGroup?.addChildEntry(node)
incrementEntry++
}
// Stop searching when we have max entries
return incrementEntry < max
// Search all strings in the KDBX entry
if (searchParameters.searchInTitles) {
if (checkSearchQuery(entry.title, searchParameters))
return true
}
if (searchParameters.searchInUserNames) {
if (checkSearchQuery(entry.username, searchParameters))
return true
}
if (searchParameters.searchInPasswords) {
if (checkSearchQuery(entry.password, searchParameters))
return true
}
if (searchParameters.searchInUrls) {
if (checkSearchQuery(entry.url, searchParameters))
return true
}
if (searchParameters.searchInNotes) {
if (checkSearchQuery(entry.notes, searchParameters))
return true
}
if (searchParameters.searchInUUIDs) {
val hexString = UuidUtil.toHexString(entry.nodeId.id)
if (hexString != null && hexString.contains(searchQuery, true))
return true
}
if (searchParameters.searchInOther) {
entry.getExtraFields().forEach { field ->
if (field.name != OTP_FIELD
|| (field.name == OTP_FIELD && searchParameters.searchInOTP)) {
if (checkSearchQuery(field.protectedValue.toString(), searchParameters))
return true
}
},
object : NodeHandler<Group>() {
override fun operate(node: Group): Boolean {
return when {
incrementEntry >= max -> false
database.isGroupSearchable(node, omitBackup) -> true
else -> false
}
}
},
false)
return searchGroup
}
private fun entryContainsString(entry: Entry,
searchQuery: String,
searchParameters: SearchParameters): Boolean {
// Entry don't contains string if the search string is empty
if (searchQuery.isEmpty())
return false
// Search all strings in the entry
var iterator: Iterator<String>? = null
entry.entryKDB?.let {
iterator = EntrySearchStringIteratorKDB(it, searchParameters)
}
entry.entryKDBX?.let {
iterator = EntrySearchStringIteratorKDBX(it, searchParameters)
}
iterator?.let {
while (it.hasNext()) {
val currentString = it.next()
if (currentString.isNotEmpty()
&& currentString.contains(searchQuery, true)) {
return true
}
}
return false
}
private fun checkSearchQuery(stringToCheck: String, searchParameters: SearchParameters): Boolean {
/*
// TODO Search settings
var regularExpression = false
var ignoreCase = true
var removeAccents = true <- Too much time, to study
var excludeExpired = false
var searchOnlyInCurrentGroup = false
*/
return stringToCheck.isNotEmpty()
&& stringToCheck.contains(
searchParameters.searchQuery, true)
}
return false
}
}

View File

@@ -23,57 +23,15 @@ package com.kunzisoft.keepass.database.search
* Parameters for searching strings in the database.
*/
class SearchParameters {
var searchQuery: String = ""
var searchString: String = ""
var regularExpression = false
var searchInTitles = true
var searchInUserNames = true
var searchInPasswords = false
var searchInUrls = true
var searchInGroupNames = false
var searchInNotes = true
var searchInOTP = false
var searchInOther = true
var searchInUUIDs = false
var searchInTags = true
var ignoreCase = true
var ignoreExpired = false
var excludeExpired = false
constructor()
/**
* Copy search parameters
* @param source
*/
constructor(source: SearchParameters) {
this.regularExpression = source.regularExpression
this.searchInTitles = source.searchInTitles
this.searchInUserNames = source.searchInUserNames
this.searchInPasswords = source.searchInPasswords
this.searchInUrls = source.searchInUrls
this.searchInGroupNames = source.searchInGroupNames
this.searchInNotes = source.searchInNotes
this.searchInOTP = source.searchInOTP
this.searchInOther = source.searchInOther
this.searchInUUIDs = source.searchInUUIDs
this.searchInTags = source.searchInTags
this.ignoreCase = source.ignoreCase
this.ignoreExpired = source.ignoreExpired
this.excludeExpired = source.excludeExpired
}
fun setupNone() {
searchInTitles = false
searchInUserNames = false
searchInPasswords = false
searchInUrls = false
searchInGroupNames = false
searchInNotes = false
searchInOTP = false
searchInOther = false
searchInUUIDs = false
searchInTags = false
}
}

View File

@@ -1,60 +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.search;
import java.util.UUID;
import static com.kunzisoft.keepass.utils.StreamBytesUtilsKt.uuidTo16Bytes;
public class UuidUtil {
public static String toHexString(UUID uuid) {
if (uuid == null) { return null; }
byte[] buf = uuidTo16Bytes(uuid);
int len = buf.length;
if (len == 0) { return ""; }
StringBuilder sb = new StringBuilder();
short bt;
char high, low;
for (byte b : buf) {
bt = (short) (b & 0xFF);
high = (char) (bt >>> 4);
low = (char) (bt & 0x0F);
sb.append(byteToChar(high));
sb.append(byteToChar(low));
}
return sb.toString();
}
// Use short to represent unsigned byte
private static char byteToChar(char bt) {
if (bt >= 10) {
return (char)('A' + bt - 10);
}
else {
return (char)('0' + bt);
}
}
}

View File

@@ -1,87 +0,0 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.search.iterator
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.search.SearchParameters
import java.util.NoSuchElementException
class EntrySearchStringIteratorKDB(
private val mEntry: EntryKDB,
private val mSearchParameters: SearchParameters)
: Iterator<String> {
private var current = 0
private val currentString: String
get() {
return when (current) {
title -> mEntry.title
url -> mEntry.url
username -> mEntry.username
notes -> mEntry.notes
else -> ""
}
}
override fun hasNext(): Boolean {
return current < maxEntries
}
override fun next(): String {
// Past the end of the list
if (current == maxEntries) {
throw NoSuchElementException("Past final string")
}
useSearchParameters()
val str = currentString
current++
return str
}
private fun useSearchParameters() {
var found = false
while (!found) {
found = when (current) {
title -> mSearchParameters.searchInTitles
url -> mSearchParameters.searchInUrls
username -> mSearchParameters.searchInUserNames
notes -> mSearchParameters.searchInNotes
else -> true
}
if (!found) {
current++
}
}
}
companion object {
private const val title = 0
private const val url = 1
private const val username = 2
private const val notes = 3
private const val maxEntries = 4
}
}

View File

@@ -1,84 +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.search.iterator
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.otp.OtpEntryFields
import java.util.*
import kotlin.collections.Map.Entry
class EntrySearchStringIteratorKDBX(
entry: EntryKDBX,
private val mSearchParameters: SearchParameters)
: Iterator<String> {
private var mCurrent: String? = null
private var mSetIterator: Iterator<Entry<String, ProtectedString>>? = null
init {
mSetIterator = entry.fields.entries.iterator()
advance()
}
override fun hasNext(): Boolean {
return mCurrent != null
}
override fun next(): String {
if (mCurrent == null) {
throw NoSuchElementException("Past the end of the list.")
}
val next:String = mCurrent!!
advance()
return next
}
private fun advance() {
mSetIterator?.let {
while (it.hasNext()) {
val entry = it.next()
val key = entry.key
if (searchInField(key)) {
mCurrent = entry.value.toString()
return
}
}
}
mCurrent = null
}
private fun searchInField(key: String): Boolean {
return when (key) {
EntryKDBX.STR_TITLE -> mSearchParameters.searchInTitles
EntryKDBX.STR_USERNAME -> mSearchParameters.searchInUserNames
EntryKDBX.STR_PASSWORD -> mSearchParameters.searchInPasswords
EntryKDBX.STR_URL -> mSearchParameters.searchInUrls
EntryKDBX.STR_NOTES -> mSearchParameters.searchInNotes
OtpEntryFields.OTP_FIELD -> mSearchParameters.searchInOTP
else -> mSearchParameters.searchInOther
}
}
}

View File

@@ -93,7 +93,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
value
} else {
TokenCalculator.HOTP_INITIAL_COUNTER
throw IllegalArgumentException()
throw NumberFormatException()
}
}
@@ -186,7 +186,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
}
companion object {
const val MIN_HOTP_COUNTER = 1
const val MIN_HOTP_COUNTER = 0
const val MAX_HOTP_COUNTER = Long.MAX_VALUE
const val MIN_TOTP_PERIOD = 1

View File

@@ -295,22 +295,30 @@ object OtpEntryFields {
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
}
otpElement.type = OtpType.TOTP
if (lengthField != null) {
otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS
}
if (lengthField != null) {
otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS
}
if (periodField != null) {
otpElement.period = periodField.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
}
if (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
}
}
} catch (exception: Exception) {
return false
}
otpElement.type = OtpType.TOTP
return true
}
@@ -321,10 +329,10 @@ object OtpEntryFields {
return try {
// KeeOtp string format
val query = breakDownKeyValuePairs(plainText)
otpElement.type = OtpType.TOTP
otpElement.setBase32Secret(query[SEED_KEY] ?: "")
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.type = OtpType.TOTP
true
} catch (exception: Exception) {
false
@@ -351,6 +359,7 @@ object OtpEntryFields {
// malformed
return false
}
otpElement.type = OtpType.TOTP
otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
matcher.group(2)?.let { secondMatcher ->
try {
@@ -365,7 +374,6 @@ object OtpEntryFields {
} catch (exception: Exception) {
return false
}
otpElement.type = OtpType.TOTP
return true
}
@@ -374,6 +382,7 @@ object OtpEntryFields {
val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD)
val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD)
val secretBase64Field = getField(HMACOTP_SECRET_BASE64_FIELD)
val secretCounterField = getField(HMACOTP_SECRET_COUNTER_FIELD)
try {
when {
secretField != null -> otpElement.setUTF8Secret(secretField)
@@ -382,16 +391,13 @@ object OtpEntryFields {
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
else -> return false
}
val secretCounterField = getField(HMACOTP_SECRET_COUNTER_FIELD)
otpElement.type = OtpType.HOTP
if (secretCounterField != null) {
otpElement.counter = secretCounterField.toLongOrNull() ?: HOTP_INITIAL_COUNTER
}
} catch (exception: Exception) {
return false
}
otpElement.type = OtpType.HOTP
return true
}

View File

@@ -1,8 +1,7 @@
package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.*
import android.net.Uri
import android.os.Binder
import android.os.IBinder
@@ -10,7 +9,6 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import kotlinx.coroutines.*
class AdvancedUnlockNotificationService : NotificationService() {
@@ -18,9 +16,6 @@ class AdvancedUnlockNotificationService : NotificationService() {
private var mActionTaskBinder = AdvancedUnlockBinder()
private var notificationTimeoutMilliSecs: Long = 0
private var mTimerJob: Job? = null
inner class AdvancedUnlockBinder: Binder() {
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
@@ -50,94 +45,79 @@ class AdvancedUnlockNotificationService : NotificationService() {
return getString(R.string.advanced_unlock)
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return mActionTaskBinder
override fun onCreate() {
super.onCreate()
mTempCipherDao = ArrayList()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
val deleteIntent = Intent(this, AdvancedUnlockNotificationService::class.java).apply {
action = ACTION_REMOVE_KEYS
}
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
4577, Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION), 0)
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
val notificationBuilder = buildNewNotification().apply {
val notificationBuilder = buildNewNotification().apply {
setSmallIcon(if (biometricUnlockEnabled) {
R.drawable.notification_ic_fingerprint_unlock_24dp
} else {
R.drawable.notification_ic_device_unlock_24dp
})
intent?.let {
setContentTitle(getString(R.string.advanced_unlock))
}
setContentTitle(getString(R.string.advanced_unlock))
setContentText(getString(R.string.advanced_unlock_tap_delete))
setContentIntent(pendingDeleteIntent)
// Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent)
}
when (intent?.action) {
ACTION_TIMEOUT -> {
notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
// Not necessarily a foreground service
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
val maxPos = 100
val posDurationMills = notificationTimeoutMilliSecs / maxPos
for (pos in maxPos downTo 0) {
notificationBuilder.setProgress(maxPos, pos, false)
startForeground(notificationId, notificationBuilder.build())
delay(posDurationMills)
if (pos <= 0) {
stopSelf()
}
}
notificationManager?.cancel(notificationId)
mTimerJob = null
cancel()
}
} else {
startForeground(notificationId, notificationBuilder.build())
}
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
// Not necessarily a foreground service
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
defineTimerJob(notificationBuilder, notificationTimeoutMilliSecs) {
sendBroadcast(Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION))
}
ACTION_REMOVE_KEYS -> {
stopSelf()
}
else -> {}
} else {
startForeground(notificationId, notificationBuilder.build())
}
return START_STICKY
return mActionTaskBinder
}
override fun onCreate() {
super.onCreate()
mTempCipherDao = ArrayList()
override fun onUnbind(intent: Intent?): Boolean {
stopSelf()
return super.onUnbind(intent)
}
override fun onDestroy() {
mTempCipherDao.clear()
mTimerJob?.cancel()
super.onDestroy()
}
class AdvancedUnlockReceiver(var removeKeyAction: () -> Unit): BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
intent.action?.let {
when (it) {
REMOVE_ADVANCED_UNLOCK_KEY_ACTION -> {
removeKeyAction.invoke()
}
}
}
}
}
companion object {
private const val CHANNEL_ADVANCED_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
const val REMOVE_ADVANCED_UNLOCK_KEY_ACTION = "com.kunzisoft.keepass.REMOVE_ADVANCED_UNLOCK_KEY"
private const val ACTION_TIMEOUT = "ACTION_TIMEOUT"
private const val ACTION_REMOVE_KEYS = "ACTION_REMOVE_KEYS"
fun startServiceForTimeout(context: Context) {
if (PreferencesUtil.isTempAdvancedUnlockEnable(context)) {
context.startService(Intent(context, AdvancedUnlockNotificationService::class.java).apply {
action = ACTION_TIMEOUT
})
}
// Only one service connection
fun bindService(context: Context, serviceConnection: ServiceConnection, flags: Int) {
context.bindService(Intent(context,
AdvancedUnlockNotificationService::class.java),
serviceConnection,
flags)
}
fun stopService(context: Context) {
context.stopService(Intent(context, AdvancedUnlockNotificationService::class.java))
fun unbindService(context: Context, serviceConnection: ServiceConnection) {
context.unbindService(serviceConnection)
}
}
}

View File

@@ -26,7 +26,6 @@ import android.net.Uri
import android.os.Binder
import android.os.IBinder
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
@@ -34,6 +33,7 @@ import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.*
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
@@ -173,7 +173,8 @@ class AttachmentFileNotificationService: LockNotificationService() {
putExtra(FILE_URI_KEY, attachmentNotification.uri)
}, PendingIntent.FLAG_CANCEL_CURRENT)
val fileName = DocumentFile.fromSingleUri(this, attachmentNotification.uri)?.name ?: ""
val fileName = UriUtil.getFileData(this, attachmentNotification.uri)?.name
?: attachmentNotification.uri.path
val builder = buildNewNotification().apply {
when (attachmentNotification.entryAttachmentState.streamDirection) {

View File

@@ -37,8 +37,6 @@ class ClipboardEntryNotificationService : LockNotificationService() {
override val notificationId = 485
private var mEntryInfo: EntryInfo? = null
private var clipboardHelper: ClipboardHelper? = null
private var notificationTimeoutMilliSecs: Long = 0
private var cleanCopyNotificationTimerTask: Thread? = null
override fun retrieveChannelId(): String {
return CHANNEL_CLIPBOARD_ID
@@ -69,16 +67,13 @@ class ClipboardEntryNotificationService : LockNotificationService() {
// Get entry info from intent
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
//Get settings
notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
when {
intent == null -> Log.w(TAG, "null intent")
ACTION_NEW_NOTIFICATION == intent.action -> {
newNotification(mEntryInfo?.title, constructListOfField(intent))
}
ACTION_CLEAN_CLIPBOARD == intent.action -> {
stopTask(cleanCopyNotificationTimerTask)
mTimerJob?.cancel()
cleanClipboard()
stopNotificationAndSendLockIfNeeded()
}
@@ -121,7 +116,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
}
private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) {
stopTask(cleanCopyNotificationTimerTask)
mTimerJob?.cancel()
val builder = buildNewNotification()
.setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp)
@@ -147,7 +142,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
}
private fun copyField(fieldToCopy: ClipboardEntryNotificationField, nextFields: ArrayList<ClipboardEntryNotificationField>) {
stopTask(cleanCopyNotificationTimerTask)
mTimerJob?.cancel()
try {
var generatedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
@@ -170,40 +165,25 @@ class ClipboardEntryNotificationService : LockNotificationService() {
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT)
builder.setDeleteIntent(cleanPendingIntent)
val myNotificationId = notificationId
//Get settings
val notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
if (notificationTimeoutMilliSecs != NEVER) {
cleanCopyNotificationTimerTask = Thread {
val maxPos = 100
val posDurationMills = notificationTimeoutMilliSecs / maxPos
for (pos in maxPos downTo 0) {
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
// New auto generated value
if (generatedValue != newGeneratedValue) {
generatedValue = newGeneratedValue
clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
}
builder.setProgress(maxPos, pos, false)
notificationManager?.notify(myNotificationId, builder.build())
try {
Thread.sleep(posDurationMills)
} catch (e: InterruptedException) {
break
}
if (pos <= 0) {
stopNotificationAndSendLockIfNeeded()
}
defineTimerJob(builder, notificationTimeoutMilliSecs, {
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
// New auto generated value
if (generatedValue != newGeneratedValue) {
generatedValue = newGeneratedValue
clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
}
stopTask(cleanCopyNotificationTimerTask)
notificationManager?.cancel(myNotificationId)
}) {
stopNotificationAndSendLockIfNeeded()
// Clean password only if no next field
if (nextFields.size <= 0)
cleanClipboard()
}
cleanCopyNotificationTimerTask?.start()
} else {
// No timer
notificationManager?.notify(myNotificationId, builder.build())
notificationManager?.notify(notificationId, builder.build())
}
} catch (e: Exception) {
@@ -228,10 +208,6 @@ class ClipboardEntryNotificationService : LockNotificationService() {
override fun onDestroy() {
cleanClipboard()
stopTask(cleanCopyNotificationTimerTask)
cleanCopyNotificationTimerTask = null
super.onDestroy()
}

View File

@@ -200,10 +200,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (intentAction == null && !mDatabase.loaded) {
stopSelf()
}
if (intentAction == ACTION_DATABASE_CLOSE) {
// Send lock action
sendBroadcast(Intent(LOCK_ACTION))
}
val actionRunnable: ActionRunnable? = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
@@ -378,10 +374,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
ReadOnlyHelper.putReadOnlyInIntent(this, mDatabase.isReadOnly)
},
PendingIntent.FLAG_UPDATE_CURRENT)
val deleteIntent = Intent(this, DatabaseTaskNotificationService::class.java).apply {
action = ACTION_DATABASE_CLOSE
}
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
4576, Intent(LOCK_ACTION), 0)
// Add actions in notifications
notificationBuilder.apply {
setContentText(mDatabase.name + " (" + mDatabase.version + ")")
@@ -877,7 +871,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
const val ACTION_DATABASE_CLOSE = "ACTION_DATABASE_CLOSE"
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"

View File

@@ -35,8 +35,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION
class KeyboardEntryNotificationService : LockNotificationService() {
override val notificationId = 486
private var cleanNotificationTimerTask: Thread? = null
private var notificationTimeoutMilliSecs: Long = 0
private var mNotificationTimeoutMilliSecs: Long = 0
private var pendingDeleteIntent: PendingIntent? = null
@@ -61,7 +60,7 @@ class KeyboardEntryNotificationService : LockNotificationService() {
super.onStartCommand(intent, flags, startId)
//Get settings
notificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this)
mNotificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this)
.getString(getString(R.string.keyboard_entry_timeout_key),
getString(R.string.timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT
@@ -107,27 +106,12 @@ class KeyboardEntryNotificationService : LockNotificationService() {
notificationManager?.cancel(notificationId)
notificationManager?.notify(notificationId, builder.build())
stopTask(cleanNotificationTimerTask)
// Timeout only if notification clear is available
if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) {
if (notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
cleanNotificationTimerTask = Thread {
val maxPos = 100
val posDurationMills = notificationTimeoutMilliSecs / maxPos
for (pos in maxPos downTo 0) {
builder.setProgress(maxPos, pos, false)
notificationManager?.notify(notificationId, builder.build())
try {
Thread.sleep(posDurationMills)
} catch (e: InterruptedException) {
break
}
if (pos <= 0) {
stopNotificationAndSendLockIfNeeded()
}
}
if (mNotificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
defineTimerJob(builder, mNotificationTimeoutMilliSecs) {
stopNotificationAndSendLockIfNeeded()
}
cleanNotificationTimerTask?.start()
}
}
}
@@ -142,8 +126,6 @@ class KeyboardEntryNotificationService : LockNotificationService() {
// Remove the entry from the keyboard
MagikIME.removeEntry(this)
stopTask(cleanNotificationTimerTask)
cleanNotificationTimerTask = null
pendingDeleteIntent?.cancel()
super.onDestroy()

View File

@@ -26,8 +26,9 @@ import com.kunzisoft.keepass.utils.unregisterLockReceiver
abstract class LockNotificationService : NotificationService() {
private var onStart: Boolean = false
private var mLockReceiver: LockReceiver? = null
private var mLockReceiver: LockReceiver = LockReceiver {
actionOnLock()
}
protected open fun actionOnLock() {
// Stop the service in all cases
@@ -36,35 +37,17 @@ abstract class LockNotificationService : NotificationService() {
override fun onCreate() {
super.onCreate()
// Register a lock receiver to stop notification service when lock on keyboard is performed
mLockReceiver = LockReceiver {
if (onStart)
actionOnLock()
}
registerLockReceiver(mLockReceiver)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
onStart = true
return super.onStartCommand(intent, flags, startId)
}
protected fun stopTask(task: Thread?) {
if (task != null && task.isAlive)
task.interrupt()
}
override fun onTaskRemoved(rootIntent: Intent?) {
notificationManager?.cancel(notificationId)
stopSelf()
super.onTaskRemoved(rootIntent)
}
override fun onDestroy() {
unregisterLockReceiver(mLockReceiver)
super.onDestroy()
}
}

View File

@@ -11,6 +11,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.Stylish
import kotlinx.coroutines.*
abstract class NotificationService : Service() {
@@ -18,6 +19,8 @@ abstract class NotificationService : Service() {
protected var notificationManager: NotificationManagerCompat? = null
private var colorNotificationAccent: Int = 0
protected var mTimerJob: Job? = null
protected abstract val notificationId: Int
override fun onBind(intent: Intent): IBinder? {
@@ -71,7 +74,38 @@ abstract class NotificationService : Service() {
}
}
protected fun defineTimerJob(builder: NotificationCompat.Builder,
timeoutMilliseconds: Long,
actionAfterASecond: (() -> Unit)? = null,
actionEnd: () -> Unit) {
mTimerJob?.cancel()
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
if (timeoutMilliseconds > 0) {
val timeoutInSeconds = timeoutMilliseconds / 1000L
for (currentTime in timeoutInSeconds downTo 0) {
actionAfterASecond?.invoke()
builder.setProgress(100,
(currentTime * 100 / timeoutInSeconds).toInt(),
false)
startForeground(notificationId, builder.build())
delay(1000)
if (currentTime <= 0) {
actionEnd()
}
}
} else {
// If timeout is 0, run action once
actionEnd()
}
notificationManager?.cancel(notificationId)
mTimerJob = null
cancel()
}
}
override fun onDestroy() {
mTimerJob?.cancel()
mTimerJob = null
notificationManager?.cancel(notificationId)
super.onDestroy()

View File

@@ -20,9 +20,12 @@
package com.kunzisoft.keepass.settings
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
@@ -30,4 +33,31 @@ class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
// Load the preferences from an XML resource
setPreferencesFromResource(R.xml.preferences_keyboard, rootKey)
}
override fun onDisplayPreferenceDialog(preference: Preference?) {
var otherDialogFragment = false
var dialogFragment: DialogFragment? = null
// Main Preferences
when (preference?.key) {
getString(R.string.keyboard_entry_timeout_key) -> {
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
}
else -> otherDialogFragment = true
}
if (dialogFragment != null) {
dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
}
// Could not be handled here. Try with the super method.
else if (otherDialogFragment) {
super.onDisplayPreferenceDialog(preference)
}
}
companion object {
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
}
}

View File

@@ -30,6 +30,7 @@ import android.view.autofill.AutofillManager
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import androidx.preference.ListPreference
import androidx.preference.Preference
@@ -44,8 +45,8 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
import com.kunzisoft.keepass.utils.UriUtil
@@ -90,6 +91,20 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
true
}
findPreference<Preference>(getString(R.string.import_app_properties_key))?.setOnPreferenceClickListener { _ ->
(activity as? SettingsActivity?)?.apply {
importAppProperties()
}
true
}
findPreference<Preference>(getString(R.string.export_app_properties_key))?.setOnPreferenceClickListener { _ ->
(activity as? SettingsActivity?)?.apply {
exportAppProperties()
}
true
}
}
}
@@ -358,7 +373,6 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
})
}
AdvancedUnlockNotificationService.stopService(activity.applicationContext)
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
}
.setNegativeButton(resources.getString(android.R.string.cancel)
@@ -388,10 +402,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
Stylish.assignStyle(activity, styleIdString)
// 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)
relaunchCurrentScreen()
}
}
styleEnabled
@@ -399,10 +410,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
findPreference<ListPreference>(getString(R.string.setting_style_brightness_key))?.setOnPreferenceChangeListener { _, _ ->
(activity as? SettingsActivity?)?.apply {
keepCurrentScreen()
startActivity(intent)
finish()
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
relaunchCurrentScreen()
}
true
}
@@ -440,6 +448,31 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
override fun onDisplayPreferenceDialog(preference: Preference?) {
var otherDialogFragment = false
var dialogFragment: DialogFragment? = null
// Main Preferences
when (preference?.key) {
getString(R.string.app_timeout_key),
getString(R.string.clipboard_timeout_key),
getString(R.string.temp_advanced_unlock_timeout_key) -> {
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
}
else -> otherDialogFragment = true
}
if (dialogFragment != null) {
dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
}
// Could not be handled here. Try with the super method.
else if (otherDialogFragment) {
super.onDisplayPreferenceDialog(preference)
}
}
override fun onResume() {
super.onResume()
activity?.let { activity ->
@@ -470,7 +503,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
companion object {
private const val REQUEST_CODE_AUTOFILL = 5201
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
}
}

View File

@@ -576,7 +576,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
}
companion object {
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
}
}

View File

@@ -21,14 +21,17 @@ package com.kunzisoft.keepass.settings
import android.app.backup.BackupManager
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import android.net.Uri
import android.util.Log
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.timeout.TimeoutHelper
import java.util.*
@@ -134,28 +137,48 @@ object PreferencesUtil {
}
fun getStyle(context: Context): String {
val stylishPrefKey = context.getString(R.string.setting_style_key)
val defaultStyleString = context.getString(R.string.list_style_name_light)
val defaultStyleString = Stylish.defaultStyle(context)
val styleString = PreferenceManager.getDefaultSharedPreferences(context)
.getString(stylishPrefKey, defaultStyleString)
.getString(context.getString(R.string.setting_style_key), defaultStyleString)
?: defaultStyleString
return Stylish.retrieveEquivalentLightStyle(context, styleString)
// Return the system style
return Stylish.retrieveEquivalentSystemStyle(context, styleString)
}
fun setStyle(context: Context, styleString: String) {
var tempThemeString = styleString
if (BuildConfig.CLOSED_STORE || !Education.isEducationScreenReclickedPerformed(context)) {
if (tempThemeString in BuildConfig.STYLES_DISABLED) {
tempThemeString = Stylish.defaultStyle(context)
}
}
// Store light style to show selection in array list
tempThemeString = Stylish.retrieveEquivalentLightStyle(context, tempThemeString)
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putString(context.getString(R.string.setting_style_key), tempThemeString)
.apply()
Stylish.load(context)
}
fun getStyleBrightness(context: Context): String? {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.setting_style_brightness_key),
context.resources.getString(R.string.list_style_brightness_follow_system))
context.getString(R.string.list_style_brightness_follow_system))
}
/**
* Retrieve the text size in % (1 for 100%)
*/
fun getListTextSize(context: Context): Float {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val listSizeString = prefs.getString(context.getString(R.string.list_size_key),
context.getString(R.string.list_size_string_medium))
val index = context.resources.getStringArray(R.array.list_size_string_values).indexOf(listSizeString)
val index = try {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val listSizeString = prefs.getString(context.getString(R.string.list_size_key),
context.getString(R.string.list_size_string_medium))
context.resources.getStringArray(R.array.list_size_string_values).indexOf(listSizeString)
} catch (e: Exception) {
1
}
val typedArray = context.resources.obtainTypedArray(R.array.list_size_values)
val listSize = typedArray.getFloat(index, 1.0F)
typedArray.recycle()
@@ -289,11 +312,13 @@ object PreferencesUtil {
}
fun getListSort(context: Context): SortNodeEnum {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.getString(context.getString(R.string.sort_node_key),
SortNodeEnum.DB.name)?.let {
return SortNodeEnum.valueOf(it)
}
try {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.getString(context.getString(R.string.sort_node_key),
SortNodeEnum.DB.name)?.let {
return SortNodeEnum.valueOf(it)
}
} catch (e: Exception) {}
return SortNodeEnum.DB
}
@@ -517,4 +542,133 @@ object PreferencesUtil {
.putStringSet(context.getString(R.string.autofill_web_domain_blocklist_key), setItems)
.apply()
}
fun getAppProperties(context: Context): Properties {
val properties = Properties()
for ((name, value) in PreferenceManager.getDefaultSharedPreferences(context).all) {
properties[name] = value.toString()
}
for ((name, value) in Education.getEducationSharedPreferences(context).all) {
properties[name] = value.toString()
}
return properties
}
private fun getStringSetFromProperties(value: String): Set<String> {
return value.removePrefix("[")
.removeSuffix("]")
.split(", ")
.toSet()
}
private fun putPropertiesInPreferences(properties: Properties,
preferences: SharedPreferences,
putProperty: (editor: SharedPreferences.Editor,
name: String,
value: String) -> Unit) {
preferences.edit().apply {
for ((name, value) in properties) {
try {
putProperty(this, name as String, value as String)
} catch (e:Exception) {
Log.e("PreferencesUtil", "Error when trying to parse app property $name=$value", e)
}
}
}.apply()
}
fun setAppProperties(context: Context, properties: Properties) {
putPropertiesInPreferences(properties,
PreferenceManager.getDefaultSharedPreferences(context)) { editor, name, value ->
when (name) {
context.getString(R.string.allow_no_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.delete_entered_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_read_only_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_auto_save_database_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.omit_backup_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.auto_focus_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.subdomain_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.app_timeout_key) -> editor.putString(name, value.toLong().toString())
context.getString(R.string.lock_database_screen_off_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.lock_database_back_root_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.lock_database_show_button_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.password_length_key) -> editor.putInt(name, value.toInt())
context.getString(R.string.list_password_generator_options_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.allow_copy_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.remember_database_locations_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.show_recent_files_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_broken_locations_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.remember_keyfile_locations_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.biometric_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.device_credential_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.biometric_auto_open_prompt_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.temp_advanced_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.temp_advanced_unlock_timeout_key) -> editor.putString(name, value.toLong().toString())
context.getString(R.string.magic_keyboard_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.clipboard_notifications_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.clear_clipboard_notification_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.clipboard_timeout_key) -> editor.putString(name, value.toLong().toString())
context.getString(R.string.settings_autofill_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_notification_entry_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString())
context.getString(R.string.keyboard_selection_entry_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_search_share_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_auto_go_action_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_key_vibrate_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_key_sound_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_database_credentials_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_auto_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_ask_to_save_data_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.autofill_application_id_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
context.getString(R.string.autofill_web_domain_blocklist_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
context.getString(R.string.setting_style_key) -> setStyle(context, value)
context.getString(R.string.setting_style_brightness_key) -> editor.putString(name, value)
context.getString(R.string.setting_icon_pack_choose_key) -> editor.putString(name, value)
context.getString(R.string.list_entries_show_username_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.list_groups_show_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.list_size_key) -> editor.putString(name, value)
context.getString(R.string.monospace_font_fields_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.show_uuid_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.sort_node_key) -> editor.putString(name, value)
context.getString(R.string.sort_group_before_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.sort_ascending_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.sort_recycle_bin_bottom_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.allow_copy_password_first_time_key) -> editor.putBoolean(name, value.toBoolean())
}
}
putPropertiesInPreferences(properties,
Education.getEducationSharedPreferences(context)) { editor, name, value ->
when (name) {
context.getString(R.string.education_create_db_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_select_db_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_unlock_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_read_only_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_biometric_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_new_node_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_sort_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_lock_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_copy_username_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_entry_edit_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_password_generator_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_entry_new_field_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_add_attachment_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_setup_OTP_key) -> editor.putBoolean(name, value.toBoolean())
}
}
}
}

View File

@@ -24,22 +24,27 @@ import android.app.backup.BackupManager
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import java.util.*
open class SettingsActivity
: LockingActivity(),
@@ -48,6 +53,8 @@ open class SettingsActivity
PasswordEncodingDialogFragment.Listener {
private var backupManager: BackupManager? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var appPropertiesFileCreationRequestCode: Int? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var toolbar: Toolbar? = null
@@ -70,6 +77,8 @@ open class SettingsActivity
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
toolbar = findViewById(R.id.toolbar)
mExternalFileHelper = ExternalFileHelper(this)
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
toolbar?.setTitle(R.string.settings)
else
@@ -216,6 +225,13 @@ open class SettingsActivity
hideOrShowLockButton(key)
}
fun relaunchCurrentScreen() {
keepCurrentScreen()
startActivity(intent)
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
/**
* To keep the current screen when activity is reloaded
*/
@@ -235,6 +251,58 @@ open class SettingsActivity
replaceFragment(key, reload)
}
fun importAppProperties() {
mExternalFileHelper?.openDocument()
}
fun exportAppProperties() {
appPropertiesFileCreationRequestCode = mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name))
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Import app properties result
try {
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { selectedfileUri ->
selectedfileUri?.let { uri ->
val appProperties = Properties()
contentResolver?.openInputStream(uri)?.use { inputStream ->
appProperties.load(inputStream)
}
PreferencesUtil.setAppProperties(this, appProperties)
// Restart the current activity
relaunchCurrentScreen()
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
}
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
Log.e(TAG, "Unable to import app properties", e)
}
// Export app properties result
try {
if (requestCode == appPropertiesFileCreationRequestCode) {
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
createdFileUri?.let { uri ->
contentResolver?.openOutputStream(uri)?.use { outputStream ->
PreferencesUtil
.getAppProperties(this)
.store(outputStream, getString(R.string.description_app_properties))
}
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
}
}
appPropertiesFileCreationRequestCode = null
}
} catch (e: Exception) {
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
Log.e(LockingActivity.TAG, "Unable to export app properties", e)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
@@ -244,6 +312,8 @@ open class SettingsActivity
companion object {
private val TAG = SettingsActivity::class.java.name
private const val SHOW_LOCK = "SHOW_LOCK"
private const val TITLE_KEY = "TITLE_KEY"
private const val TAG_NESTED = "TAG_NESTED"

View File

@@ -0,0 +1,77 @@
/*
* 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.content.res.TypedArray
import android.util.AttributeSet
import androidx.preference.DialogPreference
import com.kunzisoft.keepass.R
class DurationDialogPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
private var mDuration: Long = 0L
override fun getDialogLayoutResource(): Int {
return R.layout.pref_dialog_duration
}
/**
* Get current duration of preference
*/
fun getDuration(): Long {
return if (mDuration >= 0) mDuration else -1
}
/**
* Assign [duration] of preference
*/
fun setDuration(duration: Long) {
persistString(duration.toString())
notifyChanged()
}
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
if (restorePersistedValue) {
mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration
} else {
mDuration = defaultValue?.toString()?.toLongOrNull() ?: mDuration
persistString(mDuration.toString())
}
}
override fun onGetDefaultValue(a: TypedArray?, index: Int): Any {
return try {
a?.getString(index)?.toLongOrNull() ?: mDuration
} catch (e: Exception) {
mDuration
}
}
// Was previously a string
override fun persistString(value: String?): Boolean {
mDuration = value?.toLongOrNull() ?: mDuration
return super.persistString(value)
}
}

View File

@@ -0,0 +1,180 @@
/*
* 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.settings.preferencedialogfragment
import android.os.Bundle
import android.view.View
import android.widget.NumberPicker
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.preference.DurationDialogPreference
class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
private var mEnabled = true
private var mDays = 0
private var mHours = 0
private var mMinutes = 0
private var mSeconds = 0
private var daysNumberPicker: NumberPicker? = null
private var hoursNumberPicker: NumberPicker? = null
private var minutesNumberPicker: NumberPicker? = null
private var secondsNumberPicker: NumberPicker? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// To get items from saved instance state
if (savedInstanceState != null
&& savedInstanceState.containsKey(ENABLE_KEY)
&& savedInstanceState.containsKey(DAYS_KEY)
&& savedInstanceState.containsKey(HOURS_KEY)
&& savedInstanceState.containsKey(MINUTES_KEY)
&& savedInstanceState.containsKey(SECONDS_KEY)) {
mEnabled = savedInstanceState.getBoolean(ENABLE_KEY)
mDays = savedInstanceState.getInt(DAYS_KEY)
mHours = savedInstanceState.getInt(HOURS_KEY)
mMinutes = savedInstanceState.getInt(MINUTES_KEY)
mSeconds = savedInstanceState.getInt(SECONDS_KEY)
} else {
val currentPreference = preference
if (currentPreference is DurationDialogPreference) {
durationToDaysHoursMinutesSeconds(currentPreference.getDuration())
}
}
}
private fun durationToDaysHoursMinutesSeconds(duration: Long) {
if (duration < 0) {
mDays = 0
mHours = 0
mMinutes = 0
mSeconds = 0
} else {
mDays = (duration / (24L * 60L * 60L * 1000L)).toInt()
val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L
mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt()
val hoursMilliseconds = mHours * 60L * 60L * 1000L
mMinutes = ((duration - daysMilliseconds - hoursMilliseconds) / (60L * 1000L)).toInt()
val minutesMilliseconds = mMinutes * 60L * 1000L
mSeconds = ((duration - daysMilliseconds - hoursMilliseconds - minutesMilliseconds) / (1000L)).toInt()
}
}
private fun assignValuesInViews() {
daysNumberPicker?.value = mDays
hoursNumberPicker?.value = mHours
minutesNumberPicker?.value = mMinutes
secondsNumberPicker?.value = mSeconds
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
daysNumberPicker = view.findViewById<NumberPicker>(R.id.days_picker).apply {
minValue = 0
maxValue = 364
setOnValueChangedListener { _, _, newVal ->
mDays = newVal
activateSwitch()
}
}
hoursNumberPicker = view.findViewById<NumberPicker>(R.id.hours_picker).apply {
minValue = 0
maxValue = 23
setOnValueChangedListener { _, _, newVal ->
mHours = newVal
activateSwitch()
}
}
minutesNumberPicker = view.findViewById<NumberPicker>(R.id.minutes_picker).apply {
minValue = 0
maxValue = 59
setOnValueChangedListener { _, _, newVal ->
mMinutes = newVal
activateSwitch()
}
}
secondsNumberPicker = view.findViewById<NumberPicker>(R.id.seconds_picker).apply {
minValue = 0
maxValue = 59
setOnValueChangedListener { _, _, newVal ->
mSeconds = newVal
activateSwitch()
}
}
setSwitchAction({ isChecked ->
mEnabled = isChecked
}, mDays + mHours + mMinutes + mSeconds > 0)
assignValuesInViews()
}
private fun buildDuration(): Long {
return if (mEnabled) {
mDays * 24L * 60L * 60L * 1000L +
mHours * 60L * 60L * 1000L +
mMinutes * 60L * 1000L +
mSeconds * 1000L
} else {
-1
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(ENABLE_KEY, mEnabled)
outState.putInt(DAYS_KEY, mDays)
outState.putInt(HOURS_KEY, mHours)
outState.putInt(MINUTES_KEY, mMinutes)
outState.putInt(SECONDS_KEY, mSeconds)
}
override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) {
val currentPreference = preference
if (currentPreference is DurationDialogPreference) {
currentPreference.setDuration(buildDuration())
}
}
}
companion object {
private const val ENABLE_KEY = "ENABLE_KEY"
private const val DAYS_KEY = "DAYS_KEY"
private const val HOURS_KEY = "HOURS_KEY"
private const val MINUTES_KEY = "MINUTES_KEY"
private const val SECONDS_KEY = "SECONDS_KEY"
fun newInstance(key: String): DurationDialogFragmentCompat {
val fragment = DurationDialogFragmentCompat()
val bundle = Bundle(1)
bundle.putString(ARG_KEY, key)
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -154,4 +154,14 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
onCheckedChange?.invoke(isChecked)
}
}
fun activateSwitch() {
if (switchElementView?.isChecked != true)
switchElementView?.isChecked = true
}
fun deactivateSwitch() {
if (switchElementView?.isChecked == true)
switchElementView?.isChecked = false
}
}

View File

@@ -1,86 +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.utils
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
private var fileRequestCodes = ArrayList<Int>()
fun getUnusedCreateFileRequestCode(): Int {
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
fileRequestCodes.add(newCreateFileRequestCode)
return newCreateFileRequestCode
}
@SuppressLint("InlinedApi")
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager): Boolean {
return when {
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/x-keepass"
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
}
else -> true
}
}
fun createDocument(activity: FragmentActivity,
titleString: String,
typeString: String = "application/octet-stream"): Int? {
val idCode = getUnusedCreateFileRequestCode()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
activity.startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
putExtra(Intent.EXTRA_TITLE, titleString)
}, idCode)
return idCode
} catch (e: Exception) {
FileManagerDialogFragment().show(activity.supportFragmentManager, "browserDialog")
}
} else {
FileManagerDialogFragment().show(activity.supportFragmentManager, "browserDialog")
}
return null
}
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
action: (fileCreated: Uri?)->Unit) {
// Retrieve the created URI from the file manager
if (fileRequestCodes.contains(requestCode) && resultCode == Activity.RESULT_OK) {
action.invoke(data?.data)
fileRequestCodes.remove(requestCode)
}
}

View File

@@ -42,8 +42,12 @@ object ParcelableUtil {
val size = parcel.readInt()
val map = HashMap<K, V>(size)
for (i in 0 until size) {
val key: K? = kClass.cast(parcel.readParcelable(kClass.classLoader))
val value: V? = vClass.cast(parcel.readParcelable(vClass.classLoader))
val key: K? = try {
parcel.readParcelable(kClass.classLoader)
} catch (e: Exception) { null }
val value: V? = try {
parcel.readParcelable(vClass.classLoader)
} catch (e: Exception) { null }
if (key != null && value != null)
map[key] = value
}
@@ -52,7 +56,7 @@ object ParcelableUtil {
// For writing map with string key to a Parcel
fun <V : Parcelable> writeStringParcelableMap(
parcel: Parcel, flags: Int, map: LinkedHashMap<String, V>) {
parcel: Parcel, flags: Int, map: HashMap<String, V>) {
parcel.writeInt(map.size)
for ((key, value) in map) {
parcel.writeString(key)
@@ -76,7 +80,9 @@ object ParcelableUtil {
val map = LinkedHashMap<String, V>(size)
for (i in 0 until size) {
val key: String? = parcel.readString()
val value: V? = vClass.cast(parcel.readParcelable(vClass.classLoader))
val value: V? = try {
parcel.readParcelable(vClass.classLoader)
} catch (e: Exception) { null }
if (key != null && value != null)
map[key] = value
}

View File

@@ -1,5 +1,7 @@
package com.kunzisoft.keepass.utils
import java.text.Normalizer
object StringUtil {
fun String.removeLineChars(): String {

View File

@@ -19,8 +19,6 @@
*/
package com.kunzisoft.keepass.utils
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
class UnsignedInt(private var unsignedValue: Int) {
constructor(unsignedValue: UnsignedInt) : this(unsignedValue.toKotlinInt())

View File

@@ -38,16 +38,26 @@ object UriUtil {
fun getFileData(context: Context, fileUri: Uri?): DocumentFile? {
if (fileUri == null)
return null
return when {
isFileScheme(fileUri) -> {
fileUri.path?.let {
File(it).let { file ->
return DocumentFile.fromFile(file)
return try {
when {
isFileScheme(fileUri) -> {
fileUri.path?.let {
File(it).let { file ->
return DocumentFile.fromFile(file)
}
}
}
isContentScheme(fileUri) -> {
DocumentFile.fromSingleUri(context, fileUri)
}
else -> {
Log.e("FileData", "Content scheme not known")
null
}
}
isContentScheme(fileUri) -> DocumentFile.fromSingleUri(context, fileUri)
else -> null
} catch (e: Exception) {
Log.e("FileData", "Unable to get document file", e)
null
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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.utils;
import androidx.annotation.Nullable;
import java.util.UUID;
import static com.kunzisoft.keepass.utils.StreamBytesUtilsKt.uuidTo16Bytes;
public class UuidUtil {
public static @Nullable String toHexString(@Nullable UUID uuid) {
if (uuid == null) { return null; }
try {
byte[] buf = uuidTo16Bytes(uuid);
int len = buf.length;
if (len == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
short bt;
char high, low;
for (byte b : buf) {
bt = (short) (b & 0xFF);
high = (char) (bt >>> 4);
low = (char) (bt & 0x0F);
sb.append(byteToChar(high));
sb.append(byteToChar(low));
}
return sb.toString();
} catch (Exception e) {
return null;
}
}
public static @Nullable UUID fromHexString(@Nullable String hexString) {
if (hexString == null)
return null;
if (hexString.length() != 32)
return null;
char[] charArray = hexString.toLowerCase().toCharArray();
char[] leastSignificantChars = new char[16];
char[] mostSignificantChars = new char[16];
for (int i = 31; i >= 0; i = i-2) {
if (i >= 16) {
mostSignificantChars[32-i] = charArray[i];
mostSignificantChars[31-i] = charArray[i-1];
} else {
leastSignificantChars[16-i] = charArray[i];
leastSignificantChars[15-i] = charArray[i-1];
}
}
StringBuilder standardUUIDString = new StringBuilder();
standardUUIDString.append(leastSignificantChars);
standardUUIDString.append(mostSignificantChars);
standardUUIDString.insert(8, '-');
standardUUIDString.insert(13, '-');
standardUUIDString.insert(18, '-');
standardUUIDString.insert(23, '-');
try {
return UUID.fromString(standardUUIDString.toString());
} catch (Exception e) {
return null;
}
}
// Use short to represent unsigned byte
private static char byteToChar(char bt) {
if (bt >= 10) {
return (char)('A' + bt - 10);
}
else {
return (char)('0' + bt);
}
}
}

View File

@@ -39,7 +39,7 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.search.UuidUtil
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement

View File

@@ -46,10 +46,10 @@ class EntryField @JvmOverloads constructor(context: Context,
var hiddenProtectedValue: Boolean
get() {
return showButtonView.isSelected
return !showButtonView.isSelected
}
set(value) {
showButtonView.isSelected = !value
showButtonView.isSelected = value
changeProtectedValueParameters()
}
@@ -101,7 +101,7 @@ class EntryField @JvmOverloads constructor(context: Context,
} else {
setTextIsSelectable(true)
}
applyHiddenStyle(isProtected && !showButtonView.isSelected)
applyHiddenStyle(isProtected && showButtonView.isSelected)
if (!isProtected) linkify()
}
}

View File

@@ -6,9 +6,9 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.documentfile.provider.DocumentFile
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil
class KeyFileSelectionView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
@@ -51,7 +51,7 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
set(value) {
mUri = value
keyFileNameView.text = value?.let {
DocumentFile.fromSingleUri(context, value)?.name ?: value.path
UriUtil.getFileData(context, value)?.name ?: value.path
} ?: ""
}
}

View File

@@ -50,14 +50,18 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
).execute()
}
private fun getDatabaseFilesLoadedValue(): DatabaseFileData {
var newValue = databaseFilesLoaded.value
if (newValue == null) {
newValue = DatabaseFileData()
}
return newValue
}
fun loadListOfDatabases() {
checkDefaultDatabase()
mFileDatabaseHistoryAction?.getDatabaseFileList { databaseFileListRetrieved ->
var newValue = databaseFilesLoaded.value
if (newValue == null) {
newValue = DatabaseFileData()
}
newValue.apply {
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
databaseFileAction = DatabaseFileAction.NONE
databaseFileToActivate = null
databaseFileList.apply {
@@ -65,14 +69,13 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
addAll(databaseFileListRetrieved)
}
}
databaseFilesLoaded.value = newValue
}
}
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?) {
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseUri, keyFileUri) { databaseFileAdded ->
databaseFileAdded?.let { _ ->
databaseFilesLoaded.value = databaseFilesLoaded.value?.apply {
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
this.databaseFileAction = DatabaseFileAction.ADD
this.databaseFileList.add(databaseFileAdded)
this.databaseFileToActivate = databaseFileAdded
@@ -84,7 +87,7 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
fun updateDatabaseFile(databaseFileToUpdate: DatabaseFile) {
mFileDatabaseHistoryAction?.addOrUpdateDatabaseFile(databaseFileToUpdate) { databaseFileUpdated ->
databaseFileUpdated?.let { _ ->
databaseFilesLoaded.value = databaseFilesLoaded.value?.apply {
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
this.databaseFileAction = DatabaseFileAction.UPDATE
this.databaseFileList
.find { it.databaseUri == databaseFileUpdated.databaseUri }
@@ -104,7 +107,7 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile) {
mFileDatabaseHistoryAction?.deleteDatabaseFile(databaseFileToDelete) { databaseFileDeleted ->
databaseFileDeleted?.let { _ ->
databaseFilesLoaded.value = databaseFilesLoaded.value?.apply {
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
databaseFileAction = DatabaseFileAction.DELETE
databaseFileToActivate = databaseFileDeleted
databaseFileList.remove(databaseFileDeleted)

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.11,0 -1.99,0.9 -1.99,2L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19L5,19L5,8h14v11zM7,10h5v5L7,15z"/>
</vector>

View File

@@ -17,17 +17,42 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/icon_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="80dp"
android:background="@drawable/background_item_selection">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/icon_name"
android:layout_gravity="center"
android:layout_margin="16dp">
</androidx.appcompat.widget.AppCompatImageView>
</FrameLayout>
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/icon_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/icon_image"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_gravity="center"
android:gravity="center"
android:maxLines="3"
android:paddingStart="4dp"
android:paddingLeft="4dp"
android:paddingEnd="4dp"
android:paddingRight="4dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/edit"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/explanation_text"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:gravity="center"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_element"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/explanation_text" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/switch_element">
<LinearLayout
android:id="@+id/duration_days_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toStartOf="@+id/duration_hours_picker"
app:layout_constraintRight_toLeftOf="@+id/duration_hours_picker">
<NumberPicker
android:id="@+id/days_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:src="@drawable/ic_day_white_24dp"
app:tint="?android:attr/textColor"
android:contentDescription="@string/digits" />
</LinearLayout>
<LinearLayout
android:id="@+id/duration_hours_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="HardcodedText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/duration_days_picker"
app:layout_constraintLeft_toRightOf="@+id/duration_days_picker"
app:layout_constraintEnd_toStartOf="@+id/duration_time_picker"
app:layout_constraintRight_toLeftOf="@+id/duration_time_picker">
<NumberPicker
android:id="@+id/hours_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textStyle="bold"
android:text=":" />
</LinearLayout>
<LinearLayout
android:id="@+id/duration_time_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="HardcodedText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/duration_hours_picker"
app:layout_constraintLeft_toRightOf="@+id/duration_hours_picker"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent">
<NumberPicker
android:id="@+id/minutes_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="28sp"
android:text="'"/>
<NumberPicker
android:id="@+id/seconds_picker"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="28sp"
android:text="''"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -137,7 +137,6 @@
<string name="error_string_key">يجب أن يكون لكل سلسلة اسم حقل.</string>
<string name="error_wrong_length">أدخل عددًا صحيحًا موجبًا في حقل «الطول».</string>
<string name="error_autofill_enable_service">تعذر تمكين خدمة الملء التلقائي.</string>
<string name="error_move_folder_in_itself">لا يمكن نقل مجموعة إلى نفسها.</string>
<string name="file_not_found_content">تعذر إيجاد الملف. جرِّب فتحه من متصفح ملفات.</string>
<string name="file_browser">مدير الملفات</string>
<string name="invalid_credentials">تعذر قراءة الإعتمادات.</string>

View File

@@ -85,7 +85,6 @@
<string name="error_copy_group_here">Ovde ne možete kopirati grupu.</string>
<string name="error_copy_entry_here">Ovde ne možete kopirati unos.</string>
<string name="error_move_entry_here">Ovde ne možete premestiti unos.</string>
<string name="error_move_folder_in_itself">Ne možete premestiti grupu u samu sebe.</string>
<string name="error_autofill_enable_service">Nije moguće omogućiti uslugu automatskog popunjavanja.</string>
<string name="error_wrong_length">Unesite pozitivan ceo broj u polje \"Dužina\".</string>
<string name="error_label_exists">Ova oznaka već postoji.</string>

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