Compare commits

...

553 Commits

Author SHA1 Message Date
J-Jamet
d6a43fd8e5 Merge branch 'release/2.10.3' 2021-06-14 10:13:56 +02:00
J-Jamet
9887b8142d Fix service starting #1025 2021-06-13 17:57:30 +02:00
J-Jamet
4d92d6dc2b Update CHANGELOG 2021-06-13 17:15:14 +02:00
J-Jamet
d8cd84ed9e Remove special chars 2021-06-13 17:09:43 +02:00
J-Jamet
6bc740e881 Merge branch 'djibux-master' into develop 2021-06-13 16:53:46 +02:00
J-Jamet
db348cc368 Merge branch 'translations' into develop 2021-06-13 16:51:56 +02:00
J-Jamet
6ebf59d7ff Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-06-13 16:51:21 +02:00
J-Jamet
d35e31d128 Fix biometric prompt #1018 2021-06-12 12:25:04 +02:00
J-Jamet
b9b6d3d2cb Fix database opened without notification (Database is now closed when screen is killed in background #1025) 2021-06-12 11:39:41 +02:00
J-Jamet
728b111ac9 Upgrade to 2.10.3 2021-06-12 10:31:18 +02:00
Reza Almanda
9e5ce589ae Translated using Weblate (Indonesian)
Currently translated at 74.8% (395 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-06-11 03:36:00 +02:00
djib
f486150a0f Fix a typo 2021-06-09 20:58:08 +02:00
djib
075ee815f0 Improve French translation mostly for Magikeyboard 2021-06-09 20:54:19 +02:00
djib
d321283b13 Improve Magikeyboard options descriptions 2021-06-09 20:42:16 +02:00
solokot
8be382fa7e 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-06-09 12:33:30 +02:00
VfBFan
13002f96f1 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-06-06 17:34:13 +02:00
J-Jamet
008ded4a5c Merge tag '2.10.2' into develop
2.10.2
2021-06-02 10:37:27 +02:00
J-Jamet
d476574d05 Merge branch 'release/2.10.2' 2021-06-02 10:37:21 +02:00
Yudong
371b3813d4 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-06-02 10:34:27 +02:00
J-Jamet
fe08d034bb Merge branch 'iArchitSharma-patch-1' into develop 2021-05-31 16:16:10 +02:00
J-Jamet
18f4714410 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-05-31 16:14:24 +02:00
J-Jamet
1b6c416893 Fix autotype #997 2021-05-31 16:10:31 +02:00
Archit Sharma
6153a28b4b fixed and added some hindi translation 2021-05-31 20:47:20 +07:00
J-Jamet
9574cf16fb Fix custom fields iteration 2021-05-31 15:32:35 +02:00
J-Jamet
d309a67416 Capture exception when restart service 2021-05-31 14:48:59 +02:00
J-Jamet
fb865af088 Capture placeholder exception 2021-05-31 14:45:19 +02:00
Yudong
c1e7039357 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-05-29 17:34:07 +02:00
C. Rüdinger
0fd3b37641 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-28 12:34:14 +02:00
VfBFan
cea91f7b2f 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-26 10:34:21 +02:00
Yngvar Skjaldulfsson
3959896832 Translated using Weblate (Spanish)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-05-21 15:41:22 +02:00
Joan Jaume Oliver
d55dccdeb1 Translated using Weblate (Spanish)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-05-21 07:19:58 +02:00
Yngvar Skjaldulfsson
c46c286b51 Translated using Weblate (Spanish)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-05-21 07:19:57 +02:00
zer0-x
aa15d261f3 Translated using Weblate (Arabic)
Currently translated at 65.3% (345 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-05-17 12:32:27 +02:00
Paco Chan
00a32463c7 Translated using Weblate (Spanish)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-05-17 12:32:26 +02:00
Sebastian
dd60ff8b74 Translated using Weblate (Danish)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2021-05-17 12:32:26 +02:00
Paco Chan
4588611cbf Translated using Weblate (Catalan)
Currently translated at 47.5% (251 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2021-05-17 12:32:25 +02:00
J-Jamet
1460c1364a Fix search fields references #987 2021-05-11 12:05:09 +02:00
J-Jamet
37f38fe988 Fix fields references #987 2021-05-11 11:49:25 +02:00
J-Jamet
cf025b9135 Update version and CHANGELOG 2021-05-11 11:47:20 +02:00
ssantos
283ff7a280 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-11 11:34:49 +02:00
Reza Almanda
e668f016b4 Translated using Weblate (Indonesian)
Currently translated at 72.1% (381 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-05-11 11:34:49 +02:00
J-Jamet
256c2c955a Merge tag '2.10.1' into develop
2.10.1
2021-05-10 07:24:57 +02:00
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
J-Jamet
c9c739fd52 Merge branch 'release/2.9.15' 2021-03-29 22:01:44 +02:00
J-Jamet
2b359cc592 Remove unused code 2021-03-29 21:00:10 +02:00
J-Jamet
151b7a323d Upgrade version code 2021-03-29 13:58:30 +02:00
J-Jamet
1063dc2b63 Add TODO SparseArray 2021-03-29 13:57:30 +02:00
J-Jamet
f9f59a6eb1 Replace serializable UUID by Parcelable UUID 2021-03-29 13:49:49 +02:00
J-Jamet
73156cc337 Fix clipboard null exception 2021-03-29 13:09:22 +02:00
J-Jamet
7d53607f49 Capture exception when launching cipher action 2021-03-29 13:07:55 +02:00
J-Jamet
7539945465 Capture exception when error when launching database action 2021-03-29 13:01:31 +02:00
J-Jamet
51df8e7bb1 Try to fix rare bug 2021-03-29 12:52:12 +02:00
J-Jamet
17029ce67c Fix bad padding exception 2021-03-29 12:42:53 +02:00
J-Jamet
8cedc313cf Upgrade version code 2021-03-27 10:54:26 +01:00
J-Jamet
5afe3acac1 Remove unused string 2021-03-27 10:23:38 +01:00
J-Jamet
9887b58b71 Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-03-27 10:18:55 +01:00
J-Jamet
ec8363ba6a Merge branch 'develop' into release/2.9.15 2021-03-27 10:13:53 +01:00
J-Jamet
fcfb71f13b Fix disable Memory Usage setting with AES #941 2021-03-27 10:12:29 +01:00
J-Jamet
3a12e431ff Update CHANGELOG 2021-03-27 05:49:29 +01:00
J-Jamet
bc4ed8e123 Update CHANGELOG 2021-03-27 05:44:09 +01:00
J-Jamet
445e9540a5 Merge branch 'feature/Dynamic_Memory_And_Encrypt_Module' into develop 2021-03-26 20:06:12 +01:00
Joan Jaume Oliver
bbc2a2a9dd Translated using Weblate (Spanish)
Currently translated at 98.6% (515 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-03-26 19:29:42 +01:00
Joan Jaume Oliver
5117bc78b6 Translated using Weblate (Catalan)
Currently translated at 48.2% (252 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2021-03-26 19:29:41 +01:00
J-Jamet
10bf149a07 Rename final-key to aes and remove unused string resource 2021-03-26 19:27:19 +01:00
J-Jamet
0a976bd012 Rollback AES & Argon2 as native C lib 2021-03-26 19:09:30 +01:00
J-Jamet
df31c43e59 Rename crypto module 2021-03-26 16:53:09 +01:00
J-Jamet
c5d30b9b23 Native AES call refactoring 2021-03-26 16:10:55 +01:00
J-Jamet
2e18beff27 Native AES call refactoring 2021-03-26 16:06:28 +01:00
J-Jamet
25e0cec2cc Fix CPU usage with RecyclerView (Weird that the recyclerview uses so much CPU continuously, probably an Android view bug) 2021-03-26 13:51:51 +01:00
J-Jamet
16cc4c5c97 Better activity_password layout implementation 2021-03-26 13:45:08 +01:00
J-Jamet
e5cfb6b7eb Rollback AES thread implementation 2021-03-25 14:59:39 +01:00
J-Jamet
a882ba07e9 Fix create database education 2021-03-25 12:42:40 +01:00
J-Jamet
801f3f99aa Fix app timeout and decrease keyboard timeout #934 2021-03-25 12:36:56 +01:00
J-Jamet
2338b9b57d Fix SHA-512 2021-03-25 12:23:31 +01:00
J-Jamet
8ba396c693 Remove pro guard files 2021-03-25 12:16:04 +01:00
J-Jamet
1164022765 Upgrade gradle and kotlin 2021-03-25 11:29:24 +01:00
J-Jamet
b0c5519da5 Upgrade build tool version to 30.0.3 in icon packs 2021-03-25 11:27:25 +01:00
J-Jamet
f5073238d8 Better KDBX version implementation (new code) 2021-03-25 11:07:51 +01:00
J-Jamet
3ffa89bfaf Revert "Better KDBX version implementation"
This reverts commit 9fae343668.
2021-03-25 10:57:19 +01:00
Reza Almanda
26d8b2fa22 Translated using Weblate (Indonesian)
Currently translated at 74.3% (388 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-03-25 01:29:42 +01:00
J-Jamet
6b17502694 Better hash implementation 2021-03-24 22:17:15 +01:00
J-Jamet
a69d57a4f4 Move classes in right places 2021-03-24 21:29:58 +01:00
J-Jamet
430bc6150f Remove manual cipher input stream 2021-03-24 21:09:44 +01:00
J-Jamet
b888615e0d Fix small variable name 2021-03-24 21:04:12 +01:00
J-Jamet
9fae343668 Better KDBX version implementation 2021-03-24 21:00:41 +01:00
J-Jamet
db467889b0 Encapsulate SHA-256 2021-03-24 20:30:13 +01:00
J-Jamet
153b8d1f37 Better getHmacKey64 method 2021-03-24 20:05:40 +01:00
J-Jamet
87858762d4 Remove NullOutputStream 2021-03-24 19:50:56 +01:00
J-Jamet
50d3282a65 Refactor HMAC methods 2021-03-24 19:15:45 +01:00
J-Jamet
aee0500b38 Merge branch 'develop' into feature/Dynamic_Memory_And_Encrypt_Module 2021-03-24 18:25:28 +01:00
J-Jamet
f2ef6eb94e Decrease default clipboard time #934 2021-03-24 18:25:10 +01:00
J-Jamet
151a5a7e73 Select Twofish with KDB database 2021-03-24 18:08:11 +01:00
J-Jamet
6a088c58de Fix Twofish algorithm 2021-03-24 17:50:08 +01:00
J-Jamet
23e7bf9f89 Update test 2021-03-24 16:13:28 +01:00
J-Jamet
59f24206ad Merge branch 'feature/Dynamic_Memory' into feature/Dynamic_Memory_And_Encrypt_Module 2021-03-24 16:02:23 +01:00
J-Jamet
e45ef019c0 Small encapsulation 2021-03-24 14:57:41 +01:00
J-Jamet
2830d3c8fa Small encapsulation 2021-03-24 14:21:19 +01:00
J-Jamet
088816dfab Change Unsigned Long Implementation 2021-03-24 14:05:45 +01:00
J-Jamet
453a29b81c Rename .java to .kt 2021-03-24 14:05:44 +01:00
J-Jamet
e5cb160aa4 Remove Little Endian output stream 2021-03-24 12:22:34 +01:00
J-Jamet
844588a0d4 Remove Little Endian input stream 2021-03-24 11:55:41 +01:00
J-Jamet
cfcfd47705 Fix test 2021-03-24 11:53:36 +01:00
J-Jamet
f416c0ec7d Merge branch 'feature/Dynamic_Memory' into feature/Dynamic_Memory_And_Encrypt_Module 2021-03-24 11:32:17 +01:00
J-Jamet
78f707c07c Simpler method 2021-03-24 11:29:15 +01:00
J-Jamet
d28a59a2fe Throw exception if bad header 2021-03-24 11:20:30 +01:00
J-Jamet
edf3525a3f Remove unecessary TODO 2021-03-24 11:17:52 +01:00
random r
2b7fe35305 Translated using Weblate (Italian)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-03-23 21:30:04 +01:00
Vít Šindlář
d5819ea4d0 Translated using Weblate (Czech)
Currently translated at 99.6% (520 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-03-23 21:30:04 +01:00
J-Jamet
1099126def Merge branch 'feature/Dynamic_Memory' into feature/Dynamic_Memory_And_Encrypt_Module 2021-03-23 21:24:54 +01:00
J-Jamet
4ba3a797e3 Simpler dataExists 2021-03-23 21:24:30 +01:00
J-Jamet
51b9bb88e5 Merge branch 'feature/Dynamic_Memory' into feature/Dynamic_Memory_And_Encrypt_Module 2021-03-23 21:06:04 +01:00
J-Jamet
fa376148bd Move CustomIconPool in the right package 2021-03-23 21:05:48 +01:00
J-Jamet
452b9677da Merge branch 'feature/Dynamic_Memory' into feature/Dynamic_Memory_And_Encrypt_Module 2021-03-23 21:00:40 +01:00
J-Jamet
02a779f9a2 Fix binary save 2021-03-23 21:00:23 +01:00
J-Jamet
ea60645247 Merge branch 'feature/Encrypt_Module' into feature/Dynamic_Memory_And_Encrypt_Module 2021-03-23 20:25:49 +01:00
J-Jamet
b444a13285 Fix small warnings 2021-03-23 20:24:53 +01:00
J-Jamet
0c6b2a13eb Merge branch 'feature/Encrypt_Module' into feature/Dynamic_Memory_And_Encrypt_Module 2021-03-23 20:23:19 +01:00
J-Jamet
e10bdc1169 Fix clear 2021-03-23 20:02:39 +01:00
J-Jamet
7512cffca3 Code refactoring 2021-03-23 20:00:56 +01:00
J-Jamet
203440e9b8 Better cache management 2021-03-23 19:44:14 +01:00
J-Jamet
145030e854 Refactoring Binary cache 2021-03-23 17:44:07 +01:00
J-Jamet
2b81dfb100 Refactor KeyBinaryByte 2021-03-23 14:57:56 +01:00
J-Jamet
c96ace5281 Add binary byte as an encrypted element of the entire app 2021-03-23 14:45:26 +01:00
J-Jamet
520c6b60be First commit to allocate dynamic memory 2021-03-23 13:07:49 +01:00
J-Jamet
492382d552 Upgrade max binary bytes 2021-03-23 10:57:45 +01:00
J-Jamet
7ca55dd531 Fix themes 2021-03-23 10:38:06 +01:00
J-Jamet
ce4ba73fc4 Replace version by 2.9.15 2021-03-23 09:59:20 +01:00
J-Jamet
5622d92cbb Better native AES KDF method 2021-03-22 22:13:16 +01:00
J-Jamet
8de1c5fd36 Remove unused variables 2021-03-22 21:26:50 +01:00
J-Jamet
6c84fea8dc Update AES and SHA libs 2021-03-22 20:55:27 +01:00
J-Jamet
0b94070086 Simpler AES KDF implementation 2021-03-22 20:36:44 +01:00
J-Jamet
2f209182f5 Replace Argon2 lib 2021-03-22 15:41:57 +01:00
J-Jamet
d7bc572f3e Rename .java to .kt 2021-03-22 14:47:50 +01:00
J-Jamet
4985b49194 Refactor AES tests 2021-03-22 14:46:32 +01:00
J-Jamet
a792df2021 Create timers to calculate Database opening 2021-03-22 12:56:20 +01:00
J-Jamet
a42ec74723 Small refactoring 2021-03-22 11:04:17 +01:00
J-Jamet
de3dbe3b36 Better exception 2021-03-22 10:37:06 +01:00
J-Jamet
874fdb7da0 Refactor KDB cipher and better tests 2021-03-22 10:33:32 +01:00
J-Jamet
3bf0de3888 Refactor ciphers 2021-03-21 16:02:56 +01:00
J-Jamet
d4020c5e0f Better inner random stream implementation 2021-03-21 11:02:52 +01:00
J-Jamet
6175fc00ad Rename .java to .kt 2021-03-21 11:02:52 +01:00
J-Jamet
76e996f429 Remove unused stream 2021-03-21 10:22:05 +01:00
J-Jamet
fd222b73ce Fix unit tests 2021-03-21 10:20:24 +01:00
J-Jamet
a8cc0b1edf Change ObjectNameResource dependencies 2021-03-21 10:06:26 +01:00
J-Jamet
ea2f3545a6 Create encrypt module 2021-03-20 17:35:08 +01:00
Hosted Weblate
0cf136712a Merge branch 'origin/develop' into Weblate. 2021-03-20 14:07:16 +01:00
Milo Ivir
34a453873a Translated using Weblate (Croatian)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-03-20 14:07:16 +01:00
Kunzisoft
0acac3b096 Translated using Weblate (French)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-03-20 14:07:04 +01:00
J-Jamet
37141410e0 Upgrade to version 3.0.0 2021-03-20 12:03:56 +01:00
J-Jamet
69b0e276e3 Merge tag '2.9.14' into develop
2.9.14
2021-03-20 11:42:44 +01:00
J-Jamet
5872376f50 Merge branch 'release/2.9.14' 2021-03-20 11:42:33 +01:00
J-Jamet
075f72d9f6 Update version code 2021-03-18 15:48:58 +01:00
J-Jamet
4441ec1b14 Add small comments 2021-03-18 15:07:38 +01:00
J-Jamet
0735cc1a54 Condition to choose BinaryByte instead of BinaryFile 2021-03-18 14:58:56 +01:00
J-Jamet
dea2ad6904 Compress methods for byte array 2021-03-18 14:25:29 +01:00
J-Jamet
0f0b6b4a8a Better method to put binary 2021-03-18 13:37:33 +01:00
J-Jamet
174e562dcb Add BinaryByte and BinaryFile 2021-03-18 13:20:35 +01:00
J-Jamet
f080750545 Rename BinaryFile by BinaryData 2021-03-18 11:42:28 +01:00
J-Jamet
621415fe51 Replace BinaryFile length method 2021-03-18 11:40:01 +01:00
J-Jamet
428c2818a5 Upgrade version code 2021-03-17 22:17:37 +01:00
J-Jamet
5aa1c70999 Better binary hash implementation 2021-03-17 22:17:14 +01:00
J-Jamet
3508f47842 Externalize binary in custom icon 2021-03-17 15:31:12 +01:00
J-Jamet
62d4993e6d Better loaded cipher key implementation to prevent bad database file is key is not accessible 2021-03-17 13:56:18 +01:00
Oymate
ede6070e43 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 4.2% (22 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/bn_BD/
2021-03-17 08:18:07 +01:00
jan madsen
a61b1d4337 Translated using Weblate (Danish)
Currently translated at 93.8% (490 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2021-03-17 08:18:06 +01:00
J-Jamet
631d946dcf Upgrade version code 2021-03-15 20:23:27 +01:00
J-Jamet
8945334f37 Fix reloading issue 2021-03-15 20:09:31 +01:00
J-Jamet
af2df11a56 Check binary length #924 2021-03-15 18:47:36 +01:00
J-Jamet
6dc0c42b1e Fix database save with bad binary #924 2021-03-15 18:44:53 +01:00
J-Jamet
0328293746 Upgrade version code to 61 2021-03-15 17:31:52 +01:00
J-Jamet
ad406947cf Check data in custom icon 2021-03-15 17:30:44 +01:00
J-Jamet
6bb4c1171f Better database stream exception caught 2021-03-15 17:02:13 +01:00
WaldiS
0f8c71a9df Translated using Weblate (Polish)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-03-14 13:02:53 +01:00
J-Jamet
faa39190fc Icon list optimization 2021-03-13 14:05:44 +01:00
J-Jamet
7c0b925c96 Rename BinaryStreamManager to BinaryDatabaseManager 2021-03-13 13:46:32 +01:00
J-Jamet
1b8c453fd0 Allow IconImage to have null custom icon 2021-03-13 13:06:40 +01:00
J-Jamet
8cc8f595bd upgrade version code 2021-03-13 12:26:10 +01:00
gnu-ewm
cb0b6e010d Translated using Weblate (Polish)
Currently translated at 99.8% (521 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-03-12 21:02:54 +01:00
Hisikawa Mizuki
23dc7be1ab Translated using Weblate (Japanese)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-03-12 21:02:54 +01:00
Oliver Cervera
4b14ad07d2 Translated using Weblate (Italian)
Currently translated at 99.8% (521 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-03-12 21:02:54 +01:00
J-Jamet
9a5a8ae23a Downgrade material lib to 1.1.0 2021-03-12 14:00:10 +01:00
J-Jamet
02b27e235c Upgrade version code to upload a new beta version 2021-03-12 13:59:00 +01:00
Oğuz Ersen
8a4bf7896f Translated using Weblate (Turkish)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-03-11 17:14:10 +01:00
Allan Nordhøy
208ea29643 Translated using Weblate (Norwegian Bokmål)
Currently translated at 76.6% (400 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2021-03-11 17:14:09 +01:00
Eric
7c52ec731a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-03-11 17:14:09 +01:00
Ihor Hordiichuk
c6ee38e435 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-03-11 17:14:08 +01:00
solokot
65253cc5b9 Translated using Weblate (Russian)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-03-11 17:14:08 +01:00
Stephan Paternotte
d1a1a23cbc Translated using Weblate (Dutch)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-03-11 17:14:08 +01:00
HARADA Hiroyuki
e8bb3a5ba7 Translated using Weblate (Japanese)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-03-11 17:14:07 +01:00
Retrial
22b8f82770 Translated using Weblate (Greek)
Currently translated at 100.0% (522 of 522 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-03-11 17:14:07 +01:00
J-Jamet
5874c5b9cb Resize image stream dynamically to show image preview #919 2021-03-11 16:02:15 +01:00
J-Jamet
fad09b2cd5 Better themes files organization 2021-03-11 11:01:16 +01:00
J-Jamet
cfb08afd7d Fix dateTime themes 2021-03-11 10:45:03 +01:00
J-Jamet
16d939c601 Fix dateTime dialog purple theme 2021-03-10 22:37:54 +01:00
J-Jamet
af072648c1 Fix reload database dialog when database created and when file modification is not 0 2021-03-10 21:24:00 +01:00
J-Jamet
45d2609494 Revert "Remove unused Base64 stream in temp file"
This reverts commit 4ecb8d4483.
2021-03-10 21:05:33 +01:00
J-Jamet
f5ea65f18c Change default icon width 2021-03-10 19:48:59 +01:00
J-Jamet
e9fc6bed23 Load icons as coroutine 2021-03-10 19:24:00 +01:00
J-Jamet
ccc8e4664d Update gradle 2021-03-10 17:43:11 +01:00
J-Jamet
651ef04137 Fix recognize iconId -1 2021-03-10 14:51:30 +01:00
Hosted Weblate
063aba333c Merge branch 'origin/develop' into Weblate. 2021-03-10 13:58:23 +01:00
J-Jamet
a3a517ff89 Fix reloading 2021-03-10 13:46:15 +01:00
Milo Ivir
40da29b681 Translated using Weblate (Croatian)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-03-09 19:03:13 +01:00
Oğuz Ersen
684d81c895 Translated using Weblate (Turkish)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-03-09 19:03:13 +01:00
Eric
c6ddd3b238 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-03-09 19:03:12 +01:00
Ihor Hordiichuk
3186413bee Translated using Weblate (Ukrainian)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-03-09 19:03:12 +01:00
solokot
aae1c4cf1c Translated using Weblate (Russian)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-03-09 19:03:11 +01:00
WaldiS
e64f264f12 Translated using Weblate (Polish)
Currently translated at 99.8% (519 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-03-09 19:03:11 +01:00
Retrial
08cf747f52 Translated using Weblate (Greek)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-03-09 19:03:11 +01:00
J-Jamet
383437a3c7 Fix search layout 2021-03-08 20:22:35 +01:00
J-Jamet
c7cba3f50b Fix notes in group 2021-03-08 20:07:16 +01:00
J-Jamet
7feb499d50 Update CHANGELOG 2021-03-08 19:11:15 +01:00
J-Jamet
2e6c25b651 Merge branch 'feature/Add_Group_Root' into develop 2021-03-08 19:09:58 +01:00
J-Jamet
192903e8d7 Add group to root 2021-03-08 19:09:21 +01:00
J-Jamet
6f72ade4d4 Change add node button view 2021-03-08 18:49:54 +01:00
J-Jamet
7e3fc0fa59 Fix 'kotlin-android-extensions' Gradle plugin deprecated 2021-03-08 18:22:43 +01:00
J-Jamet
4ea896b57c Fix small warning 2021-03-08 18:06:56 +01:00
J-Jamet
073ccb9b52 Revert androidx.fragment:fragment-ktx:1.2.5 2021-03-08 18:03:20 +01:00
J-Jamet
f32c944d31 Remove unused import 2021-03-08 17:50:06 +01:00
J-Jamet
acd1e3bdfc Refactoring virtual group 2021-03-08 17:49:10 +01:00
J-Jamet
774cbdf0fe Fix group search 2021-03-08 17:28:55 +01:00
J-Jamet
f5fd527590 Fix status bar color 2021-03-08 17:01:08 +01:00
J-Jamet
7ac9a7e94a Fix add button in a search 2021-03-08 16:26:43 +01:00
J-Jamet
5735f7a945 Better custom icons duplication implementation 2021-03-08 14:43:50 +01:00
J-Jamet
f8f423b5c1 Better XML writing for custom icons 2021-03-08 13:35:57 +01:00
J-Jamet
81ba7f0721 Prevent duplication during database saving 2021-03-08 13:29:02 +01:00
J-Jamet
e6be8c23fb Rollback toolbar home icon 2021-03-08 13:05:53 +01:00
J-Jamet
9cc1764a18 Consume icon added 2021-03-08 13:02:48 +01:00
J-Jamet
6a77adc313 Fix error after orientation change 2021-03-08 12:58:01 +01:00
J-Jamet
e532572d5a Prevent adding duplicate icon 2021-03-08 12:51:30 +01:00
zeritti
dcae49c5f8 Translated using Weblate (Czech)
Currently translated at 100.0% (520 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-03-08 10:56:58 +01:00
Oliver Cervera
7321c01e8c Translated using Weblate (Italian)
Currently translated at 98.6% (513 of 520 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-03-08 08:16:00 +01:00
J-Jamet
a85b9998c3 Fix purple color 2021-03-07 19:10:56 +01:00
J-Jamet
30d2ce43d1 Update CHANGELOG 2021-03-07 19:00:41 +01:00
J-Jamet
d46edfc9b7 Fix flickering 2021-03-07 18:49:19 +01:00
J-Jamet
79d11138e6 Change home button icon 2021-03-07 18:28:55 +01:00
J-Jamet
f5cd019b6c Fix orientation change during icon selection 2021-03-07 17:58:31 +01:00
J-Jamet
42c4de56fd Encapsulate setResult 2021-03-07 17:33:46 +01:00
J-Jamet
44b3c28a2a Fix icon removed no longer accessible 2021-03-07 17:24:37 +01:00
J-Jamet
e5184a1568 Fix icon selection and icon removed no longer accessible 2021-03-07 17:15:48 +01:00
J-Jamet
76d60ded4c Fix color icon selection 2021-03-07 17:05:36 +01:00
J-Jamet
d2e7e925f7 Remove custom icons 2021-03-07 16:56:51 +01:00
J-Jamet
6357a30acb Upgrade material lib to 1.3.0 2021-03-06 14:57:44 +01:00
J-Jamet
4b1fdd0e38 Upgrade fragment lib 2021-03-06 14:49:30 +01:00
J-Jamet
227fc060b9 Fix restricted API and upgrade Autofill lib 2021-03-06 14:45:34 +01:00
J-Jamet
32e5aba906 Better color integration 2021-03-06 14:41:31 +01:00
J-Jamet
55013bb220 Change background dark color 2021-03-06 13:43:25 +01:00
J-Jamet
5544b20d7f Auto convert old themes 2021-03-06 13:28:10 +01:00
J-Jamet
d6c7f9c68b Fix button hidden in dialog #903 2021-03-06 12:53:44 +01:00
J-Jamet
8b5004e500 Change package import in dialog 2021-03-06 12:53:11 +01:00
J-Jamet
d6cf11b87d Fix file manager button 2021-03-06 12:35:07 +01:00
J-Jamet
d4c3a3be6b Center validate button in toolbar 2021-03-06 12:10:47 +01:00
J-Jamet
e724b188ef Remove unused code 2021-03-06 11:10:45 +01:00
J-Jamet
ebf92b1103 Remove unused code 2021-03-06 11:08:03 +01:00
J-Jamet
42e2a49af6 Better draw factory implementation 2021-03-06 10:48:05 +01:00
J-Jamet
be7cd3275a Better draw factory implementation 2021-03-06 10:22:14 +01:00
J-Jamet
966df11beb Fix subtitle toolbar color 2021-03-06 09:31:16 +01:00
J-Jamet
fad852f00d Fix icon standard color 2021-03-05 21:01:48 +01:00
Hosted Weblate
9c3e6eb823 Merge branch 'origin/develop' into Weblate. 2021-03-05 20:54:33 +01:00
J-Jamet
8b88f72efc OTP wiki link 2021-03-05 20:52:50 +01:00
J-Jamet
e0aab6cfbf Default custom tab when custom icon is already selected 2021-03-05 20:36:23 +01:00
J-Jamet
29a2e60e05 Fix fragment style 2021-03-05 20:06:30 +01:00
J-Jamet
12df74b3a7 Fix fragment style 2021-03-05 19:55:51 +01:00
Retrial
22d943c9e2 Translated using Weblate (Greek)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-03-05 19:50:50 +01:00
J-Jamet
5839f51f44 Fix otp key uppercase label #909 2021-03-05 15:14:52 +01:00
J-Jamet
4ecb8d4483 Remove unused Base64 stream in temp file 2021-03-05 14:52:56 +01:00
J-Jamet
a08035551a Move fragment in right package 2021-03-05 14:51:03 +01:00
J-Jamet
c2460d7262 Better code encapsulation 2021-03-05 14:46:23 +01:00
J-Jamet
4776eac07e Prevent custom icon usage with KDB database 2021-03-05 14:32:01 +01:00
J-Jamet
4952d107dd Upgrade NDK to v21 LTS 2021-03-05 13:17:03 +01:00
J-Jamet
b5d6ee9dee Fix education hints color 2021-03-05 13:13:07 +01:00
J-Jamet
e7a30c6024 Merge branch 'feature/Custom_Icons' into develop 2021-03-04 20:06:03 +01:00
J-Jamet
6578e52ec5 Fix error view in edit entry 2021-03-04 20:04:44 +01:00
J-Jamet
97508beb5c Icon error as warning 2021-03-04 19:50:00 +01:00
J-Jamet
e063b0d6fc Remove unused code 2021-03-04 19:48:13 +01:00
J-Jamet
bb65dc0e81 Change icon interface name 2021-03-04 19:41:43 +01:00
J-Jamet
09e00ec119 Better icon type implementation 2021-03-04 19:33:16 +01:00
J-Jamet
985f8fad3b Keep icon history 2021-03-04 19:00:17 +01:00
J-Jamet
a9accc8c42 Replace deprecated Password toggle 2021-03-04 18:34:15 +01:00
J-Jamet
41316d2bd3 Prevent add empty icon 2021-03-03 19:43:46 +01:00
J-Jamet
2c172eb8d3 Fix multiple addition 2021-03-03 19:30:33 +01:00
J-Jamet
d49827f9f8 Remove unused method 2021-03-03 17:38:45 +01:00
J-Jamet
b2aafda2b1 Auto select custom tab after upload an icon 2021-03-03 15:16:56 +01:00
J-Jamet
b4c50e0262 Async icon loading 2021-03-03 14:34:08 +01:00
J-Jamet
fbe51c12c1 Icon drawable as WeakReference 2021-03-03 12:32:06 +01:00
J-Jamet
991959416b Replace string section 2021-03-03 12:24:46 +01:00
J-Jamet
41f0e61f60 Scroll to the end when an icon is added 2021-03-02 16:55:41 +01:00
J-Jamet
eab8cd101f Prevent uploading icon in error 2021-03-02 16:50:35 +01:00
J-Jamet
97765d798c Remove apache commons collection 2021-03-02 16:23:13 +01:00
J-Jamet
a7f76248ac Better icon list implementation 2021-03-02 16:11:39 +01:00
J-Jamet
8de670fcf2 Fix uncaught exception 2021-03-02 14:48:15 +01:00
J-Jamet
744823cce4 Fix unique id for each binary file 2021-03-02 14:43:48 +01:00
J-Jamet
d95a3e00aa Resize icon file to upload 2021-03-02 13:59:15 +01:00
Deleted User
ccc190f7b0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 75.2% (386 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2021-03-02 07:03:29 +01:00
J-Jamet
3e6cd98cb9 Add big image file error 2021-03-01 17:54:10 +01:00
J-Jamet
0e3b8fdbb6 Upload custom attachment with thread 2021-03-01 16:39:46 +01:00
J-Jamet
5aa3f79616 Upload custom attachment and fix warning 2021-03-01 15:50:11 +01:00
vachan-maker
42427d0690 Translated using Weblate (Malayalam)
Currently translated at 74.8% (384 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ml/
2021-03-01 10:50:33 +01:00
J-Jamet
1a7b32e6d1 Select icon file 2021-03-01 10:48:38 +01:00
naofum
83555bfdc5 Translated using Weblate (Japanese)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-02-28 06:50:53 +01:00
J-Jamet
03990c1dd9 Add lock action 2021-02-25 19:46:51 +01:00
J-Jamet
b361be5cb0 Fix listener by using view model 2021-02-25 19:36:42 +01:00
J-Jamet
d02f6d1e67 Merge branch 'develop' into feature/Custom_Icons 2021-02-25 19:05:50 +01:00
J-Jamet
1e56c34e2f Fix ImageButton style 2021-02-25 19:05:31 +01:00
J-Jamet
2a2f8dcecd Migrate to ViewPager2 and as Activity 2021-02-25 18:54:42 +01:00
J-Jamet
b609ed3ad4 Fix number of icons 2021-02-25 15:44:23 +01:00
J-Jamet
80521f8ec2 Change list to recyclerview 2021-02-25 15:13:34 +01:00
J-Jamet
3a5df6a893 Add IconCustomFragment 2021-02-25 13:23:42 +01:00
Reza Almanda
0e60c4f910 Translated using Weblate (Indonesian)
Currently translated at 63.3% (325 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-02-24 23:50:38 +01:00
abidin toumi
f5f2d3c883 Translated using Weblate (Arabic)
Currently translated at 67.6% (347 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-02-24 23:50:36 +01:00
Kornelijus Tvarijanavičius
3c830bfaf2 Translated using Weblate (Lithuanian)
Currently translated at 22.4% (115 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2021-02-24 23:50:36 +01:00
random r
98caf9b5bf Translated using Weblate (Italian)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-02-24 23:50:35 +01:00
J-Jamet
cfbb8fab1b Merge branch 'develop' into feature/Custom_Icons 2021-02-24 21:18:08 +01:00
J-Jamet
3069e5e566 Change bioemtric unlock description #900 2021-02-24 21:13:32 +01:00
J-Jamet
ac050a09e8 Remove unused interface 2021-02-24 20:59:07 +01:00
J-Jamet
78406ccdbf Merge branch 'develop' into feature/Custom_Icons 2021-02-24 20:51:08 +01:00
J-Jamet
2efea1bb00 Merge branch 'feature/Refactor_Icons' into develop #96 2021-02-24 20:29:41 +01:00
J-Jamet
0157a160f0 Fix new entry icon inheritance 2021-02-24 20:27:19 +01:00
J-Jamet
eb4084a6a4 Factorize icon class 2021-02-24 20:19:25 +01:00
J-Jamet
63f5e5416f Refactor IconPool 2021-02-24 18:40:43 +01:00
J-Jamet
38e0433e8f Fix iconId #901 2021-02-22 21:25:14 +01:00
J-Jamet
fc5ffc5f62 Fix binary deduplication #715 2021-02-22 18:56:21 +01:00
J-Jamet
460e3558a9 First commit to create custom attachments 2021-02-22 17:17:31 +01:00
Vít Šindlář
b06f223124 Translated using Weblate (Czech)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-02-21 04:48:37 +01:00
Suyono Hermanto
aabfb2adfd Translated using Weblate (Indonesian)
Currently translated at 44.6% (229 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-02-20 17:50:38 +01:00
Kornelijus Tvarijanavičius
f9c47c9035 Translated using Weblate (Lithuanian)
Currently translated at 18.7% (96 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2021-02-20 17:50:38 +01:00
zeritti
e9485ebf56 Translated using Weblate (Czech)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-02-20 17:50:37 +01:00
J-Jamet
fcd7fd2889 Merge branch 'feature/Dark_Mode' into develop #714 2021-02-20 11:16:57 +01:00
J-Jamet
279f4a347a Update CHANGELOG 2021-02-20 11:16:46 +01:00
J-Jamet
b29fe23403 Fix clear style binaries color 2021-02-19 12:57:17 +01:00
J-Jamet
1972a551e9 Rename styles 2021-02-19 12:48:20 +01:00
J-Jamet
7e2ffd2ec4 Change Night word by Dark 2021-02-19 12:34:32 +01:00
J-Jamet
06793ae13e Fix database list elevation 2021-02-19 12:24:07 +01:00
J-Jamet
26e961d356 Fix color selection 2021-02-19 12:20:41 +01:00
J-Jamet
da956d3bd5 Change white colors 2021-02-19 12:01:36 +01:00
J-Jamet
318a72a123 Fix toolbar popup background 2021-02-19 11:46:42 +01:00
J-Jamet
5c4b4864d2 Setting to select theme brightness 2021-02-18 20:07:03 +01:00
J-Jamet
4ab31fe21a Small color change 2021-02-18 19:04:03 +01:00
J-Jamet
c22a213635 Fix title shadow 2021-02-18 18:39:28 +01:00
J-Jamet
aa166a0104 Fix windowLightStatusBar in v21 2021-02-18 18:34:38 +01:00
J-Jamet
2a36626731 Change blue 2021-02-18 18:22:43 +01:00
J-Jamet
2f2360fd48 Fix fab menu text color 2021-02-18 18:19:07 +01:00
J-Jamet
e466643229 Fix overflow color button 2021-02-18 18:09:47 +01:00
J-Jamet
a9044c3dc4 Fix white and black themes 2021-02-18 18:01:15 +01:00
J-Jamet
2d28cc21a0 Lock button with white color 2021-02-18 14:30:52 +01:00
J-Jamet
ebb0e7e118 Fix white & clear themes 2021-02-18 14:30:28 +01:00
J-Jamet
8205454858 Fix red & blue themes 2021-02-18 14:05:10 +01:00
ButterflyOfFire
e909112ab8 Translated using Weblate (Arabic)
Currently translated at 67.4% (346 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2021-02-18 13:50:31 +01:00
Stephan Paternotte
469923855a Translated using Weblate (Dutch)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-02-18 13:50:30 +01:00
Caetano Demián Moreno
f2aca08886 Translated using Weblate (Spanish)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-02-18 13:50:30 +01:00
J-Jamet
9900f8fecb Fix black theme 2021-02-18 13:48:33 +01:00
J-Jamet
73224887b9 Refactor toolbar popup theme 2021-02-18 13:41:29 +01:00
J-Jamet
e31574015b Refactor toolbar special appearance 2021-02-18 12:48:39 +01:00
J-Jamet
ef26251469 Refactor toolbar styles 2021-02-18 12:35:32 +01:00
J-Jamet
798bce2759 Fix custom bar home button color 2021-02-18 11:40:15 +01:00
J-Jamet
88fee5f6de Tint menu icon 2021-02-18 11:32:31 +01:00
J-Jamet
937695a1e5 First commit to implement Dark mode 2021-02-18 10:50:29 +01:00
J-Jamet
a43b580d67 Elevation in password view 2021-02-17 16:21:58 +01:00
J-Jamet
b8d0bff22b Remove unused style code 2021-02-17 15:00:24 +01:00
J-Jamet
c180d38394 Remove unused style code 2021-02-17 12:54:14 +01:00
J-Jamet
10f7d955ff Small style refactoring 2021-02-17 12:39:34 +01:00
J-Jamet
143651099a Auto open biometric prompt by default 2021-02-17 11:27:14 +01:00
J-Jamet
63bb12269f Update to version 2.9.14 2021-02-17 11:22:24 +01:00
J-Jamet
00ee80184e Merge tag '2.9.13' into develop
2.9.13
2021-02-15 12:47:54 +01:00
Milo Ivir
1cf8131b6c Translated using Weblate (Croatian)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-02-13 17:50:46 +01:00
WaldiS
1b38bd59ef Translated using Weblate (Polish)
Currently translated at 100.0% (513 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-02-13 17:50:46 +01:00
Oliver Cervera
2e409c3246 Translated using Weblate (Italian)
Currently translated at 99.2% (509 of 513 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-02-13 17:50:46 +01:00
434 changed files with 23186 additions and 20279 deletions

View File

@@ -1,3 +1,60 @@
KeePassDX(2.10.3)
* Improve Magikeyboard options description #1022 #1023 (Thx @djibux)
* Fix database opened without notification (database is now closed when screen is killed in background #1025)
* Fix biometric prompt #1018
KeePassDX(2.10.2)
* Fix search fields references #987
* Fix Auto-Types with same key #997
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
* Better opening performance #929 #933
* Fix memory usage setting #941
KeePassDX(2.9.14)
* Add custom icons #96
* Dark Themes #532 #714
* Fix binary deduplication #715
* Fix IconId #901
* Resize image stream dynamically to prevent slowdown #919
* Small changes #795 #900 #903 #909 #914
KeePassDX(2.9.13) KeePassDX(2.9.13)
* Binary image viewer #473 #749 * Binary image viewer #473 #749
* Fix TOTP plugin settings #878 * Fix TOTP plugin settings #878

View File

@@ -1,19 +1,18 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 30 compileSdkVersion 30
buildToolsVersion '30.0.3' buildToolsVersion "30.0.3"
ndkVersion '21.3.6528147' ndkVersion "21.4.7075529"
defaultConfig { defaultConfig {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 14 minSdkVersion 15
targetSdkVersion 30 targetSdkVersion 30
versionCode = 57 versionCode = 81
versionName = "2.9.13" versionName = "2.10.3"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -30,12 +29,6 @@ android {
} }
} }
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt"
}
}
buildTypes { buildTypes {
release { release {
minifyEnabled = false minifyEnabled = false
@@ -51,7 +44,11 @@ android {
buildConfigField "String", "BUILD_VERSION", "\"libre\"" buildConfigField "String", "BUILD_VERSION", "\"libre\""
buildConfigField "boolean", "FULL_VERSION", "true" buildConfigField "boolean", "FULL_VERSION", "true"
buildConfigField "boolean", "CLOSED_STORE", "false" buildConfigField "boolean", "CLOSED_STORE", "false"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\",\"KeepassDXStyle_Purple_Dark\"}" buildConfigField "String[]", "STYLES_DISABLED",
"{\"KeepassDXStyle_Red\"," +
"\"KeepassDXStyle_Red_Night\"," +
"\"KeepassDXStyle_Purple\"," +
"\"KeepassDXStyle_Purple_Dark\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
} }
pro { pro {
@@ -70,7 +67,13 @@ android {
buildConfigField "String", "BUILD_VERSION", "\"free\"" buildConfigField "String", "BUILD_VERSION", "\"free\""
buildConfigField "boolean", "FULL_VERSION", "false" buildConfigField "boolean", "FULL_VERSION", "false"
buildConfigField "boolean", "CLOSED_STORE", "true" buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\",\"KeepassDXStyle_Purple_Dark\"}" buildConfigField "String[]", "STYLES_DISABLED",
"{\"KeepassDXStyle_Blue\"," +
"\"KeepassDXStyle_Blue_Night\"," +
"\"KeepassDXStyle_Red\"," +
"\"KeepassDXStyle_Red_Night\"," +
"\"KeepassDXStyle_Purple\"," +
"\"KeepassDXStyle_Purple_Dark\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ] manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
} }
@@ -104,20 +107,19 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.documentfile:documentfile:1.0.1' 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 // Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.core:core-ktx:1.3.2"
implementation 'androidx.fragment:fragment-ktx:1.2.5' implementation 'androidx.fragment:fragment-ktx:1.2.5'
// WARNING: To upgrade with style, bug in edit text // WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.1.0'
// Database // Database
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
// Autofill // Autofill
implementation "androidx.autofill:autofill:1.1.0-rc01" implementation "androidx.autofill:autofill:1.1.0"
// Crypto
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
// Time // Time
implementation 'joda-time:joda-time:2.10.6' implementation 'joda-time:joda-time:2.10.6'
// Color // Color
@@ -125,9 +127,10 @@ dependencies {
// Education // Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
// Apache Commons // Apache Commons
implementation 'commons-collections:commons-collections:3.2.2'
implementation 'commons-io:commons-io:2.8.0' implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15' implementation 'commons-codec:commons-codec:1.15'
// Encrypt lib
implementation project(path: ':crypto')
// Icon pack // Icon pack
implementation project(path: ':icon-pack-classic') implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material') implementation project(path: ':icon-pack-material')

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2017 Brian Pellin, 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.tests.crypto
import org.junit.Assert.assertArrayEquals
import java.io.IOException
import java.util.Random
import junit.framework.TestCase
import com.kunzisoft.keepass.crypto.finalkey.AndroidAESKeyTransformer
import com.kunzisoft.keepass.crypto.finalkey.NativeAESKeyTransformer
class AESKeyTest : TestCase() {
private lateinit var mRand: Random
@Throws(Exception::class)
override fun setUp() {
super.setUp()
mRand = Random()
}
@Throws(IOException::class)
fun testAES() {
// Test both an old and an even number to test my flip variable
testAESFinalKey(5)
testAESFinalKey(6)
}
@Throws(IOException::class)
private fun testAESFinalKey(rounds: Long) {
val seed = ByteArray(32)
val key = ByteArray(32)
val nativeKey: ByteArray?
val androidKey: ByteArray?
mRand.nextBytes(seed)
mRand.nextBytes(key)
val androidAESKey = AndroidAESKeyTransformer()
androidKey = androidAESKey.transformMasterKey(seed, key, rounds)
val nativeAESKey = NativeAESKeyTransformer()
nativeKey = nativeAESKey.transformMasterKey(seed, key, rounds)
assertArrayEquals("Does not match", androidKey, nativeKey)
}
}

View File

@@ -1,83 +0,0 @@
/*
* Copyright 2017 Brian Pellin, 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.tests.crypto
import com.kunzisoft.keepass.crypto.CipherFactory
import junit.framework.TestCase
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.Random
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import org.junit.Assert.assertArrayEquals
class AESTest : TestCase() {
private val mRand = Random()
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidAlgorithmParameterException::class)
fun testEncrypt() {
// Test above below and at the blocksize
testFinal(15)
testFinal(16)
testFinal(17)
// Test random larger sizes
val size = mRand.nextInt(494) + 18
testFinal(size)
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
private fun testFinal(dataSize: Int) {
// Generate some input
val input = ByteArray(dataSize)
mRand.nextBytes(input)
// Generate key
val keyArray = ByteArray(32)
mRand.nextBytes(keyArray)
val key = SecretKeySpec(keyArray, "AES")
// Generate IV
val ivArray = ByteArray(16)
mRand.nextBytes(ivArray)
val iv = IvParameterSpec(ivArray)
val android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true)
android.init(Cipher.ENCRYPT_MODE, key, iv)
val outAndroid = android.doFinal(input, 0, dataSize)
val nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
nat.init(Cipher.ENCRYPT_MODE, key, iv)
val outNative = nat.doFinal(input, 0, dataSize)
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2021 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePassDX. * This file is part of KeePassDX.
* *
@@ -19,44 +19,32 @@
*/ */
package com.kunzisoft.keepass.tests.crypto package com.kunzisoft.keepass.tests.crypto
import com.kunzisoft.keepass.utils.readBytesLength
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertArrayEquals
import org.junit.Test
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.util.*
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.Random
import javax.crypto.BadPaddingException
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
import javax.crypto.IllegalBlockSizeException
import javax.crypto.NoSuchPaddingException
import junit.framework.TestCase class EncryptionTest {
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.engine.AesEngine
import com.kunzisoft.keepass.stream.BetterCipherInputStream
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
class CipherTest : TestCase() {
private val rand = Random() private val rand = Random()
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class) @Test
fun testCipherFactory() { fun testCipherFactory() {
val key = ByteArray(32) val key = ByteArray(32)
rand.nextBytes(key)
val iv = ByteArray(16) val iv = ByteArray(16)
rand.nextBytes(iv)
val plaintext = ByteArray(1024) val plaintext = ByteArray(1024)
rand.nextBytes(key)
rand.nextBytes(iv)
rand.nextBytes(plaintext) rand.nextBytes(plaintext)
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID) val aes = EncryptionAlgorithm.AESRijndael.cipherEngine
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv) val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv) val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
@@ -66,20 +54,20 @@ class CipherTest : TestCase() {
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext) assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
} }
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class, IOException::class) @Test
fun testCipherStreams() { fun testCipherStreams() {
val MESSAGE_LENGTH = 1024 val length = 1024
val key = ByteArray(32) val key = ByteArray(32)
val iv = ByteArray(16)
val plaintext = ByteArray(MESSAGE_LENGTH)
rand.nextBytes(key) rand.nextBytes(key)
val iv = ByteArray(16)
rand.nextBytes(iv) rand.nextBytes(iv)
val plaintext = ByteArray(length)
rand.nextBytes(plaintext) rand.nextBytes(plaintext)
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID) val aes = EncryptionAlgorithm.AESRijndael.cipherEngine
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv) val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv) val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
@@ -91,10 +79,9 @@ class CipherTest : TestCase() {
val secrettext = bos.toByteArray() val secrettext = bos.toByteArray()
val bis = ByteArrayInputStream(secrettext) val bis = ByteArrayInputStream(secrettext)
val cis = BetterCipherInputStream(bis, decrypt) val cis = CipherInputStream(bis, decrypt)
val lis = LittleEndianDataInputStream(cis)
val decrypttext = lis.readBytes(MESSAGE_LENGTH) val decrypttext = cis.readBytesLength(length)
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext) assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
} }

View File

@@ -1,156 +0,0 @@
package com.kunzisoft.keepass.tests.stream
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.stream.readAllBytes
import com.kunzisoft.keepass.utils.UriUtil
import junit.framework.TestCase.assertEquals
import org.junit.Test
import java.io.DataInputStream
import java.io.File
import java.io.InputStream
import java.lang.Exception
import java.security.MessageDigest
class BinaryAttachmentTest {
private val context: Context by lazy {
InstrumentationRegistry.getInstrumentation().context
}
private val cacheDirectory = UriUtil.getBinaryDir(InstrumentationRegistry.getInstrumentation().targetContext)
private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A)
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
private val loadedKey = Database.LoadedKey.generateNewCipherKey()
private fun saveBinary(asset: String, binaryAttachment: BinaryAttachment) {
context.assets.open(asset).use { assetInputStream ->
binaryAttachment.getOutputDataStream(loadedKey).use { binaryOutputStream ->
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
binaryOutputStream.write(buffer)
}
}
}
}
@Test
fun testSaveTextInCache() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
assertEquals("Save text binary length failed.", binaryA.length, binaryB.length)
assertEquals("Save text binary MD5 failed.", binaryA.md5(), binaryB.md5())
}
@Test
fun testSaveImageInCache() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
assertEquals("Save image binary length failed.", binaryA.length, binaryB.length)
assertEquals("Save image binary failed.", binaryA.md5(), binaryB.md5())
}
@Test
fun testCompressText() {
val binaryA = BinaryAttachment(fileA)
val binaryB = BinaryAttachment(fileB)
val binaryC = BinaryAttachment(fileC)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
saveBinary(TEST_TEXT_ASSET, binaryC)
binaryA.compress(loadedKey)
binaryB.compress(loadedKey)
assertEquals("Compress text length failed.", binaryA.length, binaryB.length)
assertEquals("Compress text MD5 failed.", binaryA.md5(), binaryB.md5())
binaryB.decompress(loadedKey)
assertEquals("Decompress text length failed.", binaryB.length, binaryC.length)
assertEquals("Decompress text MD5 failed.", binaryB.md5(), binaryC.md5())
}
@Test
fun testCompressImage() {
val binaryA = BinaryAttachment(fileA)
var binaryB = BinaryAttachment(fileB)
val binaryC = BinaryAttachment(fileC)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
saveBinary(TEST_IMAGE_ASSET, binaryC)
binaryA.compress(loadedKey)
binaryB.compress(loadedKey)
assertEquals("Compress image length failed.", binaryA.length, binaryA.length)
assertEquals("Compress image failed.", binaryA.md5(), binaryA.md5())
binaryB = BinaryAttachment(fileB, true)
binaryB.decompress(loadedKey)
assertEquals("Decompress image length failed.", binaryB.length, binaryC.length)
assertEquals("Decompress image failed.", binaryB.md5(), binaryC.md5())
}
@Test
fun testReadText() {
val binaryA = BinaryAttachment(fileA)
saveBinary(TEST_TEXT_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
binaryA.getInputDataStream(loadedKey)))
}
@Test
fun testReadImage() {
val binaryA = BinaryAttachment(fileA)
saveBinary(TEST_IMAGE_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET),
binaryA.getInputDataStream(loadedKey)))
}
private fun streamAreEquals(inputStreamA: InputStream,
inputStreamB: InputStream): Boolean {
val bufferA = ByteArray(DEFAULT_BUFFER_SIZE)
val bufferB = ByteArray(DEFAULT_BUFFER_SIZE)
val dataInputStreamB = DataInputStream(inputStreamB)
try {
var len: Int
while (inputStreamA.read(bufferA).also { len = it } > 0) {
dataInputStreamB.readFully(bufferB, 0, len)
for (i in 0 until len) {
if (bufferA[i] != bufferB[i])
return false
}
}
return inputStreamB.read() < 0 // is the end of the second file also.
} catch (e: Exception) {
return false
}
finally {
inputStreamA.close()
inputStreamB.close()
}
}
private fun BinaryAttachment.md5(): String {
val md = MessageDigest.getInstance("MD5")
return this.getInputDataStream(loadedKey).use { fis ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
generateSequence {
when (val bytesRead = fis.read(buffer)) {
-1 -> null
else -> bytesRead
}
}.forEach { bytesRead -> md.update(buffer, 0, bytesRead) }
md.digest().joinToString("") { "%02x".format(it) }
}
}
companion object {
private const val TEST_FILE_CACHE_A = "testA"
private const val TEST_FILE_CACHE_B = "testB"
private const val TEST_FILE_CACHE_C = "testC"
private const val TEST_IMAGE_ASSET = "test_image.png"
private const val TEST_TEXT_ASSET = "test_text.txt"
}
}

View File

@@ -0,0 +1,176 @@
package com.kunzisoft.keepass.tests.stream
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.kunzisoft.keepass.utils.readAllBytes
import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.binary.BinaryFile
import com.kunzisoft.keepass.utils.UriUtil
import junit.framework.TestCase.assertEquals
import org.junit.Test
import java.io.DataInputStream
import java.io.File
import java.io.InputStream
import kotlin.random.Random
class BinaryDataTest {
private val context: Context by lazy {
InstrumentationRegistry.getInstrumentation().context
}
private val cacheDirectory = UriUtil.getBinaryDir(InstrumentationRegistry.getInstrumentation().targetContext)
private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A)
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
private val binaryCache = BinaryCache()
private fun saveBinary(asset: String, binaryData: BinaryFile) {
context.assets.open(asset).use { assetInputStream ->
binaryData.getOutputDataStream(binaryCache).use { binaryOutputStream ->
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
binaryOutputStream.write(buffer)
}
}
}
}
@Test
fun testSaveTextInCache() {
val binaryA = BinaryFile(fileA)
val binaryB = BinaryFile(fileB)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
assertEquals("Save text binary length failed.", binaryA.getSize(), binaryB.getSize())
assertEquals("Save text binary MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
}
@Test
fun testSaveImageInCache() {
val binaryA = BinaryFile(fileA)
val binaryB = BinaryFile(fileB)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
assertEquals("Save image binary length failed.", binaryA.getSize(), binaryB.getSize())
assertEquals("Save image binary failed.", binaryA.binaryHash(), binaryB.binaryHash())
}
@Test
fun testCompressText() {
val binaryA = BinaryFile(fileA)
val binaryB = BinaryFile(fileB)
val binaryC = BinaryFile(fileC)
saveBinary(TEST_TEXT_ASSET, binaryA)
saveBinary(TEST_TEXT_ASSET, binaryB)
saveBinary(TEST_TEXT_ASSET, binaryC)
binaryA.compress(binaryCache)
binaryB.compress(binaryCache)
assertEquals("Compress text length failed.", binaryA.getSize(), binaryB.getSize())
assertEquals("Compress text MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
binaryB.decompress(binaryCache)
assertEquals("Decompress text length failed.", binaryB.getSize(), binaryC.getSize())
assertEquals("Decompress text MD5 failed.", binaryB.binaryHash(), binaryC.binaryHash())
}
@Test
fun testCompressImage() {
val binaryA = BinaryFile(fileA)
var binaryB = BinaryFile(fileB)
val binaryC = BinaryFile(fileC)
saveBinary(TEST_IMAGE_ASSET, binaryA)
saveBinary(TEST_IMAGE_ASSET, binaryB)
saveBinary(TEST_IMAGE_ASSET, binaryC)
binaryA.compress(binaryCache)
binaryB.compress(binaryCache)
assertEquals("Compress image length failed.", binaryA.getSize(), binaryA.getSize())
assertEquals("Compress image failed.", binaryA.binaryHash(), binaryA.binaryHash())
binaryB = BinaryFile(fileB, true)
binaryB.decompress(binaryCache)
assertEquals("Decompress image length failed.", binaryB.getSize(), binaryC.getSize())
assertEquals("Decompress image failed.", binaryB.binaryHash(), binaryC.binaryHash())
}
@Test
fun testCompressBytes() {
// Test random byte array
val byteArray = ByteArray(50)
Random.nextBytes(byteArray)
testCompressBytes(byteArray)
// Test empty byte array
testCompressBytes(ByteArray(0))
}
private fun testCompressBytes(byteArray: ByteArray) {
val binaryA = binaryCache.getBinaryData("0", true)
binaryA.getOutputDataStream(binaryCache).use { outputStream ->
outputStream.write(byteArray)
}
val binaryB = binaryCache.getBinaryData("1", true)
binaryB.getOutputDataStream(binaryCache).use { outputStream ->
outputStream.write(byteArray)
}
val binaryC = binaryCache.getBinaryData("2", true)
binaryC.getOutputDataStream(binaryCache).use { outputStream ->
outputStream.write(byteArray)
}
binaryA.compress(binaryCache)
binaryB.compress(binaryCache)
assertEquals("Compress bytes decompressed failed.", binaryA.isCompressed, true)
assertEquals("Compress bytes length failed.", binaryA.getSize(), binaryA.getSize())
assertEquals("Compress bytes failed.", binaryA.binaryHash(), binaryA.binaryHash())
binaryB.decompress(binaryCache)
assertEquals("Decompress bytes decompressed failed.", binaryB.isCompressed, false)
assertEquals("Decompress bytes length failed.", binaryB.getSize(), binaryC.getSize())
assertEquals("Decompress bytes failed.", binaryB.binaryHash(), binaryC.binaryHash())
}
@Test
fun testReadText() {
val binaryA = BinaryFile(fileA)
saveBinary(TEST_TEXT_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
binaryA.getInputDataStream(binaryCache)))
}
@Test
fun testReadImage() {
val binaryA = BinaryFile(fileA)
saveBinary(TEST_IMAGE_ASSET, binaryA)
assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET),
binaryA.getInputDataStream(binaryCache)))
}
private fun streamAreEquals(inputStreamA: InputStream,
inputStreamB: InputStream): Boolean {
val bufferA = ByteArray(DEFAULT_BUFFER_SIZE)
val bufferB = ByteArray(DEFAULT_BUFFER_SIZE)
val dataInputStreamB = DataInputStream(inputStreamB)
try {
var len: Int
while (inputStreamA.read(bufferA).also { len = it } > 0) {
dataInputStreamB.readFully(bufferB, 0, len)
for (i in 0 until len) {
if (bufferA[i] != bufferB[i])
return false
}
}
return inputStreamB.read() < 0 // is the end of the second file also.
} catch (e: Exception) {
return false
}
finally {
inputStreamA.close()
inputStreamB.close()
}
}
companion object {
private const val TEST_FILE_CACHE_A = "testA"
private const val TEST_FILE_CACHE_B = "testB"
private const val TEST_FILE_CACHE_C = "testC"
private const val TEST_IMAGE_ASSET = "test_image.png"
private const val TEST_TEXT_ASSET = "test_text.txt"
}
}

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

@@ -20,9 +20,7 @@
package com.kunzisoft.keepass.tests.utils package com.kunzisoft.keepass.tests.utils
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.stream.* import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.UnsignedLong
import junit.framework.TestCase import junit.framework.TestCase
import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertArrayEquals
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@@ -54,7 +52,7 @@ class ValuesTest : TestCase() {
val orig = ByteArray(8) val orig = ByteArray(8)
setArray(orig, value, 8) setArray(orig, value, 8)
assertArrayEquals(orig, longTo8Bytes(bytes64ToLong(orig))) assertArrayEquals(orig, uLongTo8Bytes(bytes64ToULong(orig)))
} }
fun testReadWriteIntZero() { fun testReadWriteIntZero() {
@@ -133,7 +131,7 @@ class ValuesTest : TestCase() {
} }
private fun testReadWriteByte(value: Byte) { private fun testReadWriteByte(value: Byte) {
val dest: Byte = UnsignedInt(UnsignedInt.fromKotlinByte(value)).toKotlinByte() val dest: Byte = UnsignedInt(value.toInt() and 0xFF).toKotlinByte()
assert(value == dest) assert(value == dest)
} }
@@ -144,13 +142,11 @@ class ValuesTest : TestCase() {
expected.set(2008, 1, 2, 3, 4, 5) expected.set(2008, 1, 2, 3, 4, 5)
val actual = Calendar.getInstance() val actual = Calendar.getInstance()
dateTo5Bytes(expected.time, cal)?.let { buf -> actual.time = DateInstant(bytes5ToDate(dateTo5Bytes(expected.time, cal), cal)).date
actual.time = bytes5ToDate(buf, cal).date
}
val jDate = DateInstant(System.currentTimeMillis()) val jDate = DateInstant(System.currentTimeMillis())
val intermediate = DateInstant(jDate) val intermediate = DateInstant(jDate)
val cDate = bytes5ToDate(dateTo5Bytes(intermediate.date)!!) val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate.date)))
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR)) assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH)) assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
@@ -183,12 +179,10 @@ class ValuesTest : TestCase() {
ulongBytes[i] = -1 ulongBytes[i] = -1
} }
val bos = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
val leos = LittleEndianDataOutputStream(bos) byteArrayOutputStream.write(UnsignedLong.MAX_BYTES)
leos.writeLong(UnsignedLong.MAX_VALUE) byteArrayOutputStream.close()
leos.close() val uLongMax = byteArrayOutputStream.toByteArray()
val uLongMax = bos.toByteArray()
assertArrayEquals(ulongBytes, uLongMax) assertArrayEquals(ulongBytes, uLongMax)
} }

View File

@@ -129,6 +129,9 @@
<activity <activity
android:name="com.kunzisoft.keepass.activities.EntryActivity" android:name="com.kunzisoft.keepass.activities.EntryActivity"
android:configChanges="keyboardHidden" /> android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
android:configChanges="keyboardHidden" />
<activity <activity
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity" android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
android:configChanges="keyboardHidden" /> android:configChanges="keyboardHidden" />

View File

@@ -39,6 +39,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
@@ -47,7 +48,6 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
@@ -94,6 +94,8 @@ class EntryActivity : LockingActivity() {
private var clipboardHelper: ClipboardHelper? = null private var clipboardHelper: ClipboardHelper? = null
private var mFirstLaunchOfActivity: Boolean = false private var mFirstLaunchOfActivity: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null
private var iconColor: Int = 0 private var iconColor: Int = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -126,7 +128,7 @@ class EntryActivity : LockingActivity() {
historyView = findViewById(R.id.history_container) historyView = findViewById(R.id.history_container)
entryContentsView = findViewById(R.id.entry_contents) entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)) entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryContentsView?.setAttachmentCipherKey(mDatabase?.loadedCipherKey) entryContentsView?.setAttachmentCipherKey(mDatabase)
entryProgress = findViewById(R.id.entry_progress) entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button) lockView = findViewById(R.id.lock_button)
@@ -141,6 +143,9 @@ class EntryActivity : LockingActivity() {
clipboardHelper = ClipboardHelper(this) clipboardHelper = ClipboardHelper(this)
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
// Init SAF manager
mExternalFileHelper = ExternalFileHelper(this)
// Init attachment service binder manager // Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
@@ -242,7 +247,9 @@ class EntryActivity : LockingActivity() {
val entryInfo = entry.getEntryInfo(mDatabase) val entryInfo = entry.getEntryInfo(mDatabase)
// Assign title icon // Assign title icon
titleIconView?.assignDatabaseIcon(mDatabase!!.drawFactory, entryInfo.icon, iconColor) titleIconView?.let { iconView ->
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
}
// Assign title text // Assign title text
val entryTitle = entryInfo.title val entryTitle = entryInfo.title
@@ -343,7 +350,7 @@ class EntryActivity : LockingActivity() {
// Manage attachments // Manage attachments
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem -> entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
createDocument(this, attachmentItem.name)?.let { requestCode -> mExternalFileHelper?.createDocument(attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem mAttachmentsToDownload[requestCode] = attachmentItem
} }
} }
@@ -379,7 +386,7 @@ class EntryActivity : LockingActivity() {
} }
} }
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri -> mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
if (createdFileUri != null) { if (createdFileUri != null) {
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload -> mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
mAttachmentFileBinderManager mAttachmentFileBinderManager

View File

@@ -43,8 +43,9 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE 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.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.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
@@ -78,7 +79,6 @@ import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class EntryEditActivity : LockingActivity(), class EntryEditActivity : LockingActivity(),
IconPickerDialogFragment.IconPickerListener,
EntryCustomFieldDialogFragment.EntryCustomFieldListener, EntryCustomFieldDialogFragment.EntryCustomFieldListener,
GeneratePasswordDialogFragment.GeneratePasswordListener, GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener, SetOTPDialogFragment.CreateOtpListener,
@@ -103,7 +103,7 @@ class EntryEditActivity : LockingActivity(),
private var lockView: View? = null private var lockView: View? = null
// To manage attachments // To manage attachments
private var mSelectFileHelper: SelectFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAllowMultipleAttachments: Boolean = false private var mAllowMultipleAttachments: Boolean = false
private var mTempAttachments = ArrayList<EntryAttachmentState>() private var mTempAttachments = ArrayList<EntryAttachmentState>()
@@ -172,10 +172,14 @@ class EntryEditActivity : LockingActivity(),
val parentIcon = mParent?.icon val parentIcon = mParent?.icon
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true) tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
// Set default icon // Set default icon
if (parentIcon != null if (parentIcon != null) {
&& parentIcon.iconId != IconImage.UNKNOWN_ID if (parentIcon.custom.isUnknown
&& parentIcon.iconId != IconImageStandard.FOLDER) { && parentIcon.standard.id != IconImageStandard.FOLDER_ID) {
tempEntryInfo?.icon = parentIcon tempEntryInfo?.icon = IconImage(parentIcon.standard)
}
if (!parentIcon.custom.isUnknown) {
tempEntryInfo?.icon = IconImage(parentIcon.custom)
}
} }
// Set default username // Set default username
tempEntryInfo?.username = mDatabase?.defaultUsername ?: "" tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
@@ -198,13 +202,13 @@ class EntryEditActivity : LockingActivity(),
// Build fragment to manage entry modification // Build fragment to manage entry modification
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment? entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
if (entryEditFragment == null) { if (entryEditFragment == null) {
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo, mDatabase?.loadedCipherKey) entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
} }
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG) .replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
.commit() .commit()
entryEditFragment?.apply { entryEditFragment?.apply {
drawFactory = mDatabase?.drawFactory drawFactory = mDatabase?.iconDrawableFactory
setOnDateClickListener = { setOnDateClickListener = {
expiryTime.date.let { expiresDate -> expiryTime.date.let { expiresDate ->
val dateTime = DateTime(expiresDate) val dateTime = DateTime(expiresDate)
@@ -219,8 +223,8 @@ class EntryEditActivity : LockingActivity(),
openPasswordGenerator() openPasswordGenerator()
} }
// Add listener to the icon // Add listener to the icon
setOnIconViewClickListener = View.OnClickListener { setOnIconViewClickListener = { iconImage ->
IconPickerDialogFragment.launch(this@EntryEditActivity) IconPickerActivity.launch(this@EntryEditActivity, iconImage)
} }
setOnRemoveAttachment = { attachment -> setOnRemoveAttachment = { attachment ->
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment) mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
@@ -237,7 +241,7 @@ class EntryEditActivity : LockingActivity(),
} }
// To retrieve attachment // To retrieve attachment
mSelectFileHelper = SelectFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mAttachmentFileBinderManager = AttachmentFileBinderManager(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Save button // Save button
@@ -454,8 +458,8 @@ class EntryEditActivity : LockingActivity(),
/** /**
* Add a new attachment * Add a new attachment
*/ */
private fun addNewAttachment(item: MenuItem) { private fun addNewAttachment() {
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item) mExternalFileHelper?.openDocument()
} }
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) { override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
@@ -481,7 +485,7 @@ class EntryEditActivity : LockingActivity(),
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) { private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
val compression = mDatabase?.compressionForNewEntry() ?: false val compression = mDatabase?.compressionForNewEntry() ?: false
mDatabase?.buildNewBinary(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment -> mDatabase?.buildNewBinaryAttachment(compression)?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment) val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment // Ask to replace the current attachment
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) || if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
@@ -497,9 +501,12 @@ class EntryEditActivity : LockingActivity(),
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri -> IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
entryEditFragment?.icon = icon
}
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
uri?.let { attachmentToUploadUri -> uri?.let { attachmentToUploadUri ->
// TODO Async to get the name
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile -> UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName -> documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) { if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
@@ -565,7 +572,7 @@ class EntryEditActivity : LockingActivity(),
// Delete temp attachment if not used // Delete temp attachment if not used
mTempAttachments.forEach { tempAttachmentState -> mTempAttachments.forEach { tempAttachmentState ->
val tempAttachment = tempAttachmentState.attachment val tempAttachment = tempAttachmentState.attachment
mDatabase?.binaryPool?.let { binaryPool -> mDatabase?.attachmentPool?.let { binaryPool ->
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) { if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
mDatabase?.removeAttachmentIfNotUsed(tempAttachment) mDatabase?.removeAttachmentIfNotUsed(tempAttachment)
} }
@@ -648,7 +655,7 @@ class EntryEditActivity : LockingActivity(),
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation( && entryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView, attachmentView,
{ {
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView) mExternalFileHelper?.openDocument()
}, },
{ {
performedNextEducation(entryEditActivityEducation) performedNextEducation(entryEditActivityEducation)
@@ -676,7 +683,7 @@ class EntryEditActivity : LockingActivity(),
return true return true
} }
R.id.menu_add_attachment -> { R.id.menu_add_attachment -> {
addNewAttachment(item) addNewAttachment()
return true return true
} }
R.id.menu_add_otp -> { R.id.menu_add_otp -> {
@@ -711,12 +718,6 @@ class EntryEditActivity : LockingActivity(),
} }
} }
override fun iconPicked(bundle: Bundle) {
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
entryEditFragment?.icon = icon
}
}
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) { override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
// To fix android 4.4 issue // To fix android 4.4 issue
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice // https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice

View File

@@ -42,8 +42,9 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode 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.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
@@ -63,14 +64,13 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import kotlinx.android.synthetic.main.activity_file_selection.*
import java.io.FileNotFoundException import java.io.FileNotFoundException
class FileDatabaseSelectActivity : SpecialModeActivity(), class FileDatabaseSelectActivity : SpecialModeActivity(),
AssignMasterKeyDialogFragment.AssignPasswordDialogListener { AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
// Views // Views
private var coordinatorLayout: CoordinatorLayout? = null private lateinit var coordinatorLayout: CoordinatorLayout
private var createDatabaseButtonView: View? = null private var createDatabaseButtonView: View? = null
private var openDatabaseButtonView: View? = null private var openDatabaseButtonView: View? = null
@@ -83,7 +83,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
private var mSelectFileHelper: SelectFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
@@ -104,14 +104,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
createDatabaseButtonView?.setOnClickListener { createNewFile() } createDatabaseButtonView?.setOnClickListener { createNewFile() }
// Open database button // Open database button
mSelectFileHelper = SelectFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
openDatabaseButtonView = findViewById(R.id.open_keyfile_button) openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.apply { openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
// History list // History list
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list) val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
@@ -163,29 +158,31 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Observe list of databases // Observe list of databases
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles -> databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
when (databaseFiles.databaseFileAction) { try {
DatabaseFilesViewModel.DatabaseFileAction.NONE -> { when (databaseFiles.databaseFileAction) {
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList) DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
} mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
} }
GroupActivity.launch(this@FileDatabaseSelectActivity, DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)) databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
} mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> { }
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate -> }
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate) DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
} databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
} mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> { }
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete -> }
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete) DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
}
} }
} }
databaseFilesViewModel.consumeAction()
} catch (e: Exception) {
Log.e(TAG, "Unable to observe database action", e)
} }
databaseFilesViewModel.consumeAction()
} }
// Observe default database // Observe default database
@@ -203,6 +200,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential() val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri) databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
} }
GroupActivity.launch(this@FileDatabaseSelectActivity,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
} }
ACTION_DATABASE_LOAD_TASK -> { ACTION_DATABASE_LOAD_TASK -> {
val database = Database.getInstance() val database = Database.getInstance()
@@ -217,7 +216,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
resultError = "$resultError $resultMessage" resultError = "$resultError $resultMessage"
} }
Log.e(TAG, resultError) Log.e(TAG, resultError)
Snackbar.make(activity_file_selection_coordinator_layout, Snackbar.make(coordinatorLayout,
resultError, resultError,
Snackbar.LENGTH_LONG).asError().show() Snackbar.LENGTH_LONG).asError().show()
} }
@@ -231,16 +230,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
* Create a new file by calling the content provider * Create a new file by calling the content provider
*/ */
private fun createNewFile() { 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") getString(R.string.database_file_extension_default), "application/x-keepass")
} }
private fun fileNoFoundAction(e: FileNotFoundException) { private fun fileNoFoundAction(e: FileNotFoundException) {
val error = getString(R.string.file_not_found_content) val error = getString(R.string.file_not_found_content)
Log.e(TAG, error, e) Log.e(TAG, error, e)
coordinatorLayout?.let { Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
} }
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) { private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
@@ -285,7 +282,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Show open and create button or special mode // Show open and create button or special mode
when (mSpecialMode) { when (mSpecialMode) {
SpecialMode.DEFAULT -> { SpecialMode.DEFAULT -> {
if (allowCreateDocumentByStorageAccessFramework(packageManager)) { if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
// There is an activity which can handle this intent. // There is an activity which can handle this intent.
createDatabaseButtonView?.visibility = View.VISIBLE createDatabaseButtonView?.visibility = View.VISIBLE
} else{ } else{
@@ -344,7 +341,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
} }
} catch (e: Exception) { } catch (e: Exception) {
val error = getString(R.string.error_create_database_file) val error = getString(R.string.error_create_database_file)
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show() Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error, e) Log.e(TAG, error, e)
} }
} }
@@ -358,23 +355,21 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
} }
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri -> mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
if (uri != null) { if (uri != null) {
launchPasswordActivityWithPath(uri) launchPasswordActivityWithPath(uri)
} }
} }
// Retrieve the created URI from the file manager // Retrieve the created URI from the file manager
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri -> mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) { if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true) AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog") .show(supportFragmentManager, "passwordDialog")
} else { } else {
val error = getString(R.string.error_create_database) val error = getString(R.string.error_create_database)
coordinatorLayout?.let { Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
Log.e(TAG, error) Log.e(TAG, error)
} }
} }
@@ -395,9 +390,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) { private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
// If no recent files // If no recent files
val createDatabaseEducationPerformed = val createDatabaseEducationPerformed =
createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE createDatabaseButtonView != null
&& createDatabaseButtonView!!.visibility == View.VISIBLE
&& mAdapterDatabaseHistory != null && mAdapterDatabaseHistory != null
&& mAdapterDatabaseHistory!!.itemCount > 0 && mAdapterDatabaseHistory!!.itemCount == 0
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation( && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
createDatabaseButtonView!!, createDatabaseButtonView!!,
{ {
@@ -412,9 +408,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
openDatabaseButtonView != null openDatabaseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( && fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
openDatabaseButtonView!!, openDatabaseButtonView!!,
{tapTargetView -> { tapTargetView ->
tapTargetView?.let { tapTargetView?.let {
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it) mExternalFileHelper?.openDocument()
} }
}, },
{} {}

View File

@@ -45,6 +45,7 @@ import androidx.fragment.app.FragmentManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.fragments.ListNodesFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
@@ -59,7 +60,6 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
@@ -81,7 +81,6 @@ import org.joda.time.DateTime
class GroupActivity : LockingActivity(), class GroupActivity : LockingActivity(),
GroupEditDialogFragment.EditGroupListener, GroupEditDialogFragment.EditGroupListener,
IconPickerDialogFragment.IconPickerListener,
DatePickerDialog.OnDateSetListener, DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener, TimePickerDialog.OnTimeSetListener,
ListNodesFragment.NodeClickListener, ListNodesFragment.NodeClickListener,
@@ -105,7 +104,6 @@ class GroupActivity : LockingActivity(),
private var mDatabase: Database? = null private var mDatabase: Database? = null
private var mListNodesFragment: ListNodesFragment? = null private var mListNodesFragment: ListNodesFragment? = null
private var mCurrentGroupIsASearch: Boolean = false
private var mRequestStartupSearch = true private var mRequestStartupSearch = true
private var actionNodeMode: ActionMode? = null private var actionNodeMode: ActionMode? = null
@@ -172,7 +170,7 @@ class GroupActivity : LockingActivity(),
} }
mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState) mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState)
mCurrentGroupIsASearch = Intent.ACTION_SEARCH == intent.action val currentGroupIsASearch = mCurrentGroup?.isVirtual == true
Log.i(TAG, "Started creating tree") Log.i(TAG, "Started creating tree")
if (mCurrentGroup == null) { if (mCurrentGroup == null) {
@@ -181,13 +179,13 @@ class GroupActivity : LockingActivity(),
} }
var fragmentTag = LIST_NODES_FRAGMENT_TAG var fragmentTag = LIST_NODES_FRAGMENT_TAG
if (mCurrentGroupIsASearch) if (currentGroupIsASearch)
fragmentTag = SEARCH_FRAGMENT_TAG fragmentTag = SEARCH_FRAGMENT_TAG
// Initialize the fragment with the list // Initialize the fragment with the list
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment? mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
if (mListNodesFragment == null) if (mListNodesFragment == null)
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, mCurrentGroupIsASearch) mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, currentGroupIsASearch)
// Attach fragment to content view // Attach fragment to content view
supportFragmentManager.beginTransaction().replace( supportFragmentManager.beginTransaction().replace(
@@ -206,9 +204,11 @@ class GroupActivity : LockingActivity(),
// Add listeners to the add buttons // Add listeners to the add buttons
addNodeButtonView?.setAddGroupClickListener { addNodeButtonView?.setAddGroupClickListener {
GroupEditDialogFragment.build() GroupEditDialogFragment.create(GroupInfo().apply {
.show(supportFragmentManager, if (mCurrentGroup?.allowAddNoteInGroup == true) {
GroupEditDialogFragment.TAG_CREATE_GROUP) notes = ""
}
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
} }
addNodeButtonView?.setAddEntryClickListener { addNodeButtonView?.setAddEntryClickListener {
mCurrentGroup?.let { currentGroup -> mCurrentGroup?.let { currentGroup ->
@@ -346,9 +346,7 @@ class GroupActivity : LockingActivity(),
ACTION_DATABASE_RELOAD_TASK -> { ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity // Reload the current activity
if (result.isSuccess) { if (result.isSuccess) {
startActivity(intent) reload()
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
} else { } else {
this.showActionErrorIfNeeded(result) this.showActionErrorIfNeeded(result)
finish() finish()
@@ -367,6 +365,14 @@ class GroupActivity : LockingActivity(),
Log.i(TAG, "Finished creating tree") Log.i(TAG, "Finished creating tree")
} }
private fun reload() {
// Reload the current activity
startActivity(intent)
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
mDatabase?.wasReloaded = false
}
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
@@ -375,13 +381,11 @@ class GroupActivity : LockingActivity(),
manageSearchInfoIntent(intentNotNull) manageSearchInfoIntent(intentNotNull)
Log.d(TAG, "setNewIntent: $intentNotNull") Log.d(TAG, "setNewIntent: $intentNotNull")
setIntent(intentNotNull) setIntent(intentNotNull)
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) { if (Intent.ACTION_SEARCH == intentNotNull.action) {
finishNodeAction()
// only one instance of search in backstack // only one instance of search in backstack
deletePreviousSearchGroup() deletePreviousSearchGroup()
openGroup(retrieveCurrentGroup(intentNotNull, null), true) openGroup(retrieveCurrentGroup(intentNotNull, null), true)
true
} else {
false
} }
} }
} }
@@ -465,12 +469,11 @@ class GroupActivity : LockingActivity(),
private fun refreshSearchGroup() { private fun refreshSearchGroup() {
deletePreviousSearchGroup() deletePreviousSearchGroup()
if (mCurrentGroupIsASearch) if (mCurrentGroup?.isVirtual == true)
openGroup(retrieveCurrentGroup(intent, null), true) openGroup(retrieveCurrentGroup(intent, null), true)
} }
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? { private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? {
// Force read only if the database is like that // Force read only if the database is like that
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
@@ -518,24 +521,21 @@ class GroupActivity : LockingActivity(),
} }
} }
} }
if (mCurrentGroupIsASearch) {
searchTitleView?.visibility = View.VISIBLE
} else {
searchTitleView?.visibility = View.GONE
}
// Assign icon if (mCurrentGroup?.isVirtual == true) {
if (mCurrentGroupIsASearch) { searchTitleView?.visibility = View.VISIBLE
if (toolbar != null) { if (toolbar != null) {
toolbar?.navigationIcon = null toolbar?.navigationIcon = null
} }
iconView?.visibility = View.GONE iconView?.visibility = View.GONE
} else { } else {
searchTitleView?.visibility = View.GONE
// Assign the group icon depending of IconPack or custom icon // Assign the group icon depending of IconPack or custom icon
iconView?.visibility = View.VISIBLE iconView?.visibility = View.VISIBLE
mCurrentGroup?.let { mCurrentGroup?.let { currentGroup ->
if (mDatabase?.drawFactory != null) iconView?.let { imageView ->
iconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, it.icon, mIconColor) mDatabase?.iconDrawableFactory?.assignDatabaseIcon(imageView, currentGroup.icon, mIconColor)
}
if (toolbar != null) { if (toolbar != null) {
if (mCurrentGroup?.containsParent() == true) if (mCurrentGroup?.containsParent() == true)
@@ -550,20 +550,25 @@ class GroupActivity : LockingActivity(),
// Assign number of children // Assign number of children
refreshNumberOfChildren() refreshNumberOfChildren()
// Show button if allowed // Hide button
addNodeButtonView?.apply { initAddButton()
}
private fun initAddButton() {
addNodeButtonView?.apply {
closeButtonIfOpen()
// To enable add button // To enable add button
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch val addGroupEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch var addEntryEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true
mCurrentGroup?.let { mCurrentGroup?.let {
if (!it.allowAddEntryIfIsRoot()) if (!it.allowAddEntryIfIsRoot)
addEntryEnabled = it != mRootGroup && addEntryEnabled addEntryEnabled = it != mRootGroup && addEntryEnabled
} }
enableAddGroup(addGroupEnabled) enableAddGroup(addGroupEnabled)
enableAddEntry(addEntryEnabled) enableAddEntry(addEntryEnabled)
if (mCurrentGroup?.isVirtual == true)
if (actionNodeMode == null) hideButton()
else if (actionNodeMode == null)
showButton() showButton()
} }
} }
@@ -783,7 +788,7 @@ class GroupActivity : LockingActivity(),
when (node.type) { when (node.type) {
Type.GROUP -> { Type.GROUP -> {
mOldGroupToUpdate = node as Group mOldGroupToUpdate = node as Group
GroupEditDialogFragment.build(mOldGroupToUpdate!!.getGroupInfo()) GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo())
.show(supportFragmentManager, .show(supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP) GroupEditDialogFragment.TAG_CREATE_GROUP)
} }
@@ -808,74 +813,75 @@ class GroupActivity : LockingActivity(),
override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?, override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
nodes: List<Node>): Boolean { nodes: List<Node>): Boolean {
// Move or copy only if allowed (in root if allowed) when (pasteMode) {
if (mCurrentGroup != mDatabase?.rootGroup ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|| mDatabase?.rootCanContainsEntry() == true) { // Copy
mCurrentGroup?.let { newParent ->
when (pasteMode) { mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> { nodes,
// Copy newParent,
mCurrentGroup?.let { newParent -> !mReadOnly && mAutoSaveEnable
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes( )
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
)
}
}
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
// Move
mCurrentGroup?.let { newParent ->
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
nodes,
newParent,
!mReadOnly && mAutoSaveEnable
)
}
}
else -> {
} }
} }
} else { ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
coordinatorLayout?.let { coordinatorLayout -> // Move
Snackbar.make(coordinatorLayout, mCurrentGroup?.let { newParent ->
R.string.error_copy_entry_here, mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
Snackbar.LENGTH_LONG).asError().show() nodes,
newParent,
!mReadOnly && mAutoSaveEnable
)
}
} }
else -> {}
} }
finishNodeAction() finishNodeAction()
return true 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 { private fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false): Boolean {
val database = mDatabase mDatabase?.let { database ->
// If recycle bin enabled, ensure it exists // If recycle bin enabled, ensure it exists
if (database != null && database.isRecycleBinEnabled) { if (database.isRecycleBinEnabled) {
database.ensureRecycleBinExists(resources) 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)
} }
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 return true
} }
@@ -893,6 +899,9 @@ class GroupActivity : LockingActivity(),
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (mDatabase?.wasReloaded == true) {
reload()
}
// Show the lock button // Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) { lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE View.VISIBLE
@@ -1069,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, override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
groupInfo: GroupInfo) { groupInfo: GroupInfo) {
@@ -1120,13 +1139,6 @@ class GroupActivity : LockingActivity(),
// Do nothing here // Do nothing here
} }
override// For icon in create tree dialog
fun iconPicked(bundle: Bundle) {
(supportFragmentManager
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment)
.iconPicked(bundle)
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) { override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters) mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
} }
@@ -1165,6 +1177,13 @@ class GroupActivity : LockingActivity(),
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
// To create tree dialog for icon
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
(supportFragmentManager
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment)
.setIcon(icon)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
} }
@@ -1189,7 +1208,6 @@ class GroupActivity : LockingActivity(),
mCurrentGroup = mListNodesFragment?.mainGroup mCurrentGroup = mListNodesFragment?.mainGroup
// Remove search in intent // Remove search in intent
deletePreviousSearchGroup() deletePreviousSearchGroup()
mCurrentGroupIsASearch = false
if (Intent.ACTION_SEARCH == intent.action) { if (Intent.ACTION_SEARCH == intent.action) {
intent.action = Intent.ACTION_DEFAULT intent.action = Intent.ACTION_DEFAULT
intent.removeExtra(SearchManager.QUERY) intent.removeExtra(SearchManager.QUERY)

View File

@@ -0,0 +1,324 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
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.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
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.*
class IconPickerActivity : LockingActivity() {
private lateinit var toolbar: Toolbar
private lateinit var coordinatorLayout: CoordinatorLayout
private lateinit var uploadButton: View
private var lockView: View? = null
private var mIconImage: IconImage = IconImage()
private val mainScope = CoroutineScope(Dispatchers.Main)
private val iconPickerViewModel: IconPickerViewModel by viewModels()
private var mCustomIconsSelectionMode = false
private var mIconsSelected: List<IconImageCustom> = ArrayList()
private var mDatabase: Database? = null
private var mExternalFileHelper: ExternalFileHelper? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_icon_picker)
mDatabase = Database.getInstance()
toolbar = findViewById(R.id.toolbar)
toolbar.title = " "
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
updateIconsSelectedViews()
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
mExternalFileHelper = ExternalFileHelper(this)
uploadButton = findViewById(R.id.icon_picker_upload)
if (mDatabase?.allowCustomIcons == true) {
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else {
uploadButton.visibility = View.GONE
}
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
intent?.getParcelableExtra<IconImage>(EXTRA_ICON)?.let {
mIconImage = it
}
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add(R.id.icon_picker_fragment, IconPickerFragment.getInstance(
// Default selection tab
if (mIconImage.custom.isUnknown)
IconPickerFragment.IconTab.STANDARD
else
IconPickerFragment.IconTab.CUSTOM
), ICON_PICKER_FRAGMENT_TAG)
}
} else {
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
}
// Focus view to reinitialize timeout
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
mIconImage.standard = iconStandard
// Remove the custom icon if a standard one is selected
mIconImage.custom = IconImageCustom()
setResult()
finish()
}
iconPickerViewModel.customIconPicked.observe(this) { iconCustom ->
// Keep the standard icon if a custom one is selected
mIconImage.custom = iconCustom
setResult()
finish()
}
iconPickerViewModel.customIconsSelected.observe(this) { iconsSelected ->
mIconsSelected = iconsSelected
updateIconsSelectedViews()
}
iconPickerViewModel.customIconAdded.observe(this) { iconCustomAdded ->
if (iconCustomAdded.error && !iconCustomAdded.errorConsumed) {
Snackbar.make(coordinatorLayout, iconCustomAdded.errorStringId, Snackbar.LENGTH_LONG).asError().show()
iconCustomAdded.errorConsumed = true
}
uploadButton.isEnabled = true
}
iconPickerViewModel.customIconRemoved.observe(this) { iconCustomRemoved ->
if (iconCustomRemoved.error && !iconCustomRemoved.errorConsumed) {
Snackbar.make(coordinatorLayout, iconCustomRemoved.errorStringId, Snackbar.LENGTH_LONG).asError().show()
iconCustomRemoved.errorConsumed = true
}
uploadButton.isEnabled = true
}
}
private fun updateIconsSelectedViews() {
if (mIconsSelected.isEmpty()) {
mCustomIconsSelectionMode = false
toolbar.title = " "
} else {
mCustomIconsSelectionMode = true
toolbar.title = mIconsSelected.size.toString()
}
invalidateOptionsMenu()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(EXTRA_ICON, mIconImage)
}
override fun onResume() {
super.onResume()
// Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
// Padding if lock button visible
toolbar.updateLockPaddingLeft()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
if (mCustomIconsSelectionMode) {
menuInflater.inflate(R.menu.icon, menu)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
if (mCustomIconsSelectionMode) {
iconPickerViewModel.deselectAllCustomIcons()
} else {
onBackPressed()
}
}
R.id.menu_delete -> {
mIconsSelected.forEach { iconToRemove ->
removeCustomIcon(iconToRemove)
}
}
}
return super.onOptionsItemSelected(item)
}
private fun addCustomIcon(iconToUploadUri: Uri?) {
uploadButton.isEnabled = false
mainScope.launch {
withContext(Dispatchers.IO) {
// on Progress with thread
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
UriUtil.getFileData(this@IconPickerActivity, iconToUploadUri)?.also { documentFile ->
if (documentFile.length() > MAX_ICON_SIZE) {
iconCustomState.errorStringId = R.string.error_file_to_big
} else {
mDatabase?.buildNewCustomIcon { customIcon, binary ->
if (customIcon != null) {
iconCustomState.iconCustom = customIcon
mDatabase?.let { database ->
BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(
contentResolver,
database,
iconToUploadUri,
binary)
when {
binary == null -> {
}
binary.getSize() <= 0 -> {
}
database.isCustomIconBinaryDuplicate(binary) -> {
iconCustomState.errorStringId = R.string.error_duplicate_file
}
else -> {
iconCustomState.error = false
}
}
}
if (iconCustomState.error) {
mDatabase?.removeCustomIcon(customIcon)
}
}
}
}
}
iconCustomState
}
withContext(Dispatchers.Main) {
asyncResult.await()?.let { customIcon ->
iconPickerViewModel.addCustomIcon(customIcon)
}
}
}
}
}
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
uploadButton.isEnabled = false
iconPickerViewModel.deselectAllCustomIcons()
mDatabase?.removeCustomIcon(iconImageCustom)
iconPickerViewModel.removeCustomIcon(
IconPickerViewModel.IconCustomState(iconImageCustom, false, R.string.error_remove_file)
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
addCustomIcon(uri)
}
}
private fun setResult() {
setResult(Activity.RESULT_OK, Intent().apply {
putExtra(EXTRA_ICON, mIconImage)
})
}
override fun onBackPressed() {
setResult()
super.onBackPressed()
}
companion object {
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
private const val ICON_SELECTED_REQUEST = 15861
private const val EXTRA_ICON = "EXTRA_ICON"
private const val MAX_ICON_SIZE = 5242880
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) {
if (requestCode == ICON_SELECTED_REQUEST) {
if (resultCode == Activity.RESULT_OK) {
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
}
}
}
fun launch(context: Activity,
previousIcon: IconImage?) {
// Create an instance to return the picker icon
context.startActivityForResult(
Intent(context,
IconPickerActivity::class.java).apply {
if (previousIcon != null)
putExtra(EXTRA_ICON, previousIcon)
},
ICON_SELECTED_REQUEST)
}
}
}

View File

@@ -26,6 +26,7 @@ import android.text.format.Formatter
import android.util.Log import android.util.Log
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import com.igreenwood.loupe.Loupe import com.igreenwood.loupe.Loupe
@@ -33,10 +34,13 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import kotlinx.android.synthetic.main.activity_image_viewer.* import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import kotlin.math.max
class ImageViewerActivity : LockingActivity() { class ImageViewerActivity : LockingActivity() {
private var mDatabase: Database? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -47,22 +51,39 @@ class ImageViewerActivity : LockingActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container)
val imageView: ImageView = findViewById(R.id.image_viewer_image) val imageView: ImageView = findViewById(R.id.image_viewer_image)
val progressView: View = findViewById(R.id.image_viewer_progress) val progressView: View = findViewById(R.id.image_viewer_progress)
// Approximately, to not OOM and allow a zoom
val mImagePreviewMaxWidth = max(
resources.displayMetrics.widthPixels * 2,
resources.displayMetrics.heightPixels * 2
)
mDatabase = Database.getInstance()
try { try {
progressView.visibility = View.VISIBLE progressView.visibility = View.VISIBLE
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment -> intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
supportActionBar?.title = attachment.name supportActionBar?.title = attachment.name
supportActionBar?.subtitle = Formatter.formatFileSize(this, attachment.binaryAttachment.length)
Attachment.loadBitmap(attachment, Database.getInstance().loadedCipherKey) { bitmapLoaded -> val size = attachment.binaryData.getSize()
if (bitmapLoaded == null) { supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
finish()
} else { mDatabase?.let { database ->
progressView.visibility = View.GONE BinaryDatabaseManager.loadBitmap(
imageView.setImageBitmap(bitmapLoaded) database,
attachment.binaryData,
mImagePreviewMaxWidth
) { bitmapLoaded ->
if (bitmapLoaded == null) {
finish()
} else {
progressView.visibility = View.GONE
imageView.setImageBitmap(bitmapLoaded)
}
} }
} }
} ?: finish() } ?: finish()
@@ -71,7 +92,7 @@ class ImageViewerActivity : LockingActivity() {
finish() finish()
} }
Loupe.create(imageView, image_viewer_container) { Loupe.create(imageView, imageContainerView) {
onViewTranslateListener = object : Loupe.OnViewTranslateListener { onViewTranslateListener = object : Loupe.OnViewTranslateListener {
override fun onStart(view: ImageView) { override fun onStart(view: ImageView) {

View File

@@ -36,15 +36,13 @@ import android.widget.*
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.*
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.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
@@ -71,7 +69,6 @@ import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException import java.io.FileNotFoundException
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener { open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
@@ -84,8 +81,9 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
private var confirmButtonView: Button? = null private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null private var checkboxKeyFileView: CompoundButton? = null
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private var infoContainerView: ViewGroup? = null private var infoContainerView: ViewGroup? = null
private lateinit var coordinatorLayout: CoordinatorLayout
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels() private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
@@ -94,7 +92,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
private var mDatabaseKeyFileUri: Uri? = null private var mDatabaseKeyFileUri: Uri? = null
private var mRememberKeyFile: Boolean = false private var mRememberKeyFile: Boolean = false
private var mSelectFileHelper: SelectFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mPermissionAsked = false private var mPermissionAsked = false
private var readOnly: Boolean = false private var readOnly: Boolean = false
@@ -131,18 +129,14 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
checkboxPasswordView = findViewById(R.id.password_checkbox) checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox) checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
infoContainerView = findViewById(R.id.activity_password_info_container) infoContainerView = findViewById(R.id.activity_password_info_container)
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mSelectFileHelper = SelectFileHelper(this@PasswordActivity) mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
keyFileSelectionView?.apply { keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
}
passwordView?.setOnEditorActionListener(onEditorActionListener) passwordView?.setOnEditorActionListener(onEditorActionListener)
passwordView?.addTextChangedListener(object : TextWatcher { passwordView?.addTextChangedListener(object : TextWatcher {
@@ -271,7 +265,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
resultError = "$resultError $resultMessage" resultError = "$resultError $resultMessage"
} }
Log.e(TAG, resultError) Log.e(TAG, resultError)
Snackbar.make(activity_password_coordinator_layout, Snackbar.make(coordinatorLayout,
resultError, resultError,
Snackbar.LENGTH_LONG).asError().show() Snackbar.LENGTH_LONG).asError().show()
} }
@@ -468,6 +462,11 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
override fun onPause() { override fun onPause() {
mProgressDatabaseTaskProvider?.unregisterProgressTask() mProgressDatabaseTaskProvider?.unregisterProgressTask()
// To prevent biometric prompt to appearing outside of the app
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockFragment?.disconnect(hideViews = false, closePrompt = true)
}
// Reinit locking activity UI variable // Reinit locking activity UI variable
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true mAllowAutoOpenBiometricPrompt = true
@@ -523,7 +522,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|| mSpecialMode == SpecialMode.REGISTRATION) || mSpecialMode == SpecialMode.REGISTRATION)
) { ) {
Log.e(TAG, getString(R.string.autofill_read_only_save)) Log.e(TAG, getString(R.string.autofill_read_only_save))
Snackbar.make(activity_password_coordinator_layout, Snackbar.make(coordinatorLayout,
R.string.autofill_read_only_save, R.string.autofill_read_only_save,
Snackbar.LENGTH_LONG).asError().show() Snackbar.LENGTH_LONG).asError().show()
} else { } else {
@@ -700,8 +699,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
} }
var keyFileResult = false var keyFileResult = false
mSelectFileHelper?.let { mExternalFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data
) { uri -> ) { uri ->
if (uri != null) { if (uri != null) {
mDatabaseKeyFileUri = uri mDatabaseKeyFileUri = uri
@@ -714,7 +713,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
when (resultCode) { when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK -> { LockingActivity.RESULT_EXIT_LOCK -> {
clearCredentialsViews() clearCredentialsViews()
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this)) Database.getInstance().clearAndClose(this)
} }
Activity.RESULT_CANCELED -> { Activity.RESULT_CANCELED -> {
clearCredentialsViews() clearCredentialsViews()

View File

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

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -31,7 +32,7 @@ class DuplicateUuidDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity -> activity?.let { activity ->
// Use the Builder class for convenient dialog construction // Use the Builder class for convenient dialog construction
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply { val builder = AlertDialog.Builder(activity).apply {
val message = getString(R.string.contains_duplicate_uuid) + val message = getString(R.string.contains_duplicate_uuid) +
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure) "\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
setMessage(message) setMessage(message)

View File

@@ -31,16 +31,17 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.IconPickerActivity
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.view.ExpirationView import com.kunzisoft.keepass.view.ExpirationView
import org.joda.time.DateTime import org.joda.time.DateTime
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener { class GroupEditDialogFragment : DialogFragment() {
private var mDatabase: Database? = null private var mDatabase: Database? = null
@@ -112,8 +113,6 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
arguments?.apply { arguments?.apply {
if (containsKey(KEY_ACTION_ID)) if (containsKey(KEY_ACTION_ID))
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID)) mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
if (mEditGroupDialogAction === CREATION)
mGroupInfo.notes = ""
if (containsKey(KEY_GROUP_INFO)) { if (containsKey(KEY_GROUP_INFO)) {
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
} }
@@ -144,7 +143,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
} }
iconButtonView.setOnClickListener { _ -> iconButtonView.setOnClickListener { _ ->
IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment") IconPickerActivity.launch(activity, mGroupInfo.icon)
} }
return builder.create() return builder.create()
@@ -204,13 +203,11 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
} }
private fun assignIconView() { private fun assignIconView() {
if (mDatabase?.drawFactory != null) { mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor)
iconButtonView.assignDatabaseIcon(mDatabase?.drawFactory!!, mGroupInfo.icon, iconColor)
}
} }
override fun iconPicked(bundle: Bundle) { fun setIcon(icon: IconImage) {
mGroupInfo.icon = IconPickerDialogFragment.getIconStandardFromBundle(bundle) ?: mGroupInfo.icon mGroupInfo.icon = icon
assignIconView() assignIconView()
} }
@@ -222,14 +219,19 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
} }
private fun isValid(): Boolean { private fun isValid(): Boolean {
if (nameTextView.text.toString().isEmpty()) { val error = mEditGroupListener?.isValidGroupName(nameTextView.text.toString()) ?: Error(false, null)
nameTextLayoutView.error = getString(R.string.error_no_name) error.messageId?.let { messageId ->
return false 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 { interface EditGroupListener {
fun isValidGroupName(name: String): Error
fun approveEditGroup(action: EditGroupDialogAction, fun approveEditGroup(action: EditGroupDialogAction,
groupInfo: GroupInfo) groupInfo: GroupInfo)
fun cancelEditGroup(action: EditGroupDialogAction, fun cancelEditGroup(action: EditGroupDialogAction,
@@ -242,15 +244,16 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
const val KEY_ACTION_ID = "KEY_ACTION_ID" const val KEY_ACTION_ID = "KEY_ACTION_ID"
const val KEY_GROUP_INFO = "KEY_GROUP_INFO" const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
fun build(): GroupEditDialogFragment { fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
val bundle = Bundle() val bundle = Bundle()
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal) bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
val fragment = GroupEditDialogFragment() val fragment = GroupEditDialogFragment()
fragment.arguments = bundle fragment.arguments = bundle
return fragment return fragment
} }
fun build(groupInfo: GroupInfo): GroupEditDialogFragment { fun update(groupInfo: GroupInfo): GroupEditDialogFragment {
val bundle = Bundle() val bundle = Bundle()
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal) bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
bundle.putParcelable(KEY_GROUP_INFO, groupInfo) bundle.putParcelable(KEY_GROUP_INFO, groupInfo)

View File

@@ -1,147 +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.dialogs
import android.app.Dialog
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.GridView
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.ImageViewCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.icons.IconPack
import com.kunzisoft.keepass.icons.IconPackChooser
class IconPickerDialogFragment : DialogFragment() {
private var iconPickerListener: IconPickerListener? = null
private var iconPack: IconPack? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
iconPickerListener = context as IconPickerListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + IconPickerListener::class.java.name)
}
}
override fun onDetach() {
iconPickerListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
iconPack = IconPackChooser.getSelectedIconPack(requireContext())
// Inflate and set the layout for the dialog
// Pass null as the parent view because its going in the dialog layout
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_picker, null)
builder.setView(root)
val currIconGridView = root.findViewById<GridView>(R.id.IconGridView)
currIconGridView.adapter = ImageAdapter(activity)
currIconGridView.setOnItemClickListener { _, _, position, _ ->
val bundle = Bundle()
bundle.putParcelable(KEY_ICON_STANDARD, IconImageStandard(position))
iconPickerListener?.iconPicked(bundle)
dismiss()
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() {
override fun getCount(): Int {
return iconPack?.numberOfIcons() ?: 0
}
override fun getItem(position: Int): Any? {
return null
}
override fun getItemId(position: Int): Long {
return 0
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val currentView: View = convertView
?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.inflate(R.layout.item_icon, parent, false)
iconPack?.let { iconPack ->
val iconImageView = currentView.findViewById<ImageView>(R.id.icon_image)
iconImageView.setImageResource(iconPack.iconToResId(position))
// Assign color if icons are tintable
if (iconPack.tintable()) {
// Retrieve the textColor to tint the icon
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
ta.recycle()
}
}
return currentView
}
}
interface IconPickerListener {
fun iconPicked(bundle: Bundle)
}
companion object {
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
fun getIconStandardFromBundle(bundle: Bundle): IconImageStandard? {
return bundle.getParcelable(KEY_ICON_STANDARD)
}
fun launch(activity: FragmentActivity) {
// Create an instance of the dialog fragment and show it
val dialog = IconPickerDialogFragment()
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
}
}
}

View File

@@ -46,6 +46,7 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
import com.kunzisoft.keepass.otp.OtpTokenType import com.kunzisoft.keepass.otp.OtpTokenType
import com.kunzisoft.keepass.otp.OtpType import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.otp.TokenCalculator import com.kunzisoft.keepass.otp.TokenCalculator
import com.kunzisoft.keepass.utils.UriUtil
import java.util.* import java.util.*
class SetOTPDialogFragment : DialogFragment() { class SetOTPDialogFragment : DialogFragment() {
@@ -223,13 +224,16 @@ class SetOTPDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
builder.apply { builder.apply {
setTitle(R.string.entry_setup_otp)
setView(root) setView(root)
.setPositiveButton(android.R.string.ok) {_, _ -> } .setPositiveButton(android.R.string.ok) {_, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
} }
} }
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.otp_explanation_url)
}
return builder.create() return builder.create()
} }
return super.onCreateDialog(savedInstanceState) return super.onCreateDialog(savedInstanceState)

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities.fragments
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
@@ -34,6 +34,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.stylish.StylishFragment
@@ -44,7 +45,6 @@ import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -78,13 +78,12 @@ class EntryEditFragment: StylishFragment() {
var drawFactory: IconDrawableFactory? = null var drawFactory: IconDrawableFactory? = null
var setOnDateClickListener: (() -> Unit)? = null var setOnDateClickListener: (() -> Unit)? = null
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
var setOnIconViewClickListener: View.OnClickListener? = null var setOnIconViewClickListener: ((IconImage) -> Unit)? = null
var setOnEditCustomField: ((Field) -> Unit)? = null var setOnEditCustomField: ((Field) -> Unit)? = null
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
// Elements to modify the current entry // Elements to modify the current entry
private var mEntryInfo = EntryInfo() private var mEntryInfo = EntryInfo()
private var mBinaryCipherKey: Database.LoadedKey? = null
private var mLastFocusedEditField: FocusedEditField? = null private var mLastFocusedEditField: FocusedEditField? = null
private var mExtraViewToRequestFocus: EditText? = null private var mExtraViewToRequestFocus: EditText? = null
@@ -100,7 +99,7 @@ class EntryEditFragment: StylishFragment() {
entryTitleView = rootView.findViewById(R.id.entry_edit_title) entryTitleView = rootView.findViewById(R.id.entry_edit_title)
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button) entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
entryIconView.setOnClickListener { entryIconView.setOnClickListener {
setOnIconViewClickListener?.onClick(it) setOnIconViewClickListener?.invoke(mEntryInfo.icon)
} }
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name) entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
@@ -122,7 +121,9 @@ class EntryEditFragment: StylishFragment() {
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container) attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list) attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext()) attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
attachmentsAdapter.binaryCipherKey = arguments?.getSerializable(KEY_BINARY_CIPHER_KEY) as? Database.LoadedKey? // TODO retrieve current database with its unique key
attachmentsAdapter.database = Database.getInstance()
//attachmentsAdapter.database = arguments?.getInt(KEY_DATABASE)
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize -> attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) { if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true) attachmentsContainerView.collapse(true)
@@ -239,9 +240,7 @@ class EntryEditFragment: StylishFragment() {
} }
set(value) { set(value) {
mEntryInfo.icon = value mEntryInfo.icon = value
drawFactory?.let { drawFactory -> drawFactory?.assignDatabaseIcon(entryIconView, value, iconColor)
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
}
} }
var username: String var username: String
@@ -315,7 +314,8 @@ class EntryEditFragment: StylishFragment() {
itemView?.id = View.NO_ID itemView?.id = View.NO_ID
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container) val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected)
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
extraFieldValueContainer?.hint = extraField.name extraFieldValueContainer?.hint = extraField.name
extraFieldValueContainer?.id = View.NO_ID extraFieldValueContainer?.id = View.NO_ID
@@ -503,7 +503,6 @@ class EntryEditFragment: StylishFragment() {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
populateEntryWithViews() populateEntryWithViews()
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo) outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
outState.putSerializable(KEY_BINARY_CIPHER_KEY, mBinaryCipherKey)
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField) outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
@@ -511,15 +510,16 @@ class EntryEditFragment: StylishFragment() {
companion object { companion object {
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO" const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
const val KEY_BINARY_CIPHER_KEY = "KEY_BINARY_CIPHER_KEY" const val KEY_DATABASE = "KEY_DATABASE"
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD" const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
fun getInstance(entryInfo: EntryInfo?, fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
loadedKey: Database.LoadedKey?): EntryEditFragment { //database: Database?): EntryEditFragment {
return EntryEditFragment().apply { return EntryEditFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo) putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
putSerializable(KEY_BINARY_CIPHER_KEY, loadedKey) // TODO Unique database key database.key
putInt(KEY_DATABASE, 0)
} }
} }
} }

View File

@@ -0,0 +1,87 @@
/*
* 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.activities.fragments
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
class IconCustomFragment : IconFragment<IconImageCustom>() {
override fun retrieveMainLayoutId(): Int {
return R.layout.fragment_icon_grid
}
override fun defineIconList() {
mDatabase?.doForEachCustomIcons { customIcon, _ ->
iconPickerAdapter.addIcon(customIcon, false)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
iconPickerViewModel.customIconsSelected.observe(viewLifecycleOwner) { customIconsSelected ->
if (customIconsSelected.isEmpty()) {
iconActionSelectionMode = false
iconPickerAdapter.deselectAllIcons()
} else {
iconActionSelectionMode = true
iconPickerAdapter.updateIconSelectedState(customIconsSelected)
}
}
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { iconCustomAdded ->
if (!iconCustomAdded.error) {
iconCustomAdded?.iconCustom?.let { icon ->
iconPickerAdapter.addIcon(icon)
iconCustomAdded.iconCustom = null
}
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
}
}
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
if (!iconCustomRemoved.error) {
iconCustomRemoved?.iconCustom?.let { icon ->
iconPickerAdapter.removeIcon(icon)
iconCustomRemoved.iconCustom = null
}
}
}
}
override fun onIconClickListener(icon: IconImageCustom) {
if (iconActionSelectionMode) {
// Same long click behavior after each single click
onIconLongClickListener(icon)
} else {
iconPickerViewModel.pickCustomIcon(icon)
}
}
override fun onIconLongClickListener(icon: IconImageCustom) {
// Select or deselect item if already selected
icon.selected = !icon.selected
iconPickerAdapter.updateIcon(icon)
iconActionSelectionMode = iconPickerAdapter.containsAnySelectedIcon()
iconPickerViewModel.selectCustomIcons(iconPickerAdapter.getSelectedIcons())
}
}

View File

@@ -0,0 +1,100 @@
/*
* 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.activities.fragments
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconPickerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
IconPickerAdapter.IconPickerListener<T> {
protected lateinit var iconsGridView: RecyclerView
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
protected var iconActionSelectionMode = false
protected var mDatabase: Database? = null
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
abstract fun retrieveMainLayoutId(): Int
abstract fun defineIconList()
override fun onAttach(context: Context) {
super.onAttach(context)
mDatabase = Database.getInstance()
// Retrieve the textColor to tint the icon
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
ta?.recycle()
iconPickerAdapter = IconPickerAdapter<T>(context, tintColor).apply {
iconDrawableFactory = mDatabase?.iconDrawableFactory
}
CoroutineScope(Dispatchers.IO).launch {
val populateList = launch {
iconPickerAdapter.clear()
defineIconList()
}
withContext(Dispatchers.Main) {
populateList.join()
iconPickerAdapter.notifyDataSetChanged()
}
}
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
val root = inflater.inflate(retrieveMainLayoutId(), container, false)
iconsGridView = root.findViewById(R.id.icons_grid_view)
iconsGridView.adapter = iconPickerAdapter
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
iconPickerAdapter.iconPickerListener = this
}
fun onIconDeleteClicked() {
iconActionSelectionMode = false
}
}

View File

@@ -0,0 +1,77 @@
package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconPickerFragment : StylishFragment() {
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
private lateinit var viewPager: ViewPager2
private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
private var mDatabase: Database? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_icon_picker, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mDatabase = Database.getInstance()
viewPager = view.findViewById(R.id.icon_picker_pager)
val tabLayout = view.findViewById<TabLayout>(R.id.icon_picker_tabs)
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (mDatabase?.allowCustomIcons == true) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
1 -> getString(R.string.icon_section_custom)
else -> getString(R.string.icon_section_standard)
}
}.attach()
arguments?.apply {
if (containsKey(ICON_TAB_ARG)) {
viewPager.currentItem = getInt(ICON_TAB_ARG)
}
remove(ICON_TAB_ARG)
}
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { _ ->
viewPager.currentItem = 1
}
}
enum class IconTab {
STANDARD, CUSTOM
}
companion object {
private const val ICON_TAB_ARG = "ICON_TAB_ARG"
fun getInstance(iconTab: IconTab): IconPickerFragment {
val fragment = IconPickerFragment()
fragment.arguments = Bundle().apply {
putInt(ICON_TAB_ARG, iconTab.ordinal)
}
return fragment
}
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2021 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePassDX. * This file is part of KeePassDX.
* *
@@ -17,35 +17,27 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.stream package com.kunzisoft.keepass.activities.fragments
import java.io.IOException import com.kunzisoft.keepass.R
import java.io.OutputStream import com.kunzisoft.keepass.database.element.icon.IconImageStandard
class NullOutputStream : OutputStream() {
@Throws(IOException::class) class IconStandardFragment : IconFragment<IconImageStandard>() {
override fun close() {
super.close() override fun retrieveMainLayoutId(): Int {
return R.layout.fragment_icon_grid
} }
@Throws(IOException::class) override fun defineIconList() {
override fun flush() { mDatabase?.doForEachStandardIcons { standardIcon ->
super.flush() iconPickerAdapter.addIcon(standardIcon, false)
}
} }
@Throws(IOException::class) override fun onIconClickListener(icon: IconImageStandard) {
override fun write(buffer: ByteArray, offset: Int, count: Int) { iconPickerViewModel.pickStandardIcon(icon)
super.write(buffer, offset, count)
} }
@Throws(IOException::class) override fun onIconLongClickListener(icon: IconImageStandard) {}
override fun write(buffer: ByteArray) { }
super.write(buffer)
}
@Throws(IOException::class)
override fun write(oneByte: Int) {
}
}

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities.fragments
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -28,6 +28,7 @@ import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
@@ -310,13 +311,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
menu?.removeItem(R.id.menu_edit) 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 if (readOnly
|| isASearchResult || isASearchResult
|| nodes.any { it.type == Type.GROUP }) { || nodes.any { it.type == Type.GROUP }) {
// TODO Copy For Group
menu?.removeItem(R.id.menu_copy) menu?.removeItem(R.id.menu_copy)
menu?.removeItem(R.id.menu_move)
} }
// Deletion // 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

@@ -20,11 +20,11 @@
package com.kunzisoft.keepass.activities.stylish package com.kunzisoft.keepass.activities.stylish
import android.content.Context import android.content.Context
import androidx.annotation.StyleRes import android.content.res.Configuration
import androidx.preference.PreferenceManager
import android.util.Log import android.util.Log
import androidx.annotation.StyleRes
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.PreferencesUtil
/** /**
* Class that provides functions to retrieve and assign a theme to a module * Class that provides functions to retrieve and assign a theme to a module
@@ -37,18 +37,63 @@ object Stylish {
* Initialize the class with a theme preference * Initialize the class with a theme preference
* @param context Context to retrieve the theme preference * @param context Context to retrieve the theme preference
*/ */
fun init(context: Context) { fun load(context: Context) {
val stylishPrefKey = context.getString(R.string.setting_style_key)
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName) Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
themeString = PreferenceManager.getDefaultSharedPreferences(context).getString(stylishPrefKey, context.getString(R.string.list_style_name_light)) themeString = PreferencesUtil.getStyle(context)
}
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
else -> {
when (context.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) {
Configuration.UI_MODE_NIGHT_YES -> true
else -> false
}
}
}
return if (systemNightMode) {
retrieveEquivalentNightStyle(context, styleString)
} else {
retrieveEquivalentLightStyle(context, styleString)
}
}
fun retrieveEquivalentLightStyle(context: Context, styleString: String): String {
return when (styleString) {
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue)
context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red)
context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple)
else -> styleString
}
}
private fun retrieveEquivalentNightStyle(context: Context, styleString: String): String {
return when (styleString) {
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night)
context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night)
context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark)
else -> styleString
}
}
fun defaultStyle(context: Context): String {
return context.getString(R.string.list_style_name_light)
} }
/** /**
* Assign the style to the class attribute * Assign the style to the class attribute
* @param styleString Style id String * @param styleString Style id String
*/ */
fun assignStyle(styleString: String) { fun assignStyle(context: Context, styleString: String) {
themeString = styleString PreferencesUtil.setStyle(context, styleString)
} }
/** /**
@@ -58,13 +103,16 @@ object Stylish {
*/ */
@StyleRes @StyleRes
fun getThemeId(context: Context): Int { fun getThemeId(context: Context): Int {
return when (retrieveEquivalentSystemStyle(context, themeString ?: context.getString(R.string.list_style_name_light))) {
return when (themeString) {
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
context.getString(R.string.list_style_name_white) -> R.style.KeepassDXStyle_White
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
else -> R.style.KeepassDXStyle_Light else -> R.style.KeepassDXStyle_Light

View File

@@ -42,6 +42,7 @@ abstract class StylishFragment : Fragment() {
contextThemed = ContextThemeWrapper(context, themeId) contextThemed = ContextThemeWrapper(context, themeId)
} }
@Suppress("DEPRECATION")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// To fix status bar color // To fix status bar color
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -53,14 +54,21 @@ abstract class StylishFragment : Fragment() {
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
taStatusBarColor?.recycle() taStatusBarColor?.recycle()
} catch (e: Exception) {} } catch (e: Exception) {}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
if (taWindowStatusLight?.getBoolean(0, false) == true) {
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
taWindowStatusLight?.recycle()
} catch (e: Exception) {}
}
try { try {
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor)) val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
taNavigationBarColor?.recycle() taNavigationBarColor?.recycle()
} catch (e: Exception) {} } catch (e: Exception) {}
} }
return super.onCreateView(inflater, container, savedInstanceState) return super.onCreateView(inflater, container, savedInstanceState)
} }

View File

@@ -32,22 +32,28 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.ImageViewerActivity import com.kunzisoft.keepass.activities.ImageViewerActivity
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.view.expand import com.kunzisoft.keepass.view.expand
import kotlin.math.max
class EntryAttachmentsItemsAdapter(context: Context) class EntryAttachmentsItemsAdapter(context: Context)
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) { : AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
var binaryCipherKey: Database.LoadedKey? = null var database: Database? = null
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
// Approximately
private val mImagePreviewMaxWidth = max(
context.resources.displayMetrics.widthPixels,
context.resources.getDimensionPixelSize(R.dimen.item_file_info_height)
)
private var mTitleColor: Int private var mTitleColor: Int
init { init {
@@ -76,17 +82,23 @@ class EntryAttachmentsItemsAdapter(context: Context)
if (entryAttachmentState.previewState == AttachmentState.NULL) { if (entryAttachmentState.previewState == AttachmentState.NULL) {
entryAttachmentState.previewState = AttachmentState.IN_PROGRESS entryAttachmentState.previewState = AttachmentState.IN_PROGRESS
// Load the bitmap image // Load the bitmap image
Attachment.loadBitmap(entryAttachmentState.attachment, binaryCipherKey) { imageLoaded -> database?.let { database ->
if (imageLoaded == null) { BinaryDatabaseManager.loadBitmap(
entryAttachmentState.previewState = AttachmentState.ERROR database,
visibility = View.GONE entryAttachmentState.attachment.binaryData,
onBinaryPreviewLoaded?.invoke(entryAttachmentState) mImagePreviewMaxWidth
} else { ) { imageLoaded ->
entryAttachmentState.previewState = AttachmentState.COMPLETE if (imageLoaded == null) {
setImageBitmap(imageLoaded) entryAttachmentState.previewState = AttachmentState.ERROR
if (visibility != View.VISIBLE) { visibility = View.GONE
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height)) { onBinaryPreviewLoaded?.invoke(entryAttachmentState)
onBinaryPreviewLoaded?.invoke(entryAttachmentState) } else {
entryAttachmentState.previewState = AttachmentState.COMPLETE
setImageBitmap(imageLoaded)
if (visibility != View.VISIBLE) {
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height)) {
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
}
} }
} }
} }
@@ -101,22 +113,23 @@ class EntryAttachmentsItemsAdapter(context: Context)
} }
holder.binaryFileBroken.apply { holder.binaryFileBroken.apply {
setColorFilter(Color.RED) setColorFilter(Color.RED)
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) { visibility = if (entryAttachmentState.attachment.binaryData.isCorrupted) {
View.VISIBLE View.VISIBLE
} else { } else {
View.GONE View.GONE
} }
} }
holder.binaryFileTitle.text = entryAttachmentState.attachment.name holder.binaryFileTitle.text = entryAttachmentState.attachment.name
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) { if (entryAttachmentState.attachment.binaryData.isCorrupted) {
holder.binaryFileTitle.setTextColor(Color.RED) holder.binaryFileTitle.setTextColor(Color.RED)
} else { } else {
holder.binaryFileTitle.setTextColor(mTitleColor) holder.binaryFileTitle.setTextColor(mTitleColor)
} }
holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachmentState.attachment.binaryAttachment.length) val size = entryAttachmentState.attachment.binaryData.getSize()
holder.binaryFileSize.text = Formatter.formatFileSize(context, size)
holder.binaryFileCompression.apply { holder.binaryFileCompression.apply {
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) { if (entryAttachmentState.attachment.binaryData.isCompressed) {
text = CompressionAlgorithm.GZip.getName(context.resources) text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE visibility = View.VISIBLE
} else { } else {

View File

@@ -0,0 +1,129 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
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
import com.kunzisoft.keepass.icons.IconDrawableFactory
class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tintIcon: Int)
: RecyclerView.Adapter<IconPickerAdapter<I>.CustomIconViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private val iconList = ArrayList<I>()
var iconDrawableFactory: IconDrawableFactory? = null
var iconPickerListener: IconPickerListener<I>? = null
val lastPosition: Int
get() = iconList.lastIndex
fun addIcon(icon: I, notify: Boolean = true) {
if (!iconList.contains(icon)) {
iconList.add(icon)
if (notify) {
notifyItemInserted(iconList.indexOf(icon))
}
}
}
fun updateIcon(icon: I) {
val index = iconList.indexOf(icon)
if (index != -1) {
iconList[index] = icon
notifyItemChanged(index)
}
}
fun updateIconSelectedState(icons: List<I>) {
icons.forEach { icon ->
val index = iconList.indexOf(icon)
if (index != -1
&& iconList[index].selected != icon.selected) {
iconList[index] = icon
notifyItemChanged(index)
}
}
}
fun removeIcon(icon: I) {
if (iconList.contains(icon)) {
val position = iconList.indexOf(icon)
iconList.remove(icon)
notifyItemRemoved(position)
}
}
fun containsAnySelectedIcon(): Boolean {
return iconList.firstOrNull { it.selected } != null
}
fun deselectAllIcons() {
iconList.forEachIndexed { index, icon ->
if (icon.selected) {
icon.selected = false
notifyItemChanged(index)
}
}
}
fun getSelectedIcons(): List<I> {
return iconList.filter { it.selected }
}
fun clear() {
iconList.clear()
}
fun setList(icons: List<I>) {
iconList.clear()
icons.forEach { iconImage ->
iconList.add(iconImage)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomIconViewHolder {
val view = inflater.inflate(R.layout.item_icon, parent, false)
return CustomIconViewHolder(view)
}
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)
}
holder.itemView.setOnLongClickListener {
iconPickerListener?.onIconLongClickListener(icon)
true
}
}
override fun getItemCount(): Int {
return iconList.size
}
interface IconPickerListener<I: IconImageDraw> {
fun onIconClickListener(icon: I)
fun onIconLongClickListener(icon: I)
}
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

@@ -0,0 +1,24 @@
package com.kunzisoft.keepass.adapters
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.kunzisoft.keepass.activities.fragments.IconCustomFragment
import com.kunzisoft.keepass.activities.fragments.IconStandardFragment
class IconPickerPagerAdapter(fragment: Fragment, val size: Int)
: FragmentStateAdapter(fragment) {
private val iconStandardFragment = IconStandardFragment()
private val iconCustomFragment = IconCustomFragment()
override fun getItemCount(): Int {
return size
}
override fun createFragment(position: Int): Fragment {
return when (position) {
1 -> iconCustomFragment
else -> iconStandardFragment
}
}
}

View File

@@ -28,6 +28,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback import androidx.recyclerview.widget.SortedListAdapterCallback
@@ -39,7 +40,6 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.setTextSize import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut import com.kunzisoft.keepass.view.strikeOut
@@ -100,9 +100,7 @@ class NodeAdapter (private val context: Context)
this.mDatabase = Database.getInstance() this.mDatabase = Database.getInstance()
// Color of content selection // Color of content selection
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
this.mContentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
taContentSelectionColor.recycle()
// Retrieve the color to tint the icon // Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK) this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
@@ -305,7 +303,7 @@ class NodeAdapter (private val context: Context)
} }
holder.imageIdentifier?.setColorFilter(iconColor) holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply { holder.icon.apply {
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor) mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
// Relative size of the icon // Relative size of the icon
layoutParams?.apply { layoutParams?.apply {
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt() height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()

View File

@@ -36,7 +36,6 @@ import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut import com.kunzisoft.keepass.view.strikeOut
@@ -81,10 +80,9 @@ class SearchEntryCursorAdapter(private val context: Context,
val viewHolder = view.tag as ViewHolder val viewHolder = view.tag as ViewHolder
// Assign image // Assign image
viewHolder.imageViewIcon?.assignDatabaseIcon( viewHolder.imageViewIcon?.let { iconView ->
database.drawFactory, database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor)
currentEntry.icon, }
iconColor)
// Assign title // Assign title
viewHolder.textViewTitle?.apply { viewHolder.textViewTitle?.apply {
@@ -108,14 +106,26 @@ class SearchEntryCursorAdapter(private val context: Context,
private fun getEntryFrom(cursor: Cursor): Entry? { private fun getEntryFrom(cursor: Cursor): Entry? {
return database.createEntry()?.apply { return database.createEntry()?.apply {
database.startManageEntry(this)
entryKDB?.let { entryKDB -> entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB, database.iconFactory) (cursor as EntryCursorKDB).populateEntry(entryKDB,
{ standardIconId ->
database.getStandardIcon(standardIconId)
},
{ customIconId ->
database.getCustomIcon(customIconId)
}
)
} }
entryKDBX?.let { entryKDBX -> entryKDBX?.let { entryKDBX ->
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, database.iconFactory) (cursor as EntryCursorKDBX).populateEntry(entryKDBX,
{ standardIconId ->
database.getStandardIcon(standardIconId)
},
{ customIconId ->
database.getCustomIcon(customIconId)
}
)
} }
database.stopManageEntry(this)
} }
} }
@@ -138,12 +148,14 @@ class SearchEntryCursorAdapter(private val context: Context,
if (searchGroup != null) { if (searchGroup != null) {
// Search in hide entries but not meta-stream // Search in hide entries but not meta-stream
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) { for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
database.startManageEntry(entry)
entry.entryKDB?.let { entry.entryKDB?.let {
cursorKDB?.addEntry(it) cursorKDB?.addEntry(it)
} }
entry.entryKDBX?.let { entry.entryKDBX?.let {
cursorKDBX?.addEntry(it) cursorKDBX?.addEntry(it)
} }
database.stopManageEntry(entry)
} }
} }

View File

@@ -22,19 +22,18 @@ package com.kunzisoft.keepass.app
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil
class App : MultiDexApplication() { class App : MultiDexApplication() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Stylish.init(this) Stylish.load(this)
PRNGFixes.apply() PRNGFixes.apply()
} }
override fun onTerminate() { override fun onTerminate() {
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this)) Database.getInstance().clearAndClose(this)
super.onTerminate() super.onTerminate()
} }
} }

View File

@@ -19,12 +19,10 @@
*/ */
package com.kunzisoft.keepass.app.database package com.kunzisoft.keepass.app.database
import android.content.ComponentName import android.content.*
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri import android.net.Uri
import android.os.IBinder import android.os.IBinder
import android.util.Log
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -41,62 +39,95 @@ class CipherDatabaseAction(context: Context) {
// Temp DAO to easily remove content if object no longer in memory // Temp DAO to easily remove content if object no longer in memory
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext) private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
private val mIntentAdvancedUnlockService = Intent(applicationContext,
AdvancedUnlockNotificationService::class.java)
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
private var mServiceConnection: ServiceConnection? = 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() { fun reloadPreferences() {
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext) useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
} }
@Synchronized @Synchronized
private fun attachService(performedAction: () -> Unit) { private fun serviceActionTask(startService: Boolean = false, performedAction: () -> Unit) {
// Check if a service is currently running else do nothing // Check if a service is currently running else call action without info
if (mBinder != null) { if (startService && mServiceConnection == null) {
attachService(performedAction)
} else {
performedAction.invoke() 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) {
applicationContext.startService(mIntentAdvancedUnlockService)
}
} }
} }
fun registerDatabaseListener(listener: DatabaseListener) { @Synchronized
mDatabaseListeners.add(listener) 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) { @Synchronized
mDatabaseListeners.remove(listener) private fun detachService() {
try {
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
} catch (e: Exception) {}
mServiceConnection?.let {
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
}
} }
interface DatabaseListener { private fun removeAllDataAndDetach() {
fun onDatabaseCleared() 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, fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) { cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
if (useTempDao) { if (useTempDao) {
attachService { serviceActionTask {
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri)) cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
} }
} else { } else {
@@ -121,7 +152,8 @@ class CipherDatabaseAction(context: Context) {
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity, fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
cipherDatabaseResultListener: (() -> Unit)? = null) { cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) { if (useTempDao) {
attachService { // The only case to create service (not needed to get an info)
serviceActionTask(true) {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity) mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke() cipherDatabaseResultListener?.invoke()
} }
@@ -146,7 +178,7 @@ class CipherDatabaseAction(context: Context) {
fun deleteByDatabaseUri(databaseUri: Uri, fun deleteByDatabaseUri(databaseUri: Uri,
cipherDatabaseResultListener: (() -> Unit)? = null) { cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) { if (useTempDao) {
attachService { serviceActionTask {
mBinder?.deleteByDatabaseUri(databaseUri) mBinder?.deleteByDatabaseUri(databaseUri)
cipherDatabaseResultListener?.invoke() cipherDatabaseResultListener?.invoke()
} }
@@ -163,15 +195,22 @@ class CipherDatabaseAction(context: Context) {
} }
fun deleteAll() { fun deleteAll() {
attachService { if (useTempDao) {
mBinder?.deleteAll() serviceActionTask {
mBinder?.deleteAll()
}
} }
// To erase the residues
IOActionTask( IOActionTask(
{ {
cipherDatabaseDao.deleteAll() cipherDatabaseDao.deleteAll()
} }
).execute() ).execute()
// Unbind
removeAllDataAndDetach()
} }
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
private val TAG = CipherDatabaseAction::class.java.name
}
} }

View File

@@ -46,8 +46,6 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.createIconFromDatabaseIcon
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
@@ -86,6 +84,24 @@ object AutofillHelper {
return "" return ""
} }
private fun newRemoteViews(context: Context,
remoteViewsText: String,
remoteViewsIcon: IconImage? = null): RemoteViews {
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
if (remoteViewsIcon != null) {
try {
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
}
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
}
return presentation
}
private fun buildDataset(context: Context, private fun buildDataset(context: Context,
entryInfo: EntryInfo, entryInfo: EntryInfo,
struct: StructureParser.Result, struct: StructureParser.Result,
@@ -116,6 +132,21 @@ object AutofillHelper {
} }
} }
/**
* Method to assign a drawable to a new icon from a database icon
*/
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
try {
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
return Icon.createWithBitmap(bitmap)
}
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
return null
}
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context, private fun buildInlinePresentationForEntry(context: Context,
@@ -267,26 +298,4 @@ object AutofillHelper {
activity.finish() activity.finish()
} }
} }
private fun newRemoteViews(context: Context,
remoteViewsText: String,
remoteViewsIcon: IconImage? = null): RemoteViews {
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
if (remoteViewsIcon != null) {
presentation.assignDatabaseIcon(context,
R.id.autofill_entry_icon,
Database.getInstance().drawFactory,
remoteViewsIcon,
ContextCompat.getColor(context, R.color.green))
}
return presentation
}
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
return createIconFromDatabaseIcon(context,
Database.getInstance().drawFactory,
entryInfo.icon,
ContextCompat.getColor(context, R.color.green))
}
} }

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.autofill package com.kunzisoft.keepass.autofill
import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.graphics.BlendMode import android.graphics.BlendMode
@@ -130,6 +131,7 @@ class KeeAutofillService : AutofillService() {
) )
} }
@SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(parseResult: StructureParser.Result,
searchInfo: SearchInfo, searchInfo: SearchInfo,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: InlineSuggestionsRequest?,

View File

@@ -223,9 +223,22 @@ class StructureParser(private val structure: AssistStructure) {
usernameValueCandidate = node.autofillValue usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}") 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, inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_PASSWORD, InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> { InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
result?.passwordId = autofillId result?.passwordId = autofillId
result?.passwordValue = node.autofillValue 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.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
@@ -68,7 +67,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private lateinit var cipherDatabaseAction : CipherDatabaseAction private lateinit var cipherDatabaseAction : CipherDatabaseAction
private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
// Only to fix multiple fingerprint menu #332 // Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false private var mAllowAdvancedUnlockMenu = false
@@ -125,8 +124,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext()) context?.let {
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext()) mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it)
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
}
keepConnection = false keepConnection = false
} }
@@ -176,34 +177,36 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
* Check unlock availability and change the current mode depending of device's state * Check unlock availability and change the current mode depending of device's state
*/ */
fun checkUnlockAvailability() { fun checkUnlockAvailability() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { context?.let { context ->
allowOpenBiometricPrompt = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) { allowOpenBiometricPrompt = true
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint) if (PreferencesUtil.isBiometricUnlockEnable(context)) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
// biometric not supported (by API level or hardware) so keep option hidden // biometric not supported (by API level or hardware) so keep option hidden
// or manually disable // or manually disable
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext()) val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context)
if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext()) if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.BIOMETRIC_UNAVAILABLE) toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) { } else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED) toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else {
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
} else { } 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() { private fun openBiometricSetting() {
mAdvancedUnlockInfoView?.setIconViewClickListener(false) { mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices... // 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) setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("") setAdvancedUnlockedMessageView("")
mAdvancedUnlockInfoView?.setIconViewClickListener(false) { context?.let { context ->
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS, mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
requireContext().getString(R.string.credential_before_click_advanced_unlock_button)) 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) { fun connect(databaseUri: Uri) {
showViews(true) showViews(true)
this.databaseFileUri = databaseUri this.databaseFileUri = databaseUri
cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener { cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
override fun onDatabaseCleared() { override fun onCipherDatabaseCleared() {
deleteEncryptedDatabaseKey() advancedUnlockManager?.closeBiometricPrompt()
checkUnlockAvailability()
} }
} }
cipherDatabaseAction.apply { cipherDatabaseAction.apply {
@@ -435,14 +441,12 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() { fun deleteEncryptedDatabaseKey() {
allowOpenBiometricPrompt = false
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
advancedUnlockManager?.closeBiometricPrompt() advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) { cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
checkUnlockAvailability() checkUnlockAvailability()
} }
} } ?: checkUnlockAvailability()
} }
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
@@ -479,7 +483,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential -> mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
advancedUnlockManager?.encryptData(credential) advancedUnlockManager?.encryptData(credential)
} }
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
} }
Mode.EXTRACT_CREDENTIAL -> { Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences // retrieve the encrypted value from preferences

View File

@@ -1,79 +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.crypto
import android.os.Build
import com.kunzisoft.keepass.crypto.engine.AesEngine
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.NoSuchAlgorithmException
import java.security.Security
import java.util.*
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
object CipherFactory {
private var blacklistInit = false
private var blacklisted: Boolean = false
init {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
}
fun deviceBlacklisted(): Boolean {
if (!blacklistInit) {
blacklistInit = true
// The Acer Iconia A500 is special and seems to always crash in the native crypto libraries
blacklisted = Build.MODEL == "A500"
}
return blacklisted
}
private fun hasNativeImplementation(transformation: String): Boolean {
return transformation == "AES/CBC/PKCS5Padding"
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
// Return the native AES if it is possible
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
Cipher.getInstance(transformation, AESProvider())
} else {
Cipher.getInstance(transformation)
}
}
/**
* Generate appropriate cipher based on KeePass 2.x UUID's
*/
@Throws(NoSuchAlgorithmException::class)
fun getInstance(uuid: UUID): CipherEngine {
return when (uuid) {
AesEngine.CIPHER_UUID -> AesEngine()
TwofishEngine.CIPHER_UUID -> TwofishEngine()
ChaCha20Engine.CIPHER_UUID -> ChaCha20Engine()
else -> throw NoSuchAlgorithmException("UUID unrecognized.")
}
}
}

View File

@@ -1,105 +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.crypto
import com.kunzisoft.keepass.stream.NullOutputStream
import com.kunzisoft.keepass.stream.longTo8Bytes
import java.io.IOException
import java.security.DigestOutputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.Mac
import kotlin.math.min
object CryptoUtil {
fun resizeKey(inBytes: ByteArray, inOffset: Int, cbIn: Int, cbOut: Int): ByteArray {
if (cbOut == 0) return ByteArray(0)
val hash: ByteArray = if (cbOut <= 32) {
hashSha256(inBytes, inOffset, cbIn)
} else {
hashSha512(inBytes, inOffset, cbIn)
}
if (cbOut == hash.size) {
return hash
}
val ret = ByteArray(cbOut)
if (cbOut < hash.size) {
System.arraycopy(hash, 0, ret, 0, cbOut)
} else {
var pos = 0
var r: Long = 0
while (pos < cbOut) {
val hmac: Mac
try {
hmac = Mac.getInstance("HmacSHA256")
} catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e)
}
val pbR = longTo8Bytes(r)
val part = hmac.doFinal(pbR)
val copy = min(cbOut - pos, part.size)
System.arraycopy(part, 0, ret, pos, copy)
pos += copy
r++
Arrays.fill(part, 0.toByte())
}
}
Arrays.fill(hash, 0.toByte())
return ret
}
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
return hashGen("SHA-256", data, offset, count)
}
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
return hashGen("SHA-512", data, offset, count)
}
private fun hashGen(transform: String, data: ByteArray, offset: Int, count: Int): ByteArray {
val hash: MessageDigest
try {
hash = MessageDigest.getInstance(transform)
} catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e)
}
val nos = NullOutputStream()
val dos = DigestOutputStream(nos, hash)
try {
dos.write(data, offset, count)
dos.close()
} catch (e: IOException) {
throw RuntimeException(e)
}
return hash.digest()
}
}

View File

@@ -1,71 +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.crypto
import org.bouncycastle.crypto.StreamCipher
import org.bouncycastle.crypto.engines.ChaCha7539Engine
import org.bouncycastle.crypto.engines.Salsa20Engine
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.crypto.params.ParametersWithIV
object StreamCipherFactory {
private val SALSA_IV = byteArrayOf(0xE8.toByte(), 0x30, 0x09, 0x4B, 0x97.toByte(), 0x20, 0x5D, 0x2A)
@Throws(Exception::class)
fun getInstance(alg: CrsAlgorithm?, key: ByteArray): StreamCipher {
return when {
alg === CrsAlgorithm.Salsa20 -> getSalsa20(key)
alg === CrsAlgorithm.ChaCha20 -> getChaCha20(key)
else -> throw Exception("Invalid random cipher")
}
}
private fun getSalsa20(key: ByteArray): StreamCipher {
// Build stream cipher key
val key32 = CryptoUtil.hashSha256(key)
val keyParam = KeyParameter(key32)
val ivParam = ParametersWithIV(keyParam, SALSA_IV)
val cipher = Salsa20Engine()
cipher.init(true, ivParam)
return cipher
}
private fun getChaCha20(key: ByteArray): StreamCipher {
// Build stream cipher key
val hash = CryptoUtil.hashSha512(key)
val key32 = ByteArray(32)
val iv = ByteArray(12)
System.arraycopy(hash, 0, key32, 0, 32)
System.arraycopy(hash, 32, iv, 0, 12)
val keyParam = KeyParameter(key32)
val ivParam = ParametersWithIV(keyParam, iv)
val cipher = ChaCha7539Engine()
cipher.init(true, ivParam)
return cipher
}
}

View File

@@ -1,68 +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.crypto.engine
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class AesEngine : CipherEngine() {
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
val cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding", androidOverride)
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
return cipher
}
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
return EncryptionAlgorithm.AESRijndael
}
companion object {
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0x31.toByte(),
0xC1.toByte(),
0xF2.toByte(),
0xE6.toByte(),
0xBF.toByte(),
0x71.toByte(),
0x43.toByte(),
0x50.toByte(),
0xBE.toByte(),
0x58.toByte(),
0x05.toByte(),
0x21.toByte(),
0x6A.toByte(),
0xFC.toByte(),
0x5A.toByte(),
0xFF.toByte()))
}
}

View File

@@ -1,72 +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.crypto.engine
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class TwofishEngine : CipherEngine() {
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
val cipher: Cipher = if (opmode == Cipher.ENCRYPT_MODE) {
CipherFactory.getInstance("Twofish/CBC/ZeroBytePadding", androidOverride)
} else {
CipherFactory.getInstance("Twofish/CBC/NoPadding", androidOverride)
}
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
return cipher
}
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
return EncryptionAlgorithm.Twofish
}
companion object {
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0xAD.toByte(),
0x68.toByte(),
0xF2.toByte(),
0x9F.toByte(),
0x57.toByte(),
0x6F.toByte(),
0x4B.toByte(),
0xB9.toByte(),
0xA3.toByte(),
0x6A.toByte(),
0xD4.toByte(),
0x7A.toByte(),
0xF9.toByte(),
0x65.toByte(),
0x34.toByte(),
0x6C.toByte()))
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2017 Brian Pellin, 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.crypto.finalkey
import com.kunzisoft.keepass.crypto.CipherFactory.deviceBlacklisted
object AESKeyTransformerFactory : KeyTransformer() {
override fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray? {
// Prefer the native final key implementation
val keyTransformer = if (!deviceBlacklisted()
&& NativeAESKeyTransformer.available()) {
NativeAESKeyTransformer()
} else {
// Fall back on the android crypto implementation
AndroidAESKeyTransformer()
}
return keyTransformer.transformMasterKey(seed, key, rounds)
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2017 Brian Pellin, 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.crypto.keyDerivation;
import com.kunzisoft.keepass.crypto.NativeLib;
import com.kunzisoft.keepass.utils.UnsignedInt;
import java.io.IOException;
public class Argon2Native {
enum CType {
ARGON2_D(0),
ARGON2_I(1),
ARGON2_ID(2);
int cValue = 0;
CType(int i) {
cValue = i;
}
}
public static byte[] transformKey(Argon2Kdf.Type type, byte[] password, byte[] salt, UnsignedInt parallelism,
UnsignedInt memory, UnsignedInt iterations, byte[] secretKey,
byte[] associatedData, UnsignedInt version) throws IOException {
NativeLib.INSTANCE.init();
CType cType = CType.ARGON2_D;
if (type.equals(Argon2Kdf.Type.ARGON2_ID))
cType = CType.ARGON2_ID;
return nTransformMasterKey(
cType.cValue,
password,
salt,
parallelism.toKotlinInt(),
memory.toKotlinInt(),
iterations.toKotlinInt(),
secretKey,
associatedData,
version.toKotlinInt());
}
private static native byte[] nTransformMasterKey(int type, byte[] password, byte[] salt, int parallelism,
int memory, int iterations, byte[] secretKey,
byte[] associatedData, int version) throws IOException;
}

View File

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

View File

@@ -25,6 +25,8 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -45,7 +47,7 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onStartRun() { override fun onStartRun() {
// Clear before we load // Clear before we load
mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(context)
} }
override fun onActionRun() { override fun onActionRun() {
@@ -55,7 +57,10 @@ class LoadDatabaseRunnable(private val context: Context,
mReadonly, mReadonly,
context.contentResolver, context.contentResolver,
UriUtil.getBinaryDir(context), UriUtil.getBinaryDir(context),
Database.LoadedKey.generateNewCipherKey(), { memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
LoadedKey.generateNewCipherKey(),
mFixDuplicateUUID, mFixDuplicateUUID,
progressTaskUpdater) progressTaskUpdater)
} }
@@ -80,7 +85,7 @@ class LoadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer // Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context) PreferencesUtil.saveCurrentTime(context)
} else { } else {
mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(context)
} }
} }

View File

@@ -23,20 +23,24 @@ import android.content.*
import android.content.Context.BIND_ABOVE_CLIENT import android.content.Context.BIND_ABOVE_CLIENT
import android.content.Context.BIND_NOT_FOREGROUND import android.content.Context.BIND_NOT_FOREGROUND
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
@@ -48,8 +52,8 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
@@ -251,11 +255,15 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
} }
private fun start(bundle: Bundle? = null, actionTask: String) { private fun start(bundle: Bundle? = null, actionTask: String) {
activity.stopService(intentDatabaseTask) try {
if (bundle != null) if (bundle != null)
intentDatabaseTask.putExtras(bundle) intentDatabaseTask.putExtras(bundle)
intentDatabaseTask.action = actionTask intentDatabaseTask.action = actionTask
activity.startService(intentDatabaseTask) activity.startService(intentDatabaseTask)
} catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e)
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
}
} }
/* /*
@@ -591,4 +599,8 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
} }
, ACTION_DATABASE_SAVE) , ACTION_DATABASE_SAVE)
} }
companion object {
private val TAG = ProgressDatabaseTaskProvider::class.java.name
}
} }

View File

@@ -21,6 +21,8 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -33,19 +35,23 @@ class ReloadDatabaseRunnable(private val context: Context,
private val mLoadDatabaseResult: ((Result) -> Unit)?) private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() { : ActionRunnable() {
private var tempCipherKey: Database.LoadedKey? = null private var tempCipherKey: LoadedKey? = null
override fun onStartRun() { override fun onStartRun() {
tempCipherKey = mDatabase.loadedCipherKey tempCipherKey = mDatabase.binaryCache.loadedCipherKey
// Clear before we load // Clear before we load
mDatabase.clear(UriUtil.getBinaryDir(context)) mDatabase.clear(UriUtil.getBinaryDir(context))
mDatabase.wasReloaded = true
} }
override fun onActionRun() { override fun onActionRun() {
try { try {
mDatabase.reloadData(context.contentResolver, mDatabase.reloadData(context.contentResolver,
UriUtil.getBinaryDir(context), UriUtil.getBinaryDir(context),
tempCipherKey ?: Database.LoadedKey.generateNewCipherKey(), { memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
tempCipherKey ?: LoadedKey.generateNewCipherKey(),
progressTaskUpdater) progressTaskUpdater)
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
setError(e) setError(e)
@@ -56,7 +62,7 @@ class ReloadDatabaseRunnable(private val context: Context,
PreferencesUtil.saveCurrentTime(context) PreferencesUtil.saveCurrentTime(context)
} else { } else {
tempCipherKey = null tempCipherKey = null
mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(context)
} }
} }

View File

@@ -52,16 +52,9 @@ class CopyNodesRunnable constructor(
if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) { if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) {
// Update entry with new values // Update entry with new values
mNewParent.touch(modified = false, touchParents = true) mNewParent.touch(modified = false, touchParents = true)
val entryCopied = database.copyEntryTo(currentNode as Entry, mNewParent) val entryCopied = database.copyEntryTo(currentNode as Entry, mNewParent)
if (entryCopied != null) { entryCopied.touch(modified = true, touchParents = true)
entryCopied.touch(modified = true, touchParents = true) mEntriesCopied.add(entryCopied)
mEntriesCopied.add(entryCopied)
} else {
Log.e(TAG, "Unable to create a copy of the entry")
setError(CopyEntryDatabaseException())
break@foreachNode
}
} else { } else {
// Only finish thread // Only finish thread
setError(CopyEntryDatabaseException()) setError(CopyEntryDatabaseException())

View File

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

View File

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

View File

@@ -42,14 +42,14 @@ class UpdateEntryRunnable constructor(
mNewEntry.addParentFrom(mOldEntry) mNewEntry.addParentFrom(mOldEntry)
// Build oldest attachments // Build oldest attachments
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true) val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true) val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true)
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments) val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
// Not use equals because only check name // Not use equals because only check name
newEntryAttachments.forEach { newAttachment -> newEntryAttachments.forEach { newAttachment ->
oldEntryAttachments.forEach { oldAttachment -> oldEntryAttachments.forEach { oldAttachment ->
if (oldAttachment.name == newAttachment.name if (oldAttachment.name == newAttachment.name
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment) && oldAttachment.binaryData == newAttachment.binaryData)
attachmentsToRemove.remove(oldAttachment) attachmentsToRemove.remove(oldAttachment)
} }
} }
@@ -60,7 +60,7 @@ class UpdateEntryRunnable constructor(
// Create an entry history (an entry history don't have history) // Create an entry history (an entry history don't have history)
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false)) mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
database.removeOldestEntryHistory(mOldEntry, database.binaryPool) database.removeOldestEntryHistory(mOldEntry, database.attachmentPool)
// Only change data in index // Only change data in index
database.updateEntry(mOldEntry) database.updateEntry(mOldEntry)

View File

@@ -0,0 +1,40 @@
/*
* 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.crypto
import com.kunzisoft.encrypt.CipherFactory
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
class AesEngine : CipherEngine() {
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
return CipherFactory.getAES(opmode, key, IV)
}
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
return EncryptionAlgorithm.AESRijndael
}
}

View File

@@ -17,19 +17,14 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.crypto.engine package com.kunzisoft.keepass.database.crypto
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.encrypt.CipherFactory
import com.kunzisoft.keepass.stream.bytes16ToUuid
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.InvalidAlgorithmParameterException import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class ChaCha20Engine : CipherEngine() { class ChaCha20Engine : CipherEngine() {
@@ -38,34 +33,11 @@ class ChaCha20Engine : CipherEngine() {
} }
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class) @Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher { override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
val cipher = Cipher.getInstance("Chacha7539", BouncyCastleProvider()) return CipherFactory.getChacha20(opmode, key, IV)
cipher.init(opmode, SecretKeySpec(key, "ChaCha7539"), IvParameterSpec(IV))
return cipher
} }
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm { override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
return EncryptionAlgorithm.ChaCha20 return EncryptionAlgorithm.ChaCha20
} }
companion object {
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0xD6.toByte(),
0x03.toByte(),
0x8A.toByte(),
0x2B.toByte(),
0x8B.toByte(),
0x6F.toByte(),
0x4C.toByte(),
0xB5.toByte(),
0xA5.toByte(),
0x24.toByte(),
0x33.toByte(),
0x9A.toByte(),
0x31.toByte(),
0xDB.toByte(),
0xB5.toByte(),
0x9A.toByte()))
}
} }

View File

@@ -17,9 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.crypto.engine package com.kunzisoft.keepass.database.crypto
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import java.security.InvalidAlgorithmParameterException import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException import java.security.InvalidKeyException
@@ -38,14 +36,12 @@ abstract class CipherEngine {
return 16 return 16
} }
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class) // Used only with padding workaround
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher var forcePaddingCompatibility = false
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class) @Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher { abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher
return getCipher(opmode, key, IV, false)
}
abstract fun getPwEncryptionAlgorithm(): EncryptionAlgorithm abstract fun getEncryptionAlgorithm(): EncryptionAlgorithm
} }

View File

@@ -17,11 +17,13 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.crypto package com.kunzisoft.keepass.database.crypto
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.encrypt.StreamCipher
enum class CrsAlgorithm constructor(val id: UnsignedInt) { enum class CrsAlgorithm(val id: UnsignedInt) {
Null(UnsignedInt(0)), Null(UnsignedInt(0)),
ArcFourVariant(UnsignedInt(1)), ArcFourVariant(UnsignedInt(1)),
@@ -30,6 +32,15 @@ enum class CrsAlgorithm constructor(val id: UnsignedInt) {
companion object { companion object {
@Throws(Exception::class)
fun getCipher(algorithm: CrsAlgorithm?, key: ByteArray): StreamCipher {
return when (algorithm) {
Salsa20 -> HashManager.getSalsa20(key)
ChaCha20 -> HashManager.getChaCha20(key)
else -> throw Exception("Invalid random cipher")
}
}
fun fromId(num: UnsignedInt): CrsAlgorithm? { fun fromId(num: UnsignedInt): CrsAlgorithm? {
for (e in values()) { for (e in values()) {
if (e.id == num) { if (e.id == num) {

View File

@@ -0,0 +1,133 @@
/*
* 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.crypto
import com.kunzisoft.keepass.utils.bytes16ToUuid
import java.security.NoSuchAlgorithmException
import java.util.*
enum class EncryptionAlgorithm {
AESRijndael,
Twofish,
ChaCha20;
val cipherEngine: CipherEngine
get() {
return when (this) {
AESRijndael -> AesEngine()
Twofish -> TwofishEngine()
ChaCha20 -> ChaCha20Engine()
}
}
val uuid: UUID
get() {
return when (this) {
AESRijndael -> AES_UUID
Twofish -> TWOFISH_UUID
ChaCha20 -> CHACHA20_UUID
}
}
override fun toString(): String {
return when (this) {
AESRijndael -> "Rijndael (AES)"
Twofish -> "Twofish"
ChaCha20 -> "ChaCha20"
}
}
companion object {
/**
* Generate appropriate cipher based on KeePass 2.x UUID's
*/
@Throws(NoSuchAlgorithmException::class)
fun getFrom(uuid: UUID): EncryptionAlgorithm {
return when (uuid) {
AES_UUID -> AESRijndael
TWOFISH_UUID -> Twofish
CHACHA20_UUID -> ChaCha20
else -> throw NoSuchAlgorithmException("UUID unrecognized.")
}
}
private val AES_UUID: UUID by lazy {
bytes16ToUuid(
byteArrayOf(0x31.toByte(),
0xC1.toByte(),
0xF2.toByte(),
0xE6.toByte(),
0xBF.toByte(),
0x71.toByte(),
0x43.toByte(),
0x50.toByte(),
0xBE.toByte(),
0x58.toByte(),
0x05.toByte(),
0x21.toByte(),
0x6A.toByte(),
0xFC.toByte(),
0x5A.toByte(),
0xFF.toByte()))
}
private val TWOFISH_UUID: UUID by lazy {
bytes16ToUuid(
byteArrayOf(0xAD.toByte(),
0x68.toByte(),
0xF2.toByte(),
0x9F.toByte(),
0x57.toByte(),
0x6F.toByte(),
0x4B.toByte(),
0xB9.toByte(),
0xA3.toByte(),
0x6A.toByte(),
0xD4.toByte(),
0x7A.toByte(),
0xF9.toByte(),
0x65.toByte(),
0x34.toByte(),
0x6C.toByte()))
}
private val CHACHA20_UUID: UUID by lazy {
bytes16ToUuid(
byteArrayOf(0xD6.toByte(),
0x03.toByte(),
0x8A.toByte(),
0x2B.toByte(),
0x8B.toByte(),
0x6F.toByte(),
0x4C.toByte(),
0xB5.toByte(),
0xA5.toByte(),
0x24.toByte(),
0x33.toByte(),
0x9A.toByte(),
0x31.toByte(),
0xDB.toByte(),
0xB5.toByte(),
0x9A.toByte()))
}
}
}

View File

@@ -17,35 +17,40 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.stream package com.kunzisoft.keepass.database.crypto
import java.io.IOException import java.io.IOException
import java.security.DigestOutputStream import java.security.InvalidKeyException
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
object HmacBlockStream { object HmacBlock {
fun getHmacKey64(key: ByteArray, blockIndex: Long): ByteArray {
fun getHmacSha256(blockKey: ByteArray): Mac {
val hmac: Mac
try {
hmac = Mac.getInstance("HmacSHA256")
val signingKey = SecretKeySpec(blockKey, "HmacSHA256")
hmac.init(signingKey)
} catch (e: NoSuchAlgorithmException) {
throw IOException("No HmacAlogirthm")
} catch (e: InvalidKeyException) {
throw IOException("Invalid Hmac Key")
}
return hmac
}
fun getHmacKey64(key: ByteArray, blockIndex: ByteArray): ByteArray {
val hash: MessageDigest val hash: MessageDigest
try { try {
hash = MessageDigest.getInstance("SHA-512") hash = MessageDigest.getInstance("SHA-512")
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e) throw RuntimeException(e)
} }
hash.update(blockIndex)
val nos = NullOutputStream() hash.update(key)
val dos = DigestOutputStream(nos, hash)
val leos = LittleEndianDataOutputStream(dos)
try {
leos.writeLong(blockIndex)
leos.write(key)
leos.close()
} catch (e: IOException) {
throw RuntimeException(e)
}
//assert(hashKey.length == 64);
return hash.digest() return hash.digest()
} }
} }

View File

@@ -0,0 +1,39 @@
/*
* 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.crypto
import com.kunzisoft.encrypt.CipherFactory
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
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, forcePaddingCompatibility)
}
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
return EncryptionAlgorithm.Twofish
}
}

View File

@@ -17,13 +17,10 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.utils package com.kunzisoft.keepass.database.crypto
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.stream.* import java.io.*
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.charset.Charset import java.nio.charset.Charset
import java.util.* import java.util.*
@@ -55,12 +52,12 @@ open class VariantDictionary {
return dict[name]?.value as UnsignedInt? return dict[name]?.value as UnsignedInt?
} }
fun setUInt64(name: String, value: Long) { fun setUInt64(name: String, value: UnsignedLong) {
putType(VdType.UInt64, name, value) putType(VdType.UInt64, name, value)
} }
fun getUInt64(name: String): Long? { fun getUInt64(name: String): UnsignedLong? {
return dict[name]?.value as Long? return dict[name]?.value as UnsignedLong?
} }
fun setBool(name: String, value: Boolean) { fun setBool(name: String, value: Boolean) {
@@ -115,22 +112,21 @@ open class VariantDictionary {
@Throws(IOException::class) @Throws(IOException::class)
fun deserialize(data: ByteArray): VariantDictionary { fun deserialize(data: ByteArray): VariantDictionary {
val inputStream = LittleEndianDataInputStream(ByteArrayInputStream(data)) val inputStream = ByteArrayInputStream(data)
return deserialize(inputStream) return deserialize(inputStream)
} }
@Throws(IOException::class) @Throws(IOException::class)
fun serialize(kdfParameters: KdfParameters): ByteArray { fun serialize(variantDictionary: VariantDictionary): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
val outputStream = LittleEndianDataOutputStream(byteArrayOutputStream) serialize(variantDictionary, byteArrayOutputStream)
serialize(kdfParameters, outputStream)
return byteArrayOutputStream.toByteArray() return byteArrayOutputStream.toByteArray()
} }
@Throws(IOException::class) @Throws(IOException::class)
fun deserialize(inputStream: LittleEndianDataInputStream): VariantDictionary { fun deserialize(inputStream: InputStream): VariantDictionary {
val dictionary = VariantDictionary() val dictionary = VariantDictionary()
val version = inputStream.readUShort() val version = inputStream.readBytes2ToUShort()
if (version and VdmCritical > VdVersion and VdmCritical) { if (version and VdmCritical > VdVersion and VdmCritical) {
throw IOException("Invalid format") throw IOException("Invalid format")
} }
@@ -143,14 +139,14 @@ open class VariantDictionary {
if (bType == VdType.None) { if (bType == VdType.None) {
break break
} }
val nameLen = inputStream.readUInt().toKotlinInt() val nameLen = inputStream.readBytes4ToUInt().toKotlinInt()
val nameBuf = inputStream.readBytes(nameLen) val nameBuf = inputStream.readBytesLength(nameLen)
if (nameLen != nameBuf.size) { if (nameLen != nameBuf.size) {
throw IOException("Invalid format") throw IOException("Invalid format")
} }
val name = String(nameBuf, UTF8Charset) val name = String(nameBuf, UTF8Charset)
val valueLen = inputStream.readUInt().toKotlinInt() val valueLen = inputStream.readBytes4ToUInt().toKotlinInt()
val valueBuf = inputStream.readBytes(valueLen) val valueBuf = inputStream.readBytesLength(valueLen)
if (valueLen != valueBuf.size) { if (valueLen != valueBuf.size) {
throw IOException("Invalid format") throw IOException("Invalid format")
} }
@@ -159,7 +155,7 @@ open class VariantDictionary {
dictionary.setUInt32(name, bytes4ToUInt(valueBuf)) dictionary.setUInt32(name, bytes4ToUInt(valueBuf))
} }
VdType.UInt64 -> if (valueLen == 8) { VdType.UInt64 -> if (valueLen == 8) {
dictionary.setUInt64(name, bytes64ToLong(valueBuf)) dictionary.setUInt64(name, bytes64ToULong(valueBuf))
} }
VdType.Bool -> if (valueLen == 1) { VdType.Bool -> if (valueLen == 1) {
dictionary.setBool(name, valueBuf[0] != 0.toByte()) dictionary.setBool(name, valueBuf[0] != 0.toByte())
@@ -181,48 +177,47 @@ open class VariantDictionary {
@Throws(IOException::class) @Throws(IOException::class)
fun serialize(variantDictionary: VariantDictionary, fun serialize(variantDictionary: VariantDictionary,
outputStream: LittleEndianDataOutputStream?) { outputStream: OutputStream?) {
if (outputStream == null) { if (outputStream == null) {
return return
} }
outputStream.writeUShort(VdVersion) outputStream.write2BytesUShort(VdVersion)
for ((name, vd) in variantDictionary.dict) { for ((name, vd) in variantDictionary.dict) {
val nameBuf = name.toByteArray(UTF8Charset) val nameBuf = name.toByteArray(UTF8Charset)
outputStream.write(vd.type.toInt()) outputStream.writeByte(vd.type)
outputStream.writeInt(nameBuf.size) outputStream.write4BytesUInt(UnsignedInt(nameBuf.size))
outputStream.write(nameBuf) outputStream.write(nameBuf)
var buf: ByteArray var buf: ByteArray
when (vd.type) { when (vd.type) {
VdType.UInt32 -> { VdType.UInt32 -> {
outputStream.writeInt(4) outputStream.write4BytesUInt(UnsignedInt(4))
outputStream.writeUInt((vd.value as UnsignedInt)) outputStream.write4BytesUInt(vd.value as UnsignedInt)
} }
VdType.UInt64 -> { VdType.UInt64 -> {
outputStream.writeInt(8) outputStream.write4BytesUInt(UnsignedInt(8))
outputStream.writeLong(vd.value as Long) outputStream.write8BytesLong(vd.value as UnsignedLong)
} }
VdType.Bool -> { VdType.Bool -> {
outputStream.writeInt(1) outputStream.write4BytesUInt(UnsignedInt(1))
val bool = if (vd.value as Boolean) 1.toByte() else 0.toByte() outputStream.writeBooleanByte(vd.value as Boolean)
outputStream.write(bool.toInt())
} }
VdType.Int32 -> { VdType.Int32 -> {
outputStream.writeInt(4) outputStream.write4BytesUInt(UnsignedInt(4))
outputStream.writeInt(vd.value as Int) outputStream.write4BytesUInt(UnsignedInt(vd.value as Int))
} }
VdType.Int64 -> { VdType.Int64 -> {
outputStream.writeInt(8) outputStream.write4BytesUInt(UnsignedInt(8))
outputStream.writeLong(vd.value as Long) outputStream.write8BytesLong(vd.value as Long)
} }
VdType.String -> { VdType.String -> {
val value = vd.value as String val value = vd.value as String
buf = value.toByteArray(UTF8Charset) buf = value.toByteArray(UTF8Charset)
outputStream.writeInt(buf.size) outputStream.write4BytesUInt(UnsignedInt(buf.size))
outputStream.write(buf) outputStream.write(buf)
} }
VdType.ByteArray -> { VdType.ByteArray -> {
buf = vd.value as ByteArray buf = vd.value as ByteArray
outputStream.writeInt(buf.size) outputStream.write4BytesUInt(UnsignedInt(buf.size))
outputStream.write(buf) outputStream.write(buf)
} }
else -> { else -> {

View File

@@ -17,13 +17,12 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.crypto.keyDerivation package com.kunzisoft.keepass.database.crypto.kdf
import android.content.res.Resources import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.utils.UnsignedLong
import com.kunzisoft.keepass.crypto.CryptoUtil import com.kunzisoft.encrypt.aes.AESTransformer
import com.kunzisoft.keepass.crypto.finalkey.AESKeyTransformerFactory import com.kunzisoft.keepass.utils.bytes16ToUuid
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.io.IOException import java.io.IOException
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*
@@ -38,32 +37,28 @@ class AesKdf : KdfEngine() {
get() { get() {
return KdfParameters(uuid!!).apply { return KdfParameters(uuid!!).apply {
setParamUUID() setParamUUID()
setUInt64(PARAM_ROUNDS, defaultKeyRounds) setUInt64(PARAM_ROUNDS, UnsignedLong(defaultKeyRounds))
} }
} }
override val defaultKeyRounds: Long = 500000L override val defaultKeyRounds = 500000L
override fun getName(resources: Resources): String {
return resources.getString(R.string.kdf_AES)
}
@Throws(IOException::class) @Throws(IOException::class)
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray { override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
var seed = kdfParameters.getByteArray(PARAM_SEED) var seed = kdfParameters.getByteArray(PARAM_SEED)
if (seed != null && seed.size != 32) { if (seed != null && seed.size != 32) {
seed = CryptoUtil.hashSha256(seed) seed = HashManager.hashSha256(seed)
} }
var currentMasterKey = masterKey var currentMasterKey = masterKey
if (currentMasterKey.size != 32) { if (currentMasterKey.size != 32) {
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey) currentMasterKey = HashManager.hashSha256(currentMasterKey)
} }
val rounds = kdfParameters.getUInt64(PARAM_ROUNDS) val rounds = kdfParameters.getUInt64(PARAM_ROUNDS)?.toKotlinLong()
return AESKeyTransformerFactory.transformMasterKey(seed, currentMasterKey, rounds) ?: ByteArray(0) return AESTransformer.transformKey(seed, currentMasterKey, rounds) ?: ByteArray(0)
} }
override fun randomize(kdfParameters: KdfParameters) { override fun randomize(kdfParameters: KdfParameters) {
@@ -76,11 +71,15 @@ class AesKdf : KdfEngine() {
} }
override fun getKeyRounds(kdfParameters: KdfParameters): Long { override fun getKeyRounds(kdfParameters: KdfParameters): Long {
return kdfParameters.getUInt64(PARAM_ROUNDS) ?: defaultKeyRounds return kdfParameters.getUInt64(PARAM_ROUNDS)?.toKotlinLong() ?: defaultKeyRounds
} }
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) { override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
kdfParameters.setUInt64(PARAM_ROUNDS, keyRounds) kdfParameters.setUInt64(PARAM_ROUNDS, UnsignedLong(keyRounds))
}
override fun toString(): String {
return "AES"
} }
companion object { companion object {

View File

@@ -17,13 +17,13 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.crypto.keyDerivation package com.kunzisoft.keepass.database.crypto.kdf
import android.content.res.Resources
import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.stream.bytes16ToUuid
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.UnsignedLong
import com.kunzisoft.encrypt.argon2.Argon2Transformer
import com.kunzisoft.encrypt.argon2.Argon2Type
import com.kunzisoft.keepass.utils.bytes16ToUuid
import java.io.IOException import java.io.IOException
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*
@@ -48,40 +48,30 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
} }
override val defaultKeyRounds: Long override val defaultKeyRounds: Long
get() = DEFAULT_ITERATIONS get() = DEFAULT_ITERATIONS.toKotlinLong()
override fun getName(resources: Resources): String {
return resources.getString(type.nameId)
}
@Throws(IOException::class) @Throws(IOException::class)
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray { override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
val salt = kdfParameters.getByteArray(PARAM_SALT) val salt = kdfParameters.getByteArray(PARAM_SALT) ?: ByteArray(0)
val parallelism = kdfParameters.getUInt32(PARAM_PARALLELISM)?.let { val parallelism = kdfParameters.getUInt32(PARAM_PARALLELISM)?.toKotlinLong() ?: DEFAULT_PARALLELISM.toKotlinLong()
UnsignedInt(it) val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.toKotlinLong()?.div(MEMORY_BLOCK_SIZE) ?: DEFAULT_MEMORY.toKotlinLong()
} val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.toKotlinLong() ?: DEFAULT_ITERATIONS.toKotlinLong()
val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.div(MEMORY_BLOCK_SIZE)?.let { val version = kdfParameters.getUInt32(PARAM_VERSION)?.toKotlinInt() ?: MAX_VERSION.toKotlinInt()
UnsignedInt.fromKotlinLong(it)
}
val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.let {
UnsignedInt.fromKotlinLong(it)
}
val version = kdfParameters.getUInt32(PARAM_VERSION)?.let {
UnsignedInt(it)
}
val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
return Argon2Native.transformKey( // Not used
type, // val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
// val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
val argonType = if (type == Type.ARGON2_ID) Argon2Type.ARGON2_ID else Argon2Type.ARGON2_D
return Argon2Transformer.transformKey(
argonType,
masterKey, masterKey,
salt, salt,
parallelism, parallelism,
memory, memory,
iterations, iterations,
secretKey,
assocData,
version) version)
} }
@@ -95,32 +85,32 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
} }
override fun getKeyRounds(kdfParameters: KdfParameters): Long { override fun getKeyRounds(kdfParameters: KdfParameters): Long {
return kdfParameters.getUInt64(PARAM_ITERATIONS) ?: defaultKeyRounds return kdfParameters.getUInt64(PARAM_ITERATIONS)?.toKotlinLong() ?: defaultKeyRounds
} }
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) { override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
kdfParameters.setUInt64(PARAM_ITERATIONS, keyRounds) kdfParameters.setUInt64(PARAM_ITERATIONS, UnsignedLong(keyRounds))
} }
override val minKeyRounds: Long override val minKeyRounds: Long
get() = MIN_ITERATIONS get() = MIN_ITERATIONS.toKotlinLong()
override val maxKeyRounds: Long override val maxKeyRounds: Long
get() = MAX_ITERATIONS get() = MAX_ITERATIONS.toKotlinLong()
override fun getMemoryUsage(kdfParameters: KdfParameters): Long { override fun getMemoryUsage(kdfParameters: KdfParameters): Long {
return kdfParameters.getUInt64(PARAM_MEMORY) ?: defaultMemoryUsage return kdfParameters.getUInt64(PARAM_MEMORY)?.toKotlinLong() ?: defaultMemoryUsage
} }
override fun setMemoryUsage(kdfParameters: KdfParameters, memory: Long) { override fun setMemoryUsage(kdfParameters: KdfParameters, memory: Long) {
kdfParameters.setUInt64(PARAM_MEMORY, memory) kdfParameters.setUInt64(PARAM_MEMORY, UnsignedLong(memory))
} }
override val defaultMemoryUsage: Long override val defaultMemoryUsage: Long
get() = DEFAULT_MEMORY get() = DEFAULT_MEMORY.toKotlinLong()
override val minMemoryUsage: Long override val minMemoryUsage: Long
get() = MIN_MEMORY get() = MIN_MEMORY.toKotlinLong()
override val maxMemoryUsage: Long override val maxMemoryUsage: Long
get() = MAX_MEMORY get() = MAX_MEMORY
@@ -135,16 +125,20 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
kdfParameters.setUInt32(PARAM_PARALLELISM, UnsignedInt.fromKotlinLong(parallelism)) kdfParameters.setUInt32(PARAM_PARALLELISM, UnsignedInt.fromKotlinLong(parallelism))
} }
override fun toString(): String {
return "$type"
}
override val defaultParallelism: Long override val defaultParallelism: Long
get() = DEFAULT_PARALLELISM.toKotlinLong() get() = DEFAULT_PARALLELISM.toKotlinLong()
override val minParallelism: Long override val minParallelism: Long
get() = MIN_PARALLELISM get() = MIN_PARALLELISM.toKotlinLong()
override val maxParallelism: Long override val maxParallelism: Long
get() = MAX_PARALLELISM get() = MAX_PARALLELISM.toKotlinLong()
enum class Type(val CIPHER_UUID: UUID, @StringRes val nameId: Int) { enum class Type(val CIPHER_UUID: UUID, private val typeName: String) {
ARGON2_D(bytes16ToUuid( ARGON2_D(bytes16ToUuid(
byteArrayOf(0xEF.toByte(), byteArrayOf(0xEF.toByte(),
0x63.toByte(), 0x63.toByte(),
@@ -161,7 +155,7 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
0x03.toByte(), 0x03.toByte(),
0xE3.toByte(), 0xE3.toByte(),
0x0A.toByte(), 0x0A.toByte(),
0x0C.toByte())), R.string.kdf_Argon2d), 0x0C.toByte())), "Argon2d"),
ARGON2_ID(bytes16ToUuid( ARGON2_ID(bytes16ToUuid(
byteArrayOf(0x9E.toByte(), byteArrayOf(0x9E.toByte(),
0x29.toByte(), 0x29.toByte(),
@@ -178,7 +172,11 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
0xC6.toByte(), 0xC6.toByte(),
0xF0.toByte(), 0xF0.toByte(),
0xA1.toByte(), 0xA1.toByte(),
0xE6.toByte())), R.string.kdf_Argon2id); 0xE6.toByte())), "Argon2id");
override fun toString(): String {
return typeName
}
} }
companion object { companion object {
@@ -194,21 +192,17 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
private val MIN_VERSION = UnsignedInt(0x10) private val MIN_VERSION = UnsignedInt(0x10)
private val MAX_VERSION = UnsignedInt(0x13) private val MAX_VERSION = UnsignedInt(0x13)
private const val MIN_SALT = 8 private val DEFAULT_ITERATIONS = UnsignedLong(2L)
private val MAX_SALT = UnsignedInt.MAX_VALUE.toKotlinLong() private val MIN_ITERATIONS = UnsignedLong(1L)
private val MAX_ITERATIONS = UnsignedLong(4294967295L)
private const val MIN_ITERATIONS: Long = 1L private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L))
private const val MAX_ITERATIONS = 4294967295L private val MIN_MEMORY = UnsignedLong(1024L * 8L)
private const val MIN_MEMORY = (1024 * 8).toLong()
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong() private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
private const val MEMORY_BLOCK_SIZE: Long = 1024L private const val MEMORY_BLOCK_SIZE: Long = 1024L
private const val MIN_PARALLELISM: Long = 1L
private const val MAX_PARALLELISM: Long = ((1 shl 24) - 1).toLong()
private const val DEFAULT_ITERATIONS: Long = 2L
private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
private val DEFAULT_PARALLELISM = UnsignedInt(2) private val DEFAULT_PARALLELISM = UnsignedInt(2)
private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L)
private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1))
} }
} }

View File

@@ -17,17 +17,15 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.crypto.keyDerivation package com.kunzisoft.keepass.database.crypto.kdf
import com.kunzisoft.keepass.utils.ObjectNameResource
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
import java.io.IOException import java.io.IOException
import java.io.Serializable import java.io.Serializable
import java.util.UUID import java.util.*
// TODO Parcelable // TODO Parcelable
abstract class KdfEngine : ObjectNameResource, Serializable { abstract class KdfEngine : Serializable {
var uuid: UUID? = null var uuid: UUID? = null

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.crypto.keyDerivation package com.kunzisoft.keepass.database.crypto.kdf
object KdfFactory { object KdfFactory {
var aesKdf = AesKdf() var aesKdf = AesKdf()

View File

@@ -17,11 +17,11 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.crypto.keyDerivation package com.kunzisoft.keepass.database.crypto.kdf
import com.kunzisoft.keepass.stream.bytes16ToUuid import com.kunzisoft.keepass.utils.bytes16ToUuid
import com.kunzisoft.keepass.stream.uuidTo16Bytes import com.kunzisoft.keepass.utils.uuidTo16Bytes
import com.kunzisoft.keepass.utils.VariantDictionary import com.kunzisoft.keepass.database.crypto.VariantDictionary
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*

View File

@@ -23,7 +23,9 @@ import android.database.MatrixCursor
import android.provider.BaseColumns import android.provider.BaseColumns
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.entry.EntryVersioned import com.kunzisoft.keepass.database.element.entry.EntryVersioned
import com.kunzisoft.keepass.database.element.icon.IconImageFactory import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import java.util.* import java.util.*
@@ -49,12 +51,16 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
abstract fun getPwNodeId(): NodeId<EntryId> abstract fun getPwNodeId(): NodeId<EntryId>
open fun populateEntry(pwEntry: PwEntryV, iconFactory: IconImageFactory) { open fun populateEntry(pwEntry: PwEntryV,
retrieveStandardIcon: (Int) -> IconImageStandard,
retrieveCustomIcon: (UUID) -> IconImageCustom) {
pwEntry.nodeId = getPwNodeId() pwEntry.nodeId = getPwNodeId()
pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE)) pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE))
val iconStandard = iconFactory.getIcon(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD))) val iconStandard = retrieveStandardIcon.invoke(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD)))
pwEntry.icon = iconStandard val iconCustom = retrieveCustomIcon.invoke(UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
pwEntry.icon = IconImage(iconStandard, iconCustom)
pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME)) pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME))
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD)) pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.database.cursor package com.kunzisoft.keepass.database.cursor
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
class EntryCursorKDB : EntryCursorUUID<EntryKDB>() { class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
@@ -30,9 +29,9 @@ class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
entry.id.mostSignificantBits, entry.id.mostSignificantBits,
entry.id.leastSignificantBits, entry.id.leastSignificantBits,
entry.title, entry.title,
entry.icon.iconId, entry.icon.standard.id,
DatabaseVersioned.UUID_ZERO.mostSignificantBits, entry.icon.custom.uuid.mostSignificantBits,
DatabaseVersioned.UUID_ZERO.leastSignificantBits, entry.icon.custom.uuid.leastSignificantBits,
entry.username, entry.username,
entry.password, entry.password,
entry.url, entry.url,

View File

@@ -20,9 +20,9 @@
package com.kunzisoft.keepass.database.cursor package com.kunzisoft.keepass.database.cursor
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageFactory import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import java.util.UUID import java.util.*
class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() { class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
@@ -34,9 +34,9 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
entry.id.mostSignificantBits, entry.id.mostSignificantBits,
entry.id.leastSignificantBits, entry.id.leastSignificantBits,
entry.title, entry.title,
entry.icon.iconId, entry.icon.standard.id,
entry.iconCustom.uuid.mostSignificantBits, entry.icon.custom.uuid.mostSignificantBits,
entry.iconCustom.uuid.leastSignificantBits, entry.icon.custom.uuid.leastSignificantBits,
entry.username, entry.username,
entry.password, entry.password,
entry.url, entry.url,
@@ -45,21 +45,17 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
entry.expires entry.expires
)) ))
for (element in entry.customFields.entries) { entry.doForEachDecodedCustomField { key, value ->
extraFieldCursor.addExtraField(entryId, element.key, element.value) extraFieldCursor.addExtraField(entryId, key, value)
} }
entryId++ entryId++
} }
override fun populateEntry(pwEntry: EntryKDBX, iconFactory: IconImageFactory) { override fun populateEntry(pwEntry: EntryKDBX,
super.populateEntry(pwEntry, iconFactory) retrieveStandardIcon: (Int) -> IconImageStandard,
retrieveCustomIcon: (UUID) -> IconImageCustom) {
// Retrieve custom icon super.populateEntry(pwEntry, retrieveStandardIcon, retrieveCustomIcon)
val iconCustom = iconFactory.getIcon(
UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
pwEntry.iconCustom = iconCustom
// Retrieve extra fields // Retrieve extra fields
if (extraFieldCursor.moveToFirst()) { if (extraFieldCursor.moveToFirst()) {

View File

@@ -42,7 +42,7 @@ class ExtraFieldCursor : MatrixCursor(arrayOf(
} }
fun populateExtraFieldInEntry(pwEntry: EntryKDBX) { fun populateExtraFieldInEntry(pwEntry: EntryKDBX) {
pwEntry.putExtraField(getString(getColumnIndex(COLUMN_LABEL)), pwEntry.putField(getString(getColumnIndex(COLUMN_LABEL)),
ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0, ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0,
getString(getColumnIndex(COLUMN_VALUE)))) getString(getColumnIndex(COLUMN_VALUE))))
} }

View File

@@ -19,24 +19,23 @@
*/ */
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryAttachment import com.kunzisoft.keepass.database.element.binary.BinaryByte
import kotlinx.coroutines.* import com.kunzisoft.keepass.database.element.binary.BinaryData
data class Attachment(var name: String, data class Attachment(var name: String,
var binaryAttachment: BinaryAttachment) : Parcelable { var binaryData: BinaryData) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readString() ?: "", parcel.readString() ?: "",
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment() parcel.readParcelable(BinaryData::class.java.classLoader) ?: BinaryByte()
) )
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name) parcel.writeString(name)
parcel.writeParcelable(binaryAttachment, flags) parcel.writeParcelable(binaryData, flags)
} }
override fun describeContents(): Int { override fun describeContents(): Int {
@@ -44,7 +43,7 @@ data class Attachment(var name: String,
} }
override fun toString(): String { override fun toString(): String {
return "$name at $binaryAttachment" return "$name at $binaryData"
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -68,28 +67,5 @@ data class Attachment(var name: String,
override fun newArray(size: Int): Array<Attachment?> { override fun newArray(size: Int): Array<Attachment?> {
return arrayOfNulls(size) return arrayOfNulls(size)
} }
fun loadBitmap(attachment: Attachment,
binaryCipherKey: Database.LoadedKey?,
actionOnFinish: (Bitmap?) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<Bitmap?> = async {
runCatching {
binaryCipherKey?.let { binaryKey ->
var bitmap: Bitmap?
attachment.binaryAttachment.getUnGzipInputDataStream(binaryKey).use { bitmapInputStream ->
bitmap = BitmapFactory.decodeStream(bitmapInputStream)
}
bitmap
}
}.getOrNull()
}
withContext(Dispatchers.Main) {
actionOnFinish(asyncResult.await())
}
}
}
}
} }
} }

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

@@ -20,20 +20,30 @@
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.database.* import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.icon.IconImageFactory import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconsManager
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
@@ -42,16 +52,12 @@ import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.stream.readBytes4ToUInt
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import java.io.* import java.io.*
import java.security.Key
import java.security.SecureRandom
import java.util.* import java.util.*
import javax.crypto.KeyGenerator
import javax.crypto.spec.IvParameterSpec
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -68,7 +74,10 @@ class Database {
var isReadOnly = false var isReadOnly = false
val drawFactory = IconDrawableFactory() val iconDrawableFactory = IconDrawableFactory(
{ binaryCache },
{ iconId -> iconsManager.getBinaryForCustomIcon(iconId) }
)
var loaded = false var loaded = false
set(value) { set(value) {
@@ -76,6 +85,11 @@ class Database {
loadTimestamp = if (field) System.currentTimeMillis() else null loadTimestamp = if (field) System.currentTimeMillis() else null
} }
/**
* To reload the main activity
*/
var wasReloaded = false
var loadTimestamp: Long? = null var loadTimestamp: Long? = null
private set private set
@@ -83,20 +97,52 @@ class Database {
* Cipher key regenerated when the database is loaded and closed * Cipher key regenerated when the database is loaded and closed
* Can be used to temporarily store database elements * Can be used to temporarily store database elements
*/ */
var loadedCipherKey: LoadedKey? var binaryCache: BinaryCache
private set(value) { private set(value) {
mDatabaseKDB?.loadedCipherKey = value mDatabaseKDB?.binaryCache = value
mDatabaseKDBX?.loadedCipherKey = value mDatabaseKDBX?.binaryCache = value
} }
get() { get() {
return mDatabaseKDB?.loadedCipherKey ?: mDatabaseKDBX?.loadedCipherKey return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache()
} }
val iconFactory: IconImageFactory private val iconsManager: IconsManager
get() { get() {
return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory() return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
} }
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
return iconsManager.doForEachStandardIcon(action)
}
fun getStandardIcon(iconId: Int): IconImageStandard {
return iconsManager.getIcon(iconId)
}
val allowCustomIcons: Boolean
get() = mDatabaseKDBX != null
fun doForEachCustomIcons(action: (IconImageCustom, BinaryData) -> Unit) {
return iconsManager.doForEachCustomIcon(action)
}
fun getCustomIcon(iconId: UUID): IconImageCustom {
return iconsManager.getIcon(iconId)
}
fun buildNewCustomIcon(result: (IconImageCustom?, BinaryData?) -> Unit) {
mDatabaseKDBX?.buildNewCustomIcon(null, result)
}
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
return mDatabaseKDBX?.isCustomIconBinaryDuplicate(binaryData) ?: false
}
fun removeCustomIcon(customIcon: IconImageCustom) {
iconDrawableFactory.clearFromCache(customIcon)
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
}
val allowName: Boolean val allowName: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
@@ -177,7 +223,7 @@ class Database {
// Default compression not necessary if stored in header // Default compression not necessary if stored in header
mDatabaseKDBX?.let { mDatabaseKDBX?.let {
return it.compressionAlgorithm == CompressionAlgorithm.GZip return it.compressionAlgorithm == CompressionAlgorithm.GZip
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong() && it.kdbxVersion.isBefore(FILE_VERSION_40)
} }
return false return false
} }
@@ -190,12 +236,9 @@ class Database {
val allowNoMasterKey: Boolean val allowNoMasterKey: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
val allowEncryptionAlgorithmModification: Boolean fun getEncryptionAlgorithmName(): String {
get() = availableEncryptionAlgorithms.size > 1 return mDatabaseKDB?.encryptionAlgorithm?.toString()
?: mDatabaseKDBX?.encryptionAlgorithm?.toString()
fun getEncryptionAlgorithmName(resources: Resources): String {
return mDatabaseKDB?.encryptionAlgorithm?.getName(resources)
?: mDatabaseKDBX?.encryptionAlgorithm?.getName(resources)
?: "" ?: ""
} }
@@ -208,7 +251,7 @@ class Database {
algorithm?.let { algorithm?.let {
mDatabaseKDBX?.encryptionAlgorithm = algorithm mDatabaseKDBX?.encryptionAlgorithm = algorithm
mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine) mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine)
mDatabaseKDBX?.dataCipher = algorithm.dataCipher mDatabaseKDBX?.cipherUuid = algorithm.uuid
} }
} }
@@ -230,8 +273,8 @@ class Database {
} }
} }
fun getKeyDerivationName(resources: Resources): String { fun getKeyDerivationName(): String {
return kdfEngine?.getName(resources) ?: "" return kdfEngine?.toString() ?: ""
} }
var numberKeyEncryptionRounds: Long var numberKeyEncryptionRounds: Long
@@ -317,15 +360,10 @@ class Database {
return null return null
} }
fun ensureRecycleBinExists(resources: Resources) { val groupNamesNotAllowed: List<String>
mDatabaseKDB?.ensureBackupExists() get() {
mDatabaseKDBX?.ensureRecycleBinExists(resources) return mDatabaseKDB?.groupNamesNotAllowed ?: ArrayList()
} }
fun removeRecycleBin() {
// Don't allow remove backup in KDB
mDatabaseKDBX?.removeRecycleBin()
}
private fun setDatabaseKDB(databaseKDB: DatabaseKDB) { private fun setDatabaseKDB(databaseKDB: DatabaseKDB) {
this.mDatabaseKDB = databaseKDB this.mDatabaseKDB = databaseKDB
@@ -339,25 +377,12 @@ class Database {
fun createData(databaseUri: Uri, databaseName: String, rootName: String) { fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
val newDatabase = DatabaseKDBX(databaseName, rootName) val newDatabase = DatabaseKDBX(databaseName, rootName)
newDatabase.loadedCipherKey = LoadedKey.generateNewCipherKey()
setDatabaseKDBX(newDatabase) setDatabaseKDBX(newDatabase)
this.fileUri = databaseUri this.fileUri = databaseUri
// Set Database state // Set Database state
this.loaded = true this.loaded = true
} }
class LoadedKey(val key: Key, val iv: ByteArray): Serializable {
companion object {
const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding"
fun generateNewCipherKey(): LoadedKey {
val iv = ByteArray(8)
SecureRandom().nextBytes(iv)
return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), iv)
}
}
}
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri, private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
openDatabaseKDB: (InputStream) -> DatabaseKDB, openDatabaseKDB: (InputStream) -> DatabaseKDB,
@@ -411,6 +436,7 @@ class Database {
readOnly: Boolean, readOnly: Boolean,
contentResolver: ContentResolver, contentResolver: ContentResolver,
cacheDirectory: File, cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
tempCipherKey: LoadedKey, tempCipherKey: LoadedKey,
fixDuplicateUUID: Boolean, fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) { progressTaskUpdater: ProgressTaskUpdater?) {
@@ -432,7 +458,7 @@ class Database {
// Read database stream for the first time // Read database stream for the first time
readDatabaseStream(contentResolver, uri, readDatabaseStream(contentResolver, uri,
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDB(cacheDirectory) DatabaseInputKDB(cacheDirectory, isRAMSufficient)
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
mainCredential.masterPassword, mainCredential.masterPassword,
keyFileInputStream, keyFileInputStream,
@@ -441,7 +467,7 @@ class Database {
fixDuplicateUUID) fixDuplicateUUID)
}, },
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDBX(cacheDirectory) DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
mainCredential.masterPassword, mainCredential.masterPassword,
keyFileInputStream, keyFileInputStream,
@@ -465,6 +491,7 @@ class Database {
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
fun reloadData(contentResolver: ContentResolver, fun reloadData(contentResolver: ContentResolver,
cacheDirectory: File, cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
tempCipherKey: LoadedKey, tempCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?) { progressTaskUpdater: ProgressTaskUpdater?) {
@@ -473,14 +500,14 @@ class Database {
fileUri?.let { oldDatabaseUri -> fileUri?.let { oldDatabaseUri ->
readDatabaseStream(contentResolver, oldDatabaseUri, readDatabaseStream(contentResolver, oldDatabaseUri,
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDB(cacheDirectory) DatabaseInputKDB(cacheDirectory, isRAMSufficient)
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
masterKey, masterKey,
tempCipherKey, tempCipherKey,
progressTaskUpdater) progressTaskUpdater)
}, },
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDBX(cacheDirectory) DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
masterKey, masterKey,
tempCipherKey, tempCipherKey,
@@ -511,30 +538,32 @@ class Database {
omitBackup: Boolean, omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper?.createVirtualGroupWithSearchResult(this,
searchQuery, SearchParameters(), omitBackup, max) SearchParameters().apply {
this.searchQuery = searchQuery
}, omitBackup, max)
} }
fun createVirtualGroupFromSearchInfo(searchInfoString: String, fun createVirtualGroupFromSearchInfo(searchInfoString: String,
omitBackup: Boolean, omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper?.createVirtualGroupWithSearchResult(this,
searchInfoString, SearchParameters().apply { SearchParameters().apply {
searchInTitles = true searchQuery = searchInfoString
searchInUserNames = false searchInTitles = true
searchInPasswords = false searchInUserNames = false
searchInUrls = true searchInPasswords = false
searchInNotes = true searchInUrls = true
searchInOTP = false searchInNotes = true
searchInOther = true searchInOTP = false
searchInUUIDs = false searchInOther = true
searchInTags = false searchInUUIDs = false
ignoreCase = true searchInTags = false
}, omitBackup, max) }, omitBackup, max)
} }
val binaryPool: BinaryPool val attachmentPool: AttachmentPool
get() { get() {
return mDatabaseKDBX?.binaryPool ?: BinaryPool() return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool(binaryCache)
} }
val allowMultipleAttachments: Boolean val allowMultipleAttachments: Boolean
@@ -546,17 +575,16 @@ class Database {
return false return false
} }
fun buildNewBinary(cacheDirectory: File, fun buildNewBinaryAttachment(compressed: Boolean = false,
compressed: Boolean = false, protected: Boolean = false): BinaryData? {
protected: Boolean = false): BinaryAttachment? { return mDatabaseKDB?.buildNewAttachment()
return mDatabaseKDB?.buildNewBinary(cacheDirectory) ?: mDatabaseKDBX?.buildNewAttachment( false, compressed, protected)
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, compressed, protected)
} }
fun removeAttachmentIfNotUsed(attachment: Attachment) { fun removeAttachmentIfNotUsed(attachment: Attachment) {
// No need in KDB database because unique attachment by entry // No need in KDB database because unique attachment by entry
// Don't clear to fix upload multiple times // Don't clear to fix upload multiple times
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false) mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryData, false)
} }
fun removeUnlinkedAttachments() { fun removeUnlinkedAttachments() {
@@ -625,7 +653,9 @@ class Database {
} }
fun clear(filesDirectory: File? = null) { fun clear(filesDirectory: File? = null) {
drawFactory.clearCache() binaryCache.clear()
iconsManager.clearCache()
iconDrawableFactory.clearCache()
// Delete the cache of the database if present // Delete the cache of the database if present
mDatabaseKDB?.clearCache() mDatabaseKDB?.clearCache()
mDatabaseKDBX?.clearCache() mDatabaseKDBX?.clearCache()
@@ -639,8 +669,8 @@ class Database {
} }
} }
fun clearAndClose(filesDirectory: File? = null) { fun clearAndClose(context: Context? = null) {
clear(filesDirectory) clear(context?.let { UriUtil.getBinaryDir(context) })
this.mDatabaseKDB = null this.mDatabaseKDB = null
this.mDatabaseKDBX = null this.mDatabaseKDBX = null
this.fileUri = null this.fileUri = null
@@ -758,11 +788,11 @@ class Database {
} }
fun addGroupTo(group: Group, parent: Group) { fun addGroupTo(group: Group, parent: Group) {
group.groupKDB?.let { entryKDB -> group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.addGroupTo(entryKDB, parent.groupKDB) mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
} }
group.groupKDBX?.let { entryKDBX -> group.groupKDBX?.let { groupKDBX ->
mDatabaseKDBX?.addGroupTo(entryKDBX, parent.groupKDBX) mDatabaseKDBX?.addGroupTo(groupKDBX, parent.groupKDBX)
} }
group.afterAssignNewParent() group.afterAssignNewParent()
} }
@@ -777,11 +807,11 @@ class Database {
} }
fun removeGroupFrom(group: Group, parent: Group) { fun removeGroupFrom(group: Group, parent: Group) {
group.groupKDB?.let { entryKDB -> group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.removeGroupFrom(entryKDB, parent.groupKDB) mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
} }
group.groupKDBX?.let { entryKDBX -> group.groupKDBX?.let { groupKDBX ->
mDatabaseKDBX?.removeGroupFrom(entryKDBX, parent.groupKDBX) mDatabaseKDBX?.removeGroupFrom(groupKDBX, parent.groupKDBX)
} }
group.afterAssignNewParent() group.afterAssignNewParent()
} }
@@ -791,7 +821,7 @@ class Database {
* @param entryToCopy * @param entryToCopy
* @param newParent * @param newParent
*/ */
fun copyEntryTo(entryToCopy: Entry, newParent: Group): Entry? { fun copyEntryTo(entryToCopy: Entry, newParent: Group): Entry {
val entryCopied = Entry(entryToCopy, false) val entryCopied = Entry(entryToCopy, false)
entryCopied.nodeId = mDatabaseKDB?.newEntryId() ?: mDatabaseKDBX?.newEntryId() ?: NodeIdUUID() entryCopied.nodeId = mDatabaseKDB?.newEntryId() ?: mDatabaseKDBX?.newEntryId() ?: NodeIdUUID()
entryCopied.parent = newParent entryCopied.parent = newParent
@@ -856,6 +886,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 { fun canRecycle(entry: Entry): Boolean {
var canRecycle: Boolean? = null var canRecycle: Boolean? = null
entry.entryKDB?.let { entry.entryKDB?.let {
@@ -948,7 +988,7 @@ class Database {
rootGroup?.doForEachChildAndForIt( rootGroup?.doForEachChildAndForIt(
object : NodeHandler<Entry>() { object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean { override fun operate(node: Entry): Boolean {
removeOldestEntryHistory(node, binaryPool) removeOldestEntryHistory(node, attachmentPool)
return true return true
} }
}, },
@@ -963,7 +1003,7 @@ class Database {
/** /**
* Remove oldest history if more than max items or max memory * Remove oldest history if more than max items or max memory
*/ */
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) { fun removeOldestEntryHistory(entry: Entry, attachmentPool: AttachmentPool) {
mDatabaseKDBX?.let { mDatabaseKDBX?.let {
val maxItems = historyMaxItems val maxItems = historyMaxItems
if (maxItems >= 0) { if (maxItems >= 0) {
@@ -977,7 +1017,7 @@ class Database {
while (true) { while (true) {
var historySize: Long = 0 var historySize: Long = 0
for (entryHistory in entry.getHistory()) { for (entryHistory in entry.getHistory()) {
historySize += entryHistory.getSize(binaryPool) historySize += entryHistory.getSize(attachmentPool)
} }
if (historySize > maxSize) { if (historySize > maxSize) {
removeOldestEntryHistory(entry) removeOldestEntryHistory(entry)
@@ -991,7 +1031,7 @@ class Database {
private fun removeOldestEntryHistory(entry: Entry) { private fun removeOldestEntryHistory(entry: Entry) {
entry.removeOldestEntryFromHistory()?.let { entry.removeOldestEntryFromHistory()?.let {
it.getAttachments(binaryPool, false).forEach { attachmentToRemove -> it.getAttachments(attachmentPool, false).forEach { attachmentToRemove ->
removeAttachmentIfNotUsed(attachmentToRemove) removeAttachmentIfNotUsed(attachmentToRemove)
} }
} }
@@ -999,7 +1039,7 @@ class Database {
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) { fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
entry.removeEntryFromHistory(entryHistoryPosition)?.let { entry.removeEntryFromHistory(entryHistoryPosition)?.let {
it.getAttachments(binaryPool, false).forEach { attachmentToRemove -> it.getAttachments(attachmentPool, false).forEach { attachmentToRemove ->
removeAttachmentIfNotUsed(attachmentToRemove) removeAttachmentIfNotUsed(attachmentToRemove)
} }
} }

View File

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

View File

@@ -19,30 +19,37 @@
*/ */
package com.kunzisoft.keepass.database.element 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 com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import java.util.Date import java.util.*
import java.util.UUID
class DeletedObject { class DeletedObject : Parcelable {
var uuid: UUID = DatabaseVersioned.UUID_ZERO 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) { if (mDeletionTime == null) {
mDeletionTime = Date(System.currentTimeMillis()) mDeletionTime = DateInstant(System.currentTimeMillis())
} }
return mDeletionTime!! return mDeletionTime!!
} }
fun setDeletionTime(deletionTime: Date) { fun setDeletionTime(deletionTime: DateInstant) {
this.mDeletionTime = deletionTime
}
constructor()
constructor(uuid: UUID, deletionTime: Date = Date()) {
this.uuid = uuid
this.mDeletionTime = deletionTime this.mDeletionTime = deletionTime
} }
@@ -59,4 +66,23 @@ class DeletedObject {
override fun hashCode(): Int { override fun hashCode(): Int {
return uuid.hashCode() 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

@@ -21,14 +21,13 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX 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.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
@@ -109,13 +108,27 @@ class Entry : Node, EntryVersionedInterface<Group> {
override var icon: IconImage override var icon: IconImage
get() { get() {
return entryKDB?.icon ?: entryKDBX?.icon ?: IconImageStandard() return entryKDB?.icon ?: entryKDBX?.icon ?: IconImage()
} }
set(value) { set(value) {
entryKDB?.icon = value entryKDB?.icon = value
entryKDBX?.icon = value 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 override val type: Type
get() = Type.ENTRY get() = Type.ENTRY
@@ -257,31 +270,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
} }
} }
/*
------------
KDB Methods
------------
*/
/**
* If it's a node with only meta information like Meta-info SYSTEM Database Color
* @return false by default, true if it's a meta stream
*/
val isMetaStream: Boolean
get() = entryKDB?.isMetaStream ?: false
/* /*
------------ ------------
KDBX Methods KDBX Methods
------------ ------------
*/ */
var iconCustom: IconImageCustom
get() = entryKDBX?.iconCustom ?: IconImageCustom.UNKNOWN_ICON
set(value) {
entryKDBX?.iconCustom = value
}
/** /**
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not) * Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
* @return Map of label/value * @return Map of label/value
@@ -289,8 +283,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun getExtraFields(): List<Field> { fun getExtraFields(): List<Field> {
val extraFields = ArrayList<Field>() val extraFields = ArrayList<Field>()
entryKDBX?.let { entryKDBX?.let {
for (field in it.customFields) { it.doForEachDecodedCustomField { key, value ->
extraFields.add(Field(field.key, field.value)) extraFields.add(Field(key, value))
} }
} }
return extraFields return extraFields
@@ -300,7 +294,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
* Update or add an extra field to the list (standard or custom) * Update or add an extra field to the list (standard or custom)
*/ */
fun putExtraField(field: Field) { fun putExtraField(field: Field) {
entryKDBX?.putExtraField(field.name, field.protectedValue) entryKDBX?.putField(field.name, field.protectedValue)
} }
private fun addExtraFields(fields: List<Field>) { private fun addExtraFields(fields: List<Field>) {
@@ -316,7 +310,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun getOtpElement(): OtpElement? { fun getOtpElement(): OtpElement? {
entryKDBX?.let { entryKDBX?.let {
return OtpEntryFields.parseFields { key -> return OtpEntryFields.parseFields { key ->
it.customFields[key]?.toString() it.getField(key)?.toString()
} }
} }
return null return null
@@ -330,12 +324,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.stopToManageFieldReferences() entryKDBX?.stopToManageFieldReferences()
} }
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> { fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
val attachments = ArrayList<Attachment>() val attachments = ArrayList<Attachment>()
entryKDB?.getAttachment()?.let { entryKDB?.getAttachment(attachmentPool)?.let {
attachments.add(it) attachments.add(it)
} }
entryKDBX?.getAttachments(binaryPool, inHistory)?.let { entryKDBX?.getAttachments(attachmentPool, inHistory)?.let {
attachments.addAll(it) attachments.addAll(it)
} }
return attachments return attachments
@@ -356,9 +350,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.removeAttachments() entryKDBX?.removeAttachments()
} }
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) { private fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
entryKDB?.putAttachment(attachment) entryKDB?.putAttachment(attachment, attachmentPool)
entryKDBX?.putAttachment(attachment, binaryPool) entryKDBX?.putAttachment(attachment, attachmentPool)
} }
fun getHistory(): ArrayList<Entry> { fun getHistory(): ArrayList<Entry> {
@@ -390,12 +384,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
return null return null
} }
fun getSize(binaryPool: BinaryPool): Long { fun getSize(attachmentPool: AttachmentPool): Long {
return entryKDBX?.getSize(binaryPool) ?: 0L return entryKDBX?.getSize(attachmentPool) ?: 0L
}
fun containsCustomData(): Boolean {
return entryKDBX?.containsCustomData() ?: false
} }
/* /*
@@ -433,7 +423,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
// Replace parameter fields by generated OTP fields // Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields) entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
} }
database?.binaryPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
entryInfo.attachments = getAttachments(binaryPool) entryInfo.attachments = getAttachments(binaryPool)
} }
@@ -460,7 +450,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
url = newEntryInfo.url url = newEntryInfo.url
notes = newEntryInfo.notes notes = newEntryInfo.notes
addExtraFields(newEntryInfo.customFields) addExtraFields(newEntryInfo.customFields)
database?.binaryPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
newEntryInfo.attachments.forEach { attachment -> newEntryInfo.attachments.forEach { attachment ->
putAttachment(attachment, binaryPool) putAttachment(attachment, binaryPool)
} }
@@ -487,16 +477,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
return result return result
} }
companion object {
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)
}
const val PMS_TAN_ENTRY = "<TAN>" const val PMS_TAN_ENTRY = "<TAN>"
/** /**
@@ -505,5 +486,16 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun newExtraFieldNameAllowed(field: Field): Boolean { fun newExtraFieldNameAllowed(field: Field): Boolean {
return EntryKDBX.newCustomNameAllowed(field.name) 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,11 +22,11 @@ package com.kunzisoft.keepass.database.element
import android.content.Context import android.content.Context
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable 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.GroupKDB
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.* import com.kunzisoft.keepass.database.element.node.*
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
@@ -41,6 +41,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
var groupKDBX: GroupKDBX? = null var groupKDBX: GroupKDBX? = null
private set private set
// Virtual group is used to defined a detached database group
var isVirtual = false
fun updateWith(group: Group) { fun updateWith(group: Group) {
group.groupKDB?.let { group.groupKDB?.let {
this.groupKDB?.updateWith(it) this.groupKDB?.updateWith(it)
@@ -78,6 +81,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
constructor(parcel: Parcel) { constructor(parcel: Parcel) {
groupKDB = parcel.readParcelable(GroupKDB::class.java.classLoader) groupKDB = parcel.readParcelable(GroupKDB::class.java.classLoader)
groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader) groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader)
isVirtual = parcel.readByte().toInt() != 0
} }
enum class ChildFilter { enum class ChildFilter {
@@ -111,6 +115,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeParcelable(groupKDB, flags) dest.writeParcelable(groupKDB, flags)
dest.writeParcelable(groupKDBX, flags) dest.writeParcelable(groupKDBX, flags)
dest.writeByte((if (isVirtual) 1 else 0).toByte())
} }
override val nodeId: NodeId<*>? override val nodeId: NodeId<*>?
@@ -124,12 +129,26 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
} }
override var icon: IconImage override var icon: IconImage
get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImageStandard() get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImage()
set(value) { set(value) {
groupKDB?.icon = value groupKDB?.icon = value
groupKDBX?.icon = value 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 override val type: Type
get() = Type.GROUP get() = Type.GROUP
@@ -344,9 +363,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.removeChildren() groupKDBX?.removeChildren()
} }
override fun allowAddEntryIfIsRoot(): Boolean { val allowAddEntryIfIsRoot: Boolean
return groupKDB?.allowAddEntryIfIsRoot() ?: groupKDBX?.allowAddEntryIfIsRoot() ?: false get() = groupKDBX != null
}
val allowAddNoteInGroup: Boolean
get() = groupKDBX != null
/* /*
------------ ------------
@@ -362,14 +383,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDB?.nodeId = id groupKDB?.nodeId = id
} }
fun getLevel(): Int {
return groupKDB?.level ?: -1
}
fun setLevel(level: Int) {
groupKDB?.level = level
}
/* /*
------------ ------------
KDBX Methods KDBX Methods
@@ -396,10 +409,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.isExpanded = expanded groupKDBX?.isExpanded = expanded
} }
fun containsCustomData(): Boolean {
return groupKDBX?.containsCustomData() ?: false
}
/* /*
------------ ------------
Converter 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

@@ -0,0 +1,44 @@
/*
* 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.database.element.binary
class AttachmentPool(binaryCache: BinaryCache) : BinaryPool<Int>(binaryCache) {
/**
* Utility method to find an unused key in the pool
*/
override fun findUnusedKey(): Int {
var unusedKey = 0
while (pool[unusedKey] != null)
unusedKey++
return unusedKey
}
/**
* To register a binary with a ref corresponding to an ordered index
*/
fun getBinaryIndexFromKey(key: Int): Int? {
val index = orderedBinariesWithoutDuplication().indexOfFirst { it.keys.contains(key) }
return if (index < 0)
null
else
index
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2018 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.element.binary
import android.os.Parcel
import android.os.Parcelable
import android.util.Base64
import android.util.Base64InputStream
import android.util.Base64OutputStream
import com.kunzisoft.keepass.utils.readAllBytes
import com.kunzisoft.keepass.database.element.binary.BinaryCache.Companion.UNKNOWN
import java.io.*
import java.util.zip.GZIPOutputStream
class BinaryByte : BinaryData {
private var mDataByteId: String
private fun getByteArray(binaryCache: BinaryCache): ByteArray {
val keyData = binaryCache.getByteArray(mDataByteId)
mDataByteId = keyData.key
return keyData.data
}
constructor() : super() {
mDataByteId = UNKNOWN
}
constructor(id: String,
compressed: Boolean = false,
protected: Boolean = false) : super(compressed, protected) {
mDataByteId = id
}
constructor(parcel: Parcel) : super(parcel) {
mDataByteId = parcel.readString() ?: UNKNOWN
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeString(mDataByteId)
}
@Throws(IOException::class)
override fun getInputDataStream(binaryCache: BinaryCache): InputStream {
return Base64InputStream(ByteArrayInputStream(getByteArray(binaryCache)), Base64.NO_WRAP)
}
@Throws(IOException::class)
override fun getOutputDataStream(binaryCache: BinaryCache): OutputStream {
return BinaryCountingOutputStream(Base64OutputStream(ByteOutputStream(binaryCache), Base64.NO_WRAP))
}
@Throws(IOException::class)
override fun compress(binaryCache: BinaryCache) {
if (!isCompressed) {
GZIPOutputStream(getOutputDataStream(binaryCache)).use { outputStream ->
getInputDataStream(binaryCache).use { inputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
isCompressed = true
}
}
}
@Throws(IOException::class)
override fun decompress(binaryCache: BinaryCache) {
if (isCompressed) {
getUnGzipInputDataStream(binaryCache).use { inputStream ->
getOutputDataStream(binaryCache).use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
isCompressed = false
}
}
}
@Throws(IOException::class)
override fun clear(binaryCache: BinaryCache) {
binaryCache.removeByteArray(mDataByteId)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is BinaryByte) return false
if (!super.equals(other)) return false
if (mDataByteId != other.mDataByteId) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + mDataByteId.hashCode()
return result
}
/**
* Custom OutputStream to calculate the size and hash of binary file
*/
private inner class ByteOutputStream(private val binaryCache: BinaryCache) : ByteArrayOutputStream() {
override fun close() {
binaryCache.setByteArray(mDataByteId, this.toByteArray())
super.close()
}
}
companion object {
private val TAG = BinaryByte::class.java.name
@JvmField
val CREATOR: Parcelable.Creator<BinaryByte> = object : Parcelable.Creator<BinaryByte> {
override fun createFromParcel(parcel: Parcel): BinaryByte {
return BinaryByte(parcel)
}
override fun newArray(size: Int): Array<BinaryByte?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -0,0 +1,82 @@
package com.kunzisoft.keepass.database.element.binary
import java.io.File
import java.util.*
class BinaryCache {
/**
* Cipher key generated when the database is loaded, and destroyed when the database is closed
* Can be used to temporarily store database elements
*/
var loadedCipherKey: LoadedKey = LoadedKey.generateNewCipherKey()
var cacheDirectory: File? = null
private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0))
fun getBinaryData(binaryId: String,
smallSize: Boolean = false,
compression: Boolean = false,
protection: Boolean = false): BinaryData {
val cacheDir = cacheDirectory
return if (smallSize || cacheDir == null) {
BinaryByte(binaryId, compression, protection)
} else {
val fileInCache = File(cacheDir, binaryId)
BinaryFile(fileInCache, compression, protection)
}
}
// Similar to file storage but much faster TODO SparseArray
private val byteArrayList = HashMap<String, ByteArray>()
fun getByteArray(key: String): KeyByteArray {
if (key == UNKNOWN) {
return voidBinary
}
if (!byteArrayList.containsKey(key)) {
val newItem = KeyByteArray(key, ByteArray(0))
byteArrayList[newItem.key] = newItem.data
return newItem
}
return KeyByteArray(key, byteArrayList[key]!!)
}
fun setByteArray(key: String, data: ByteArray): KeyByteArray {
if (key == UNKNOWN) {
return voidBinary
}
byteArrayList[key] = data
return KeyByteArray(key, data)
}
fun removeByteArray(key: String?) {
key?.let {
byteArrayList.remove(it)
}
}
fun clear() {
byteArrayList.clear()
}
companion object {
const val UNKNOWN = "UNKNOWN"
}
data class KeyByteArray(val key: String, val data: ByteArray) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is KeyByteArray) return false
if (key != other.key) return false
return true
}
override fun hashCode(): Int {
return key.hashCode()
}
}
}

View File

@@ -0,0 +1,192 @@
/*
* Copyright 2018 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.element.binary
import android.app.ActivityManager
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import org.apache.commons.io.output.CountingOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
abstract class BinaryData : Parcelable {
var isCompressed: Boolean = false
protected set
var isProtected: Boolean = false
protected set
var isCorrupted: Boolean = false
private var mLength: Long = 0
private var mBinaryHash = 0
protected constructor(compressed: Boolean = false, protected: Boolean = false) {
this.isCompressed = compressed
this.isProtected = protected
this.mLength = 0
this.mBinaryHash = 0
}
protected constructor(parcel: Parcel) {
isCompressed = parcel.readByte().toInt() != 0
isProtected = parcel.readByte().toInt() != 0
isCorrupted = parcel.readByte().toInt() != 0
mLength = parcel.readLong()
mBinaryHash = parcel.readInt()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeByte((if (isCompressed) 1 else 0).toByte())
dest.writeByte((if (isProtected) 1 else 0).toByte())
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
dest.writeLong(mLength)
dest.writeInt(mBinaryHash)
}
@Throws(IOException::class)
abstract fun getInputDataStream(binaryCache: BinaryCache): InputStream
@Throws(IOException::class)
abstract fun getOutputDataStream(binaryCache: BinaryCache): OutputStream
@Throws(IOException::class)
fun getUnGzipInputDataStream(binaryCache: BinaryCache): InputStream {
return if (isCompressed) {
GZIPInputStream(getInputDataStream(binaryCache))
} else {
getInputDataStream(binaryCache)
}
}
@Throws(IOException::class)
fun getGzipOutputDataStream(binaryCache: BinaryCache): OutputStream {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream(binaryCache))
} else {
getOutputDataStream(binaryCache)
}
}
@Throws(IOException::class)
abstract fun compress(binaryCache: BinaryCache)
@Throws(IOException::class)
abstract fun decompress(binaryCache: BinaryCache)
@Throws(IOException::class)
fun dataExists(): Boolean {
return mLength > 0
}
@Throws(IOException::class)
fun getSize(): Long {
return mLength
}
@Throws(IOException::class)
fun binaryHash(): Int {
return mBinaryHash
}
@Throws(IOException::class)
abstract fun clear(binaryCache: BinaryCache)
override fun describeContents(): Int {
return 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is BinaryData) return false
if (isCompressed != other.isCompressed) return false
if (isProtected != other.isProtected) return false
if (isCorrupted != other.isCorrupted) return false
return true
}
override fun hashCode(): Int {
var result = isCompressed.hashCode()
result = 31 * result + isProtected.hashCode()
result = 31 * result + isCorrupted.hashCode()
result = 31 * result + mLength.hashCode()
result = 31 * result + mBinaryHash
return result
}
/**
* Custom OutputStream to calculate the size and hash of binary file
*/
protected inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) {
private val mMessageDigest: MessageDigest
init {
mLength = 0
mMessageDigest = MessageDigest.getInstance("MD5")
mBinaryHash = 0
}
override fun beforeWrite(n: Int) {
super.beforeWrite(n)
mLength = byteCount
}
override fun write(idx: Int) {
super.write(idx)
mMessageDigest.update(idx.toByte())
}
override fun write(bts: ByteArray) {
super.write(bts)
mMessageDigest.update(bts)
}
override fun write(bts: ByteArray, st: Int, end: Int) {
super.write(bts, st, end)
mMessageDigest.update(bts, st, end)
}
override fun close() {
super.close()
mLength = byteCount
val bytes = mMessageDigest.digest()
mBinaryHash = ByteBuffer.wrap(bytes).int
}
}
companion object {
private val TAG = BinaryData::class.java.name
fun canMemoryBeAllocatedInRAM(context: Context, memoryWanted: Long): Boolean {
val memoryInfo = ActivityManager.MemoryInfo()
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
val availableMemory = memoryInfo.availMem
return availableMemory > memoryWanted * 3
}
}
}

View File

@@ -0,0 +1,183 @@
/*
* Copyright 2018 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.element.binary
import android.os.Parcel
import android.os.Parcelable
import android.util.Base64
import android.util.Base64InputStream
import android.util.Base64OutputStream
import com.kunzisoft.keepass.utils.readAllBytes
import java.io.*
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
class BinaryFile : BinaryData {
private var mDataFile: File? = null
// Cipher to encrypt temp file
@Transient
private var cipherEncryption: Cipher = Cipher.getInstance(LoadedKey.BINARY_CIPHER)
@Transient
private var cipherDecryption: Cipher = Cipher.getInstance(LoadedKey.BINARY_CIPHER)
constructor(dataFile: File,
compressed: Boolean = false,
protected: Boolean = false) : super(compressed, protected) {
this.mDataFile = dataFile
}
constructor(parcel: Parcel) : super(parcel) {
parcel.readString()?.let {
mDataFile = File(it)
}
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeString(mDataFile?.absolutePath)
}
@Throws(IOException::class)
override fun getInputDataStream(binaryCache: BinaryCache): InputStream {
return buildInputStream(mDataFile, binaryCache)
}
@Throws(IOException::class)
override fun getOutputDataStream(binaryCache: BinaryCache): OutputStream {
return buildOutputStream(mDataFile, binaryCache)
}
@Throws(IOException::class)
private fun buildInputStream(file: File?, binaryCache: BinaryCache): InputStream {
val cipherKey = binaryCache.loadedCipherKey
return when {
file != null && file.length() > 0 -> {
cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
Base64InputStream(CipherInputStream(FileInputStream(file), cipherDecryption), Base64.NO_WRAP)
}
else -> ByteArrayInputStream(ByteArray(0))
}
}
@Throws(IOException::class)
private fun buildOutputStream(file: File?, binaryCache: BinaryCache): OutputStream {
val cipherKey = binaryCache.loadedCipherKey
return when {
file != null -> {
cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
BinaryCountingOutputStream(Base64OutputStream(CipherOutputStream(FileOutputStream(file), cipherEncryption), Base64.NO_WRAP))
}
else -> throw IOException("Unable to write in an unknown file")
}
}
@Throws(IOException::class)
override fun compress(binaryCache: BinaryCache) {
mDataFile?.let { concreteDataFile ->
// To compress, create a new binary with file
if (!isCompressed) {
// Encrypt the new gzipped temp file
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
getInputDataStream(binaryCache).use { inputStream ->
GZIPOutputStream(buildOutputStream(fileBinaryCompress, binaryCache)).use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
// Remove ungzip file
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
}
}
}
}
}
@Throws(IOException::class)
override fun decompress(binaryCache: BinaryCache) {
mDataFile?.let { concreteDataFile ->
if (isCompressed) {
// Encrypt the new ungzipped temp file
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
getUnGzipInputDataStream(binaryCache).use { inputStream ->
buildOutputStream(fileBinaryDecompress, binaryCache).use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
// Remove gzip file
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
}
}
}
}
}
override fun clear(binaryCache: BinaryCache) {
if (mDataFile != null && !mDataFile!!.delete())
throw IOException("Unable to delete temp file " + mDataFile!!.absolutePath)
}
override fun toString(): String {
return mDataFile.toString()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is BinaryFile) return false
if (!super.equals(other)) return false
return mDataFile != null && mDataFile == other.mDataFile
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (mDataFile?.hashCode() ?: 0)
return result
}
companion object {
private val TAG = BinaryFile::class.java.name
@JvmField
val CREATOR: Parcelable.Creator<BinaryFile> = object : Parcelable.Creator<BinaryFile> {
override fun createFromParcel(parcel: Parcel): BinaryFile {
return BinaryFile(parcel)
}
override fun newArray(size: Int): Array<BinaryFile?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -0,0 +1,262 @@
/*
* 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.element.binary
import android.util.Log
import java.io.IOException
import kotlin.math.abs
abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
protected val pool = LinkedHashMap<T, BinaryData>()
// To build unique file id
private var creationId: Long = System.currentTimeMillis()
private var poolId: Int = abs(javaClass.simpleName.hashCode())
private var binaryFileIncrement = 0L
/**
* To get a binary by the pool key (ref attribute in entry)
*/
operator fun get(key: T): BinaryData? {
return pool[key]
}
/**
* Create and return a new binary file not yet linked to a binary
*/
fun put(key: T? = null,
builder: (uniqueBinaryId: String) -> BinaryData): KeyBinary<T> {
binaryFileIncrement++
val newBinaryFile: BinaryData = builder("$poolId$creationId$binaryFileIncrement")
val newKey = put(key, newBinaryFile)
return KeyBinary(newBinaryFile, newKey)
}
/**
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
*/
fun put(key: T?, value: BinaryData): T {
if (key == null)
return put(value)
else
pool[key] = value
return key
}
/**
* To put a [binaryData] in the pool,
* if already exists, replace the current one,
* else add it with a new key
*/
fun put(binaryData: BinaryData): T {
var key: T? = findKey(binaryData)
if (key == null) {
key = findUnusedKey()
}
pool[key!!] = binaryData
return key
}
/**
* Remove a binary from the pool with its [key], the file is not deleted
*/
@Throws(IOException::class)
fun remove(key: T) {
pool.remove(key)
// Don't clear attachment here because a file can be used in many BinaryAttachment
}
/**
* Remove a binary from the pool, the file is not deleted
*/
@Throws(IOException::class)
fun remove(binaryData: BinaryData) {
findKey(binaryData)?.let {
pool.remove(it)
}
// Don't clear attachment here because a file can be used in many BinaryAttachment
}
/**
* Utility method to find an unused key in the pool
*/
abstract fun findUnusedKey(): T
/**
* Return key of [binaryDataToRetrieve] or null if not found
*/
private fun findKey(binaryDataToRetrieve: BinaryData): T? {
val contains = pool.containsValue(binaryDataToRetrieve)
return if (!contains)
null
else {
for ((key, binary) in pool) {
if (binary == binaryDataToRetrieve) {
return key
}
}
return null
}
}
fun isBinaryDuplicate(binaryData: BinaryData?): Boolean {
try {
binaryData?.let {
if (it.getSize() > 0) {
val searchBinaryMD5 = it.binaryHash()
var i = 0
for ((_, binary) in pool) {
if (binary.binaryHash() == searchBinaryMD5) {
i++
if (i > 1)
return true
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to check binary duplication", e)
}
return false
}
/**
* To do an action on each binary in the pool (order is not important)
*/
private fun doForEachBinary(action: (key: T, binary: BinaryData) -> Unit,
condition: (key: T, binary: BinaryData) -> Boolean) {
for ((key, value) in pool) {
if (condition.invoke(key, value)) {
action.invoke(key, value)
}
}
}
fun doForEachBinary(action: (key: T, binary: BinaryData) -> Unit) {
doForEachBinary(action) { _, _ -> true }
}
/**
* Utility method to order binaries and solve index problem in database v4
*/
protected fun orderedBinariesWithoutDuplication(condition: ((binary: BinaryData) -> Boolean) = { true })
: List<KeyBinary<T>> {
val keyBinaryList = ArrayList<KeyBinary<T>>()
for ((key, binary) in pool) {
// Don't deduplicate
val existentBinary =
try {
if (binary.getSize() > 0) {
keyBinaryList.find {
val hash0 = it.binary.binaryHash()
val hash1 = binary.binaryHash()
hash0 != 0 && hash1 != 0 && hash0 == hash1
}
} else {
null
}
} catch (e: Exception) {
Log.e(TAG, "Unable to check binary hash", e)
null
}
if (existentBinary == null) {
val newKeyBinary = KeyBinary(binary, key)
if (condition.invoke(newKeyBinary.binary)) {
keyBinaryList.add(newKeyBinary)
}
} else {
if (condition.invoke(existentBinary.binary)) {
existentBinary.addKey(key)
}
}
}
return keyBinaryList
}
/**
* Different from doForEach, provide an ordered index to each binary
*/
private fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> Unit,
conditionToAdd: (binary: BinaryData) -> Boolean) {
orderedBinariesWithoutDuplication(conditionToAdd).forEach { keyBinary ->
action.invoke(keyBinary)
}
}
fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> Unit) {
doForEachBinaryWithoutDuplication(action, { true })
}
/**
* Different from doForEach, provide an ordered index to each binary
*/
private fun doForEachOrderedBinaryWithoutDuplication(action: (index: Int, binary: BinaryData) -> Unit,
conditionToAdd: (binary: BinaryData) -> Boolean) {
orderedBinariesWithoutDuplication(conditionToAdd).forEachIndexed { index, keyBinary ->
action.invoke(index, keyBinary.binary)
}
}
fun doForEachOrderedBinaryWithoutDuplication(action: (index: Int, binary: BinaryData) -> Unit) {
doForEachOrderedBinaryWithoutDuplication(action, { true })
}
fun isEmpty(): Boolean {
return pool.isEmpty()
}
@Throws(IOException::class)
fun clear() {
doForEachBinary { _, binary ->
binary.clear(mBinaryCache)
}
pool.clear()
}
override fun toString(): String {
val stringBuffer = StringBuffer()
for ((key, value) in pool) {
if (stringBuffer.isNotEmpty())
stringBuffer.append(", {$key:$value}")
else
stringBuffer.append("{$key:$value}")
}
return stringBuffer.toString()
}
/**
* Utility class to order binaries
*/
class KeyBinary<T>(val binary: BinaryData, key: T) {
val keys = HashSet<T>()
init {
addKey(key)
}
fun addKey(key: T) {
keys.add(key)
}
}
companion object {
private val TAG = BinaryPool::class.java.name
}
}

View File

@@ -0,0 +1,43 @@
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(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()
while (pool.containsKey(newUUID)) {
newUUID = UUID.randomUUID()
}
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

@@ -0,0 +1,18 @@
package com.kunzisoft.keepass.database.element.binary
import java.io.Serializable
import java.security.Key
import java.security.SecureRandom
import javax.crypto.KeyGenerator
class LoadedKey(val key: Key, val iv: ByteArray): Serializable {
companion object {
const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding"
fun generateNewCipherKey(): LoadedKey {
val iv = ByteArray(8)
SecureRandom().nextBytes(iv)
return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), iv)
}
}
}

View File

@@ -1,259 +0,0 @@
/*
* Copyright 2018 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.element.database
import android.os.Parcel
import android.os.Parcelable
import android.util.Base64
import android.util.Base64InputStream
import android.util.Base64OutputStream
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.stream.readAllBytes
import org.apache.commons.io.output.CountingOutputStream
import java.io.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
class BinaryAttachment : Parcelable {
private var dataFile: File? = null
var length: Long = 0
private set
var isCompressed: Boolean = false
private set
var isProtected: Boolean = false
private set
var isCorrupted: Boolean = false
// Cipher to encrypt temp file
private var cipherEncryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
private var cipherDecryption: Cipher = Cipher.getInstance(Database.LoadedKey.BINARY_CIPHER)
/**
* Empty protected binary
*/
constructor()
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
this.dataFile = dataFile
this.length = 0
this.isCompressed = compressed
this.isProtected = protected
}
private constructor(parcel: Parcel) {
parcel.readString()?.let {
dataFile = File(it)
}
length = parcel.readLong()
isCompressed = parcel.readByte().toInt() != 0
isProtected = parcel.readByte().toInt() != 0
isCorrupted = parcel.readByte().toInt() != 0
}
@Throws(IOException::class)
fun getInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return buildInputStream(dataFile!!, cipherKey)
}
@Throws(IOException::class)
fun getOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
return buildOutputStream(dataFile!!, cipherKey)
}
@Throws(IOException::class)
fun getUnGzipInputDataStream(cipherKey: Database.LoadedKey): InputStream {
return if (isCompressed) {
GZIPInputStream(getInputDataStream(cipherKey))
} else {
getInputDataStream(cipherKey)
}
}
@Throws(IOException::class)
fun getGzipOutputDataStream(cipherKey: Database.LoadedKey): OutputStream {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream(cipherKey))
} else {
getOutputDataStream(cipherKey)
}
}
@Throws(IOException::class)
private fun buildInputStream(file: File?, cipherKey: Database.LoadedKey): InputStream {
return when {
file != null && file.length() > 0 -> {
cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
Base64InputStream(CipherInputStream(FileInputStream(file), cipherDecryption), Base64.NO_WRAP)
}
else -> ByteArrayInputStream(ByteArray(0))
}
}
@Throws(IOException::class)
private fun buildOutputStream(file: File?, cipherKey: Database.LoadedKey): OutputStream {
return when {
file != null -> {
cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
BinaryCountingOutputStream(Base64OutputStream(CipherOutputStream(FileOutputStream(file), cipherEncryption), Base64.NO_WRAP))
}
else -> throw IOException("Unable to write in an unknown file")
}
}
@Throws(IOException::class)
fun compress(cipherKey: Database.LoadedKey) {
dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file
if (!isCompressed) {
// Encrypt the new gzipped temp file
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
getInputDataStream(cipherKey).use { inputStream ->
GZIPOutputStream(buildOutputStream(fileBinaryCompress, cipherKey)).use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
// Remove ungzip file
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
}
}
}
}
}
@Throws(IOException::class)
fun decompress(cipherKey: Database.LoadedKey) {
dataFile?.let { concreteDataFile ->
if (isCompressed) {
// Encrypt the new ungzipped temp file
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
getUnGzipInputDataStream(cipherKey).use { inputStream ->
buildOutputStream(fileBinaryDecompress, cipherKey).use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
// Remove gzip file
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
}
}
}
}
}
@Throws(IOException::class)
fun clear() {
if (dataFile != null && !dataFile!!.delete())
throw IOException("Unable to delete temp file " + dataFile!!.absolutePath)
}
override fun equals(other: Any?): Boolean {
if (this === other)
return true
if (other == null || javaClass != other.javaClass)
return false
if (other !is BinaryAttachment)
return false
var sameData = false
if (dataFile != null && dataFile == other.dataFile)
sameData = true
return isCompressed == other.isCompressed
&& isProtected == other.isProtected
&& isCorrupted == other.isCorrupted
&& sameData
}
override fun hashCode(): Int {
var result = 0
result = 31 * result + if (isCompressed) 1 else 0
result = 31 * result + if (isProtected) 1 else 0
result = 31 * result + if (isCorrupted) 1 else 0
result = 31 * result + dataFile!!.hashCode()
result = 31 * result + length.hashCode()
return result
}
override fun toString(): String {
return dataFile.toString()
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(dataFile?.absolutePath)
dest.writeLong(length)
dest.writeByte((if (isCompressed) 1 else 0).toByte())
dest.writeByte((if (isProtected) 1 else 0).toByte())
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
}
/**
* Custom OutputStream to calculate the size of binary file
*/
private inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) {
init {
length = 0
}
override fun beforeWrite(n: Int) {
super.beforeWrite(n)
length = byteCount
}
override fun close() {
super.close()
length = byteCount
}
}
companion object {
private val TAG = BinaryAttachment::class.java.name
@JvmField
val CREATOR: Parcelable.Creator<BinaryAttachment> = object : Parcelable.Creator<BinaryAttachment> {
override fun createFromParcel(parcel: Parcel): BinaryAttachment {
return BinaryAttachment(parcel)
}
override fun newArray(size: Int): Array<BinaryAttachment?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -1,155 +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.element.database
import java.io.IOException
class BinaryPool {
private val pool = LinkedHashMap<Int, BinaryAttachment>()
/**
* To get a binary by the pool key (ref attribute in entry)
*/
operator fun get(key: Int): BinaryAttachment? {
return pool[key]
}
/**
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
*/
fun put(key: Int?, value: BinaryAttachment) {
if (key == null)
put(value)
else
pool[key] = value
}
/**
* To put a [binaryAttachment] in the pool,
* if already exists, replace the current one,
* else add it with a new key
*/
fun put(binaryAttachment: BinaryAttachment): Int {
var key = findKey(binaryAttachment)
if (key == null) {
key = findUnusedKey()
}
pool[key] = binaryAttachment
return key
}
/**
* Remove a binary from the pool, the file is not deleted
*/
@Throws(IOException::class)
fun remove(binaryAttachment: BinaryAttachment) {
findKey(binaryAttachment)?.let {
pool.remove(it)
}
// Don't clear attachment here because a file can be used in many BinaryAttachment
}
/**
* Utility method to find an unused key in the pool
*/
private fun findUnusedKey(): Int {
var unusedKey = 0
while (pool[unusedKey] != null)
unusedKey++
return unusedKey
}
/**
* Return key of [binaryAttachmentToRetrieve] or null if not found
*/
private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? {
val contains = pool.containsValue(binaryAttachmentToRetrieve)
return if (!contains)
null
else {
for ((key, binary) in pool) {
if (binary == binaryAttachmentToRetrieve) {
return key
}
}
return null
}
}
/**
* Utility method to order binaries and solve index problem in database v4
*/
private fun orderedBinaries(): List<KeyBinary> {
val keyBinaryList = ArrayList<KeyBinary>()
for ((key, binary) in pool) {
keyBinaryList.add(KeyBinary(key, binary))
}
return keyBinaryList
}
/**
* To register a binary with a ref corresponding to an ordered index
*/
fun getBinaryIndexFromKey(key: Int): Int? {
val index = orderedBinaries().indexOfFirst { it.key == key }
return if (index < 0)
null
else
index
}
/**
* Different from doForEach, provide an ordered index to each binary
*/
fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) {
orderedBinaries().forEachIndexed(action)
}
/**
* To do an action on each binary in the pool
*/
fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) {
pool.values.forEach { action.invoke(it) }
}
@Throws(IOException::class)
fun clear() {
doForEachBinary {
it.clear()
}
pool.clear()
}
override fun toString(): String {
val stringBuffer = StringBuffer()
for ((key, value) in pool) {
if (stringBuffer.isNotEmpty())
stringBuffer.append(", {$key:$value}")
else
stringBuffer.append("{$key:$value}")
}
return stringBuffer.toString()
}
/**
* Utility data class to order binaries
*/
data class KeyBinary(val key: Int, val binary: BinaryAttachment)
}

View File

@@ -19,33 +19,27 @@
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import com.kunzisoft.keepass.crypto.finalkey.AESKeyTransformerFactory import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.encrypt.aes.AESTransformer
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.NullOutputStream
import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.security.DigestOutputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() { class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
private var kdfListV3: MutableList<KdfEngine> = ArrayList() private var kdfListV3: MutableList<KdfEngine> = ArrayList()
private var binaryIncrement = 0
override val version: String override val version: String
get() = "KeePass 1" get() = "KeePass 1"
@@ -59,16 +53,17 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return getGroupById(NodeIdInt(groupId)) return getGroupById(NodeIdInt(groupId))
} }
// Retrieve backup group in index
val backupGroup: GroupKDB? val backupGroup: GroupKDB?
get() { get() {
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID) return retrieveBackup()
null
else
getGroupById(backupGroupId)
} }
override val kdfEngine: KdfEngine? val groupNamesNotAllowed: List<String>
get() {
return listOf(BACKUP_FOLDER_TITLE)
}
override val kdfEngine: KdfEngine
get() = kdfListV3[0] get() = kdfListV3[0]
override val kdfAvailableList: List<KdfEngine> override val kdfAvailableList: List<KdfEngine>
@@ -78,17 +73,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
get() { get() {
val list = ArrayList<EncryptionAlgorithm>() val list = ArrayList<EncryptionAlgorithm>()
list.add(EncryptionAlgorithm.AESRijndael) list.add(EncryptionAlgorithm.AESRijndael)
list.add(EncryptionAlgorithm.Twofish)
return list return list
} }
val rootGroups: List<GroupKDB> val rootGroups: List<GroupKDB>
get() { get() {
val kids = ArrayList<GroupKDB>() return rootGroup?.getChildGroups() ?: ArrayList()
doForEachGroupInIndex { group ->
if (group.level == 0)
kids.add(group)
}
return kids
} }
override val passwordEncoding: String override val passwordEncoding: String
@@ -143,24 +134,11 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
} }
@Throws(IOException::class) @Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray, masterSeed2: ByteArray, numRounds: Long) { fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) {
// Write checksum Checksum
val messageDigest: MessageDigest
try {
messageDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
throw IOException("SHA-256 not implemented here.")
}
val nos = NullOutputStream()
val dos = DigestOutputStream(nos, messageDigest)
// Encrypt the master key a few times to make brute-force key-search harder // Encrypt the master key a few times to make brute-force key-search harder
dos.write(masterSeed) val transformedKey = AESTransformer.transformKey(transformSeed, masterKey, numRounds) ?: ByteArray(0)
dos.write(AESKeyTransformerFactory.transformMasterKey(masterSeed2, masterKey, numRounds) ?: ByteArray(0)) // Write checksum Checksum
finalKey = HashManager.hashSha256(masterSeed, transformedKey)
finalKey = messageDigest.digest()
} }
override fun createGroup(): GroupKDB { override fun createGroup(): GroupKDB {
@@ -175,27 +153,20 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return false return false
} }
override fun containsCustomData(): Boolean { override fun getStandardIcon(iconId: Int): IconImageStandard {
return false return this.iconsManager.getIcon(iconId)
} }
override fun isInRecycleBin(group: GroupKDB): Boolean { override fun isInRecycleBin(group: GroupKDB): Boolean {
var currentGroup: GroupKDB? = group var currentGroup: GroupKDB? = group
val currentBackupGroup = backupGroup ?: return false
// Init backup group variable if (currentGroup == currentBackupGroup)
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
findBackupGroupId()
if (backupGroup == null)
return false
if (currentGroup == backupGroup)
return true return true
val backupGroupId = currentBackupGroup.id
while (currentGroup != null) { while (currentGroup != null) {
if (currentGroup.level == 0 if (backupGroupId == currentGroup.id) {
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
backupGroupId = currentGroup.id
return true return true
} }
currentGroup = currentGroup.parent currentGroup = currentGroup.parent
@@ -203,12 +174,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return false return false
} }
private fun findBackupGroupId() { /**
rootGroups.forEach { currentGroup -> * Retrieve backup group with his name
if (currentGroup.level == 0 */
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) { private fun retrieveBackup(): GroupKDB? {
backupGroupId = currentGroup.id return rootGroup?.searchChildGroup {
} it.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)
} }
} }
@@ -217,16 +188,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
* if it doesn't exist * if it doesn't exist
*/ */
fun ensureBackupExists() { fun ensureBackupExists() {
findBackupGroupId()
if (backupGroup == null) { if (backupGroup == null) {
// Create recycle bin // Create recycle bin
val recycleBinGroup = createGroup().apply { val recycleBinGroup = createGroup().apply {
title = BACKUP_FOLDER_TITLE title = BACKUP_FOLDER_TITLE
icon = iconFactory.trashIcon icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
} }
addGroupTo(recycleBinGroup, rootGroup) addGroupTo(recycleBinGroup, rootGroup)
backupGroupId = recycleBinGroup.id
} }
} }
@@ -269,17 +237,16 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
addEntryTo(entry, origParent) addEntryTo(entry, origParent)
} }
fun buildNewBinary(cacheDirectory: File): BinaryAttachment { fun buildNewAttachment(): BinaryData {
// Generate an unique new file // Generate an unique new file
val fileInCache = File(cacheDirectory, binaryIncrement.toString()) return attachmentPool.put { uniqueBinaryId ->
binaryIncrement++ binaryCache.getBinaryData(uniqueBinaryId, false)
return BinaryAttachment(fileInCache) }.binary
} }
companion object { companion object {
val TYPE = DatabaseKDB::class.java val TYPE = DatabaseKDB::class.java
const val BACKUP_FOLDER_TITLE = "Backup" const val BACKUP_FOLDER_TITLE = "Backup"
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
} }
} }

View File

@@ -22,55 +22,63 @@ package com.kunzisoft.keepass.database.element.database
import android.content.res.Resources import android.content.res.Resources
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CryptoUtil
import com.kunzisoft.keepass.crypto.engine.AesEngine
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.AesEngine
import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
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.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject 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.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX 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.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
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.utils.StringUtil.removeSpaceChars import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.VariantDictionary import com.kunzisoft.keepass.utils.longTo8Bytes
import org.apache.commons.codec.binary.Hex import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node import org.w3c.dom.Node
import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.util.* import java.util.*
import javax.crypto.Mac
import javax.xml.XMLConstants import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException import javax.xml.parsers.ParserConfigurationException
import kotlin.math.min
class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> { class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var hmacKey: ByteArray? = null var hmacKey: ByteArray? = null
private set private set
var dataCipher = AesEngine.CIPHER_UUID var cipherUuid = EncryptionAlgorithm.AESRijndael.uuid
private var dataEngine: CipherEngine = AesEngine() private var dataEngine: CipherEngine = AesEngine()
var compressionAlgorithm = CompressionAlgorithm.GZip var compressionAlgorithm = CompressionAlgorithm.GZip
var kdfParameters: KdfParameters? = null var kdfParameters: KdfParameters? = null
private var kdfList: MutableList<KdfEngine> = ArrayList() private var kdfList: MutableList<KdfEngine> = ArrayList()
private var numKeyEncRounds: Long = 0 private var numKeyEncRounds: Long = 0
var publicCustomData = VariantDictionary() var publicCustomData = VariantDictionary()
private val mFieldReferenceEngine = FieldReferencesEngine(this)
var kdbxVersion = UnsignedInt(0) var kdbxVersion = UnsignedInt(0)
var name = "" var name = ""
@@ -96,7 +104,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
*/ */
var isRecycleBinEnabled = true var isRecycleBinEnabled = true
var recycleBinUUID: UUID = UUID_ZERO var recycleBinUUID: UUID = UUID_ZERO
var recycleBinChanged = Date() var recycleBinChanged = DateInstant()
var entryTemplatesGroup = UUID_ZERO var entryTemplatesGroup = UUID_ZERO
var entryTemplatesGroupChanged = DateInstant() var entryTemplatesGroupChanged = DateInstant()
var historyMaxItems = DEFAULT_HISTORY_MAX_ITEMS var historyMaxItems = DEFAULT_HISTORY_MAX_ITEMS
@@ -105,11 +113,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var lastTopVisibleGroupUUID = UUID_ZERO var lastTopVisibleGroupUUID = UUID_ZERO
var memoryProtection = MemoryProtectionConfig() var memoryProtection = MemoryProtectionConfig()
val deletedObjects = ArrayList<DeletedObject>() val deletedObjects = ArrayList<DeletedObject>()
val customIcons = ArrayList<IconImageCustom>() val customData = CustomData()
val customData = HashMap<String, String>()
var binaryPool = BinaryPool()
private var binaryIncrement = 0 // Unique id (don't use current time because CPU too fast)
var localizedAppName = "KeePassDX" var localizedAppName = "KeePassDX"
@@ -126,20 +130,20 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
*/ */
constructor(databaseName: String, rootName: String) { constructor(databaseName: String, rootName: String) {
name = databaseName name = databaseName
kdbxVersion = FILE_VERSION_32_3 kdbxVersion = FILE_VERSION_31
val group = createGroup().apply { val group = createGroup().apply {
title = rootName title = rootName
icon = iconFactory.folderIcon icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
} }
rootGroup = group rootGroup = group
addGroupIndex(group)
} }
override val version: String override val version: String
get() { get() {
val kdbxStringVersion = when(kdbxVersion) { val kdbxStringVersion = when(kdbxVersion) {
FILE_VERSION_32_3 -> "3.1" FILE_VERSION_31 -> "3.1"
FILE_VERSION_32_4 -> "4.0" FILE_VERSION_40 -> "4.0"
FILE_VERSION_41 -> "4.1"
else -> "UNKNOWN" else -> "UNKNOWN"
} }
return "KeePass 2 - KDBX$kdbxStringVersion" return "KeePass 2 - KDBX$kdbxStringVersion"
@@ -187,7 +191,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
CompressionAlgorithm.GZip -> { CompressionAlgorithm.GZip -> {
// Only in databaseV3.1, in databaseV4 the header is zipped during the save // Only in databaseV3.1, in databaseV4 the header is zipped during the save
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) { if (kdbxVersion.isBefore(FILE_VERSION_40)) {
compressAllBinaries() compressAllBinaries()
} }
} }
@@ -195,9 +199,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
CompressionAlgorithm.GZip -> { CompressionAlgorithm.GZip -> {
// In databaseV4 the header is zipped during the save, so not necessary here // In databaseV4 the header is zipped during the save, so not necessary here
if (kdbxVersion.toKotlinLong() >= FILE_VERSION_32_4.toKotlinLong()) { if (kdbxVersion.isBefore(FILE_VERSION_40)) {
decompressAllBinaries()
} else {
when (newCompression) { when (newCompression) {
CompressionAlgorithm.None -> { CompressionAlgorithm.None -> {
decompressAllBinaries() decompressAllBinaries()
@@ -205,18 +207,18 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
CompressionAlgorithm.GZip -> { CompressionAlgorithm.GZip -> {
} }
} }
} else {
decompressAllBinaries()
} }
} }
} }
} }
private fun compressAllBinaries() { private fun compressAllBinaries() {
binaryPool.doForEachBinary { binary -> attachmentPool.doForEachBinary { _, binary ->
try { try {
val cipherKey = loadedCipherKey
?: throw IOException("Unable to retrieve cipher key to compress binaries")
// To compress, create a new binary with file // To compress, create a new binary with file
binary.compress(cipherKey) binary.compress(binaryCache)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to compress $binary", e) Log.e(TAG, "Unable to compress $binary", e)
} }
@@ -224,11 +226,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
private fun decompressAllBinaries() { private fun decompressAllBinaries() {
binaryPool.doForEachBinary { binary -> attachmentPool.doForEachBinary { _, binary ->
try { try {
val cipherKey = loadedCipherKey binary.decompress(binaryCache)
?: throw IOException("Unable to retrieve cipher key to decompress binaries")
binary.decompress(cipherKey)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to decompress $binary", e) Log.e(TAG, "Unable to decompress $binary", e)
} }
@@ -307,24 +307,76 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
this.dataEngine = dataEngine this.dataEngine = dataEngine
} }
fun getCustomIcons(): List<IconImageCustom> { override fun getStandardIcon(iconId: Int): IconImageStandard {
return customIcons return this.iconsManager.getIcon(iconId)
} }
fun addCustomIcon(customIcon: IconImageCustom) { fun buildNewCustomIcon(customIconId: UUID? = null,
this.customIcons.add(customIcon) result: (IconImageCustom, BinaryData?) -> Unit) {
iconsManager.buildNewCustomIcon(customIconId, result)
} }
fun getCustomData(): Map<String, String> { fun addCustomIcon(customIconId: UUID? = null,
return customData name: String,
lastModificationTime: DateInstant?,
smallSize: Boolean,
result: (IconImageCustom, BinaryData?) -> Unit) {
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, smallSize, result)
} }
fun putCustomData(label: String, value: String) { fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
this.customData[label] = value return iconsManager.isCustomIconBinaryDuplicate(binary)
} }
override fun containsCustomData(): Boolean { fun getCustomIcon(iconUuid: UUID): IconImageCustom {
return getCustomData().isNotEmpty() return this.iconsManager.getIcon(iconUuid)
}
/*
* Search methods
*/
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
entry.decodeTitleKey(recursionLevel).equals(title, true)
}
}
fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
entry.decodeUsernameKey(recursionLevel).equals(username, true)
}
}
fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
entry.decodeUrlKey(recursionLevel).equals(url, true)
}
}
fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
entry.decodePasswordKey(recursionLevel).equals(password, true)
}
}
fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
entry.decodeNotesKey(recursionLevel).equals(notes, true)
}
}
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
return entryIndexes.values.find { entry ->
entry.customData.containsItemWithValue(customDataValue)
}
}
/**
* Retrieve the value of a field reference
*/
fun getFieldReferenceValue(textReference: String, recursionLevel: Int): String {
return mFieldReferenceEngine.compile(textReference, recursionLevel)
} }
@Throws(IOException::class) @Throws(IOException::class)
@@ -340,14 +392,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
masterKey = getFileKey(keyInputStream) masterKey = getFileKey(keyInputStream)
} }
val messageDigest: MessageDigest return HashManager.hashSha256(masterKey)
try {
messageDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
throw IOException("No SHA-256 implementation")
}
return messageDigest.digest(masterKey)
} }
@Throws(IOException::class) @Throws(IOException::class)
@@ -358,13 +403,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters) var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
if (transformedMasterKey.size != 32) { if (transformedMasterKey.size != 32) {
transformedMasterKey = CryptoUtil.hashSha256(transformedMasterKey) transformedMasterKey = HashManager.hashSha256(transformedMasterKey)
} }
val cmpKey = ByteArray(65) val cmpKey = ByteArray(65)
System.arraycopy(masterSeed, 0, cmpKey, 0, 32) System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32) System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
finalKey = CryptoUtil.resizeKey(cmpKey, 0, 64, dataEngine.keyLength()) finalKey = resizeKey(cmpKey, dataEngine.keyLength())
val messageDigest: MessageDigest val messageDigest: MessageDigest
try { try {
@@ -379,6 +424,47 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
} }
private fun resizeKey(inBytes: ByteArray, cbOut: Int): ByteArray {
if (cbOut == 0) return ByteArray(0)
val messageDigest = if (cbOut <= 32) HashManager.getHash256() else HashManager.getHash512()
messageDigest.update(inBytes, 0, 64)
val hash: ByteArray = messageDigest.digest()
if (cbOut == hash.size) {
return hash
}
val ret = ByteArray(cbOut)
if (cbOut < hash.size) {
System.arraycopy(hash, 0, ret, 0, cbOut)
} else {
var pos = 0
var r: Long = 0
while (pos < cbOut) {
val hmac: Mac
try {
hmac = Mac.getInstance("HmacSHA256")
} catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e)
}
val pbR = longTo8Bytes(r)
val part = hmac.doFinal(pbR)
val copy = min(cbOut - pos, part.size)
System.arraycopy(part, 0, ret, pos, copy)
pos += copy
r++
Arrays.fill(part, 0.toByte())
}
}
Arrays.fill(hash, 0.toByte())
return ret
}
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
try { try {
val documentBuilderFactory = DocumentBuilderFactory.newInstance() val documentBuilderFactory = DocumentBuilderFactory.newInstance()
@@ -476,17 +562,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
private fun checkKeyFileHash(data: String, hash: String): Boolean { private fun checkKeyFileHash(data: String, hash: String): Boolean {
val digest: MessageDigest?
var success = false var success = false
try { try {
digest = MessageDigest.getInstance("SHA-256")
digest?.reset()
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key. // hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
val dataDigest = digest.digest(Hex.decodeHex(data.toCharArray())) val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray()))
.copyOfRange(0, 4) .copyOfRange(0, 4).toHexString()
.toHexString()
success = dataDigest == hash success = dataDigest == hash
} catch (e: NoSuchAlgorithmException) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
return success return success
@@ -550,21 +632,21 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// Create recycle bin // Create recycle bin
val recycleBinGroup = createGroup().apply { val recycleBinGroup = createGroup().apply {
title = resources.getString(R.string.recycle_bin) title = resources.getString(R.string.recycle_bin)
icon = iconFactory.trashIcon icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
enableAutoType = false enableAutoType = false
enableSearching = false enableSearching = false
isExpanded = false isExpanded = false
} }
addGroupTo(recycleBinGroup, rootGroup) addGroupTo(recycleBinGroup, rootGroup)
recycleBinUUID = recycleBinGroup.id recycleBinUUID = recycleBinGroup.id
recycleBinChanged = recycleBinGroup.lastModificationTime.date recycleBinChanged = recycleBinGroup.lastModificationTime
} }
} }
fun removeRecycleBin() { fun removeRecycleBin() {
if (recycleBin != null) { if (recycleBin != null) {
recycleBinUUID = UUID_ZERO recycleBinUUID = UUID_ZERO
recycleBinChanged = DateInstant().date recycleBinChanged = DateInstant()
} }
} }
@@ -578,6 +660,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return false return false
if (recycleBin == null) if (recycleBin == null)
return false return false
if (node is GroupKDBX
&& recycleBin!!.isContainedIn(node))
return false
if (!node.isContainedIn(recycleBin!!)) if (!node.isContainedIn(recycleBin!!))
return true return true
return false return false
@@ -615,9 +700,20 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
this.deletedObjects.add(deletedObject) 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?) { override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
super.removeEntryFrom(entryToRemove, parent) super.removeEntryFrom(entryToRemove, parent)
deletedObjects.add(DeletedObject(entryToRemove.id)) deletedObjects.add(DeletedObject(entryToRemove.id))
mFieldReferenceEngine.clear()
} }
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) { override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
@@ -629,21 +725,17 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return publicCustomData.size() > 0 return publicCustomData.size() > 0
} }
fun buildNewBinary(cacheDirectory: File, fun buildNewAttachment(smallSize: Boolean,
compression: Boolean, compression: Boolean,
protection: Boolean, protection: Boolean,
binaryPoolId: Int? = null): BinaryAttachment { binaryPoolId: Int? = null): BinaryData {
// New file with current time return attachmentPool.put(binaryPoolId) { uniqueBinaryId ->
val fileInCache = File(cacheDirectory, binaryIncrement.toString()) binaryCache.getBinaryData(uniqueBinaryId, smallSize, compression, protection)
binaryIncrement++ }.binary
val binaryAttachment = BinaryAttachment(fileInCache, compression, protection)
// add attachment to pool
binaryPool.put(binaryPoolId, binaryAttachment)
return binaryAttachment
} }
fun removeUnlinkedAttachment(binary: BinaryAttachment, clear: Boolean) { fun removeUnlinkedAttachment(binary: BinaryData, clear: Boolean) {
val listBinaries = ArrayList<BinaryAttachment>() val listBinaries = ArrayList<BinaryData>()
listBinaries.add(binary) listBinaries.add(binary)
removeUnlinkedAttachments(listBinaries, clear) removeUnlinkedAttachments(listBinaries, clear)
} }
@@ -652,11 +744,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
removeUnlinkedAttachments(emptyList(), clear) removeUnlinkedAttachments(emptyList(), clear)
} }
private fun removeUnlinkedAttachments(binaries: List<BinaryAttachment>, clear: Boolean) { private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
// Build binaries to remove with all binaries known // Build binaries to remove with all binaries known
val binariesToRemove = ArrayList<BinaryAttachment>() val binariesToRemove = ArrayList<BinaryData>()
if (binaries.isEmpty()) { if (binaries.isEmpty()) {
binaryPool.doForEachBinary { binary -> attachmentPool.doForEachBinary { _, binary ->
binariesToRemove.add(binary) binariesToRemove.add(binary)
} }
} else { } else {
@@ -665,8 +757,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// Remove binaries from the list // Remove binaries from the list
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() { rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
override fun operate(node: EntryKDBX): Boolean { override fun operate(node: EntryKDBX): Boolean {
node.getAttachments(binaryPool, true).forEach { node.getAttachments(attachmentPool, true).forEach {
binariesToRemove.remove(it.binaryAttachment) binariesToRemove.remove(it.binaryData)
} }
return binariesToRemove.isNotEmpty() return binariesToRemove.isNotEmpty()
} }
@@ -674,9 +766,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// Effective removing // Effective removing
binariesToRemove.forEach { binariesToRemove.forEach {
try { try {
binaryPool.remove(it) attachmentPool.remove(it)
if (clear) if (clear)
it.clear() it.clear(binaryCache)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Unable to clean binaries", e) Log.w(TAG, "Unable to clean binaries", e)
} }
@@ -692,7 +784,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
override fun clearCache() { override fun clearCache() {
try { try {
super.clearCache() super.clearCache()
binaryPool.clear() mFieldReferenceEngine.clear()
attachmentPool.clear()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to clear cache", e) Log.e(TAG, "Unable to clear cache", e)
} }

View File

@@ -19,22 +19,22 @@
*/ */
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.entry.EntryVersioned import com.kunzisoft.keepass.database.element.entry.EntryVersioned
import com.kunzisoft.keepass.database.element.group.GroupVersioned import com.kunzisoft.keepass.database.element.group.GroupVersioned
import com.kunzisoft.keepass.database.element.icon.IconImageFactory import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconsManager
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import org.apache.commons.codec.binary.Hex import org.apache.commons.codec.binary.Hex
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.* import java.util.*
abstract class DatabaseVersioned< abstract class DatabaseVersioned<
@@ -47,21 +47,27 @@ abstract class DatabaseVersioned<
// Algorithm used to encrypt the database // Algorithm used to encrypt the database
protected var algorithm: EncryptionAlgorithm? = null protected var algorithm: EncryptionAlgorithm? = null
abstract val kdfEngine: KdfEngine? abstract val kdfEngine: com.kunzisoft.keepass.database.crypto.kdf.KdfEngine?
abstract val kdfAvailableList: List<KdfEngine> abstract val kdfAvailableList: List<com.kunzisoft.keepass.database.crypto.kdf.KdfEngine>
var masterKey = ByteArray(32) var masterKey = ByteArray(32)
var finalKey: ByteArray? = null var finalKey: ByteArray? = null
protected set protected set
var iconFactory = IconImageFactory() /**
protected set * To manage binaries in faster way
* Cipher key generated when the database is loaded, and destroyed when the database is closed
* Can be used to temporarily store database elements
*/
var binaryCache = BinaryCache()
val iconsManager = IconsManager(binaryCache)
var attachmentPool = AttachmentPool(binaryCache)
var changeDuplicateId = false var changeDuplicateId = false
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>() 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 abstract val version: String
@@ -80,6 +86,12 @@ abstract class DatabaseVersioned<
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm> abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
var rootGroup: Group? = null var rootGroup: Group? = null
set(value) {
field = value
value?.let {
addGroupIndex(it)
}
}
@Throws(IOException::class) @Throws(IOException::class)
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@@ -93,42 +105,21 @@ abstract class DatabaseVersioned<
protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray { protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
val fileKey = getFileKey(keyfileInputStream) val fileKey = getFileKey(keyfileInputStream)
val passwordKey = getPasswordKey(key) val passwordKey = getPasswordKey(key)
return HashManager.hashSha256(passwordKey, fileKey)
val messageDigest: MessageDigest
try {
messageDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
throw IOException("SHA-256 not supported")
}
messageDigest.update(passwordKey)
return messageDigest.digest(fileKey)
} }
@Throws(IOException::class) @Throws(IOException::class)
protected fun getPasswordKey(key: String): ByteArray { protected fun getPasswordKey(key: String): ByteArray {
val messageDigest: MessageDigest
try {
messageDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
throw IOException("SHA-256 not supported")
}
val bKey: ByteArray = try { val bKey: ByteArray = try {
key.toByteArray(charset(passwordEncoding)) key.toByteArray(charset(passwordEncoding))
} catch (e: UnsupportedEncodingException) { } catch (e: UnsupportedEncodingException) {
key.toByteArray() key.toByteArray()
} }
return HashManager.hashSha256(bKey)
messageDigest.update(bKey, 0, bKey.size)
return messageDigest.digest()
} }
@Throws(IOException::class) @Throws(IOException::class)
protected fun getFileKey(keyInputStream: InputStream): ByteArray { protected fun getFileKey(keyInputStream: InputStream): ByteArray {
val keyData = keyInputStream.readBytes() val keyData = keyInputStream.readBytes()
// Check XML key file // Check XML key file
@@ -146,13 +137,8 @@ abstract class DatabaseVersioned<
// Key is not base 64, treat it as binary data // Key is not base 64, treat it as binary data
} }
} }
// Hash file as binary data // Hash file as binary data
try { return HashManager.hashSha256(keyData)
return MessageDigest.getInstance("SHA-256").digest(keyData)
} catch (e: NoSuchAlgorithmException) {
throw IOException("SHA-256 not supported")
}
} }
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
@@ -329,7 +315,7 @@ abstract class DatabaseVersioned<
abstract fun rootCanContainsEntry(): Boolean abstract fun rootCanContainsEntry(): Boolean
abstract fun containsCustomData(): Boolean abstract fun getStandardIcon(iconId: Int): IconImageStandard
fun addGroupTo(newGroup: Group, parent: Group?) { fun addGroupTo(newGroup: Group, parent: Group?) {
// Add tree to parent tree // Add tree to parent tree
@@ -348,14 +334,14 @@ abstract class DatabaseVersioned<
removeGroupIndex(groupToRemove) removeGroupIndex(groupToRemove)
} }
fun addEntryTo(newEntry: Entry, parent: Group?) { open fun addEntryTo(newEntry: Entry, parent: Group?) {
// Add entry to parent // Add entry to parent
parent?.addChildEntry(newEntry) parent?.addChildEntry(newEntry)
newEntry.parent = parent newEntry.parent = parent
addEntryIndex(newEntry) addEntryIndex(newEntry)
} }
fun updateEntry(entry: Entry) { open fun updateEntry(entry: Entry) {
updateEntryIndex(entry) updateEntryIndex(entry)
} }
@@ -384,12 +370,6 @@ abstract class DatabaseVersioned<
return true return true
} }
/**
* Cipher key generated when the database is loaded, and destroyed when the database is closed
* Can be used to temporarily store database elements
*/
var loadedCipherKey: Database.LoadedKey? = null
companion object { companion object {
private const val TAG = "DatabaseVersioned" private const val TAG = "DatabaseVersioned"

View File

@@ -21,8 +21,6 @@ package com.kunzisoft.keepass.database.element.entry
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
class AutoType : Parcelable { class AutoType : Parcelable {
@@ -30,7 +28,7 @@ class AutoType : Parcelable {
var enabled = true var enabled = true
var obfuscationOptions = OBF_OPT_NONE var obfuscationOptions = OBF_OPT_NONE
var defaultSequence = "" var defaultSequence = ""
private var windowSeqPairs = LinkedHashMap<String, String>() private var windowSeqPairs = ArrayList<AutoTypeItem>()
constructor() constructor()
@@ -38,16 +36,15 @@ class AutoType : Parcelable {
this.enabled = autoType.enabled this.enabled = autoType.enabled
this.obfuscationOptions = autoType.obfuscationOptions this.obfuscationOptions = autoType.obfuscationOptions
this.defaultSequence = autoType.defaultSequence this.defaultSequence = autoType.defaultSequence
for ((key, value) in autoType.windowSeqPairs) { this.windowSeqPairs.clear()
this.windowSeqPairs[key] = value this.windowSeqPairs.addAll(autoType.windowSeqPairs)
}
} }
constructor(parcel: Parcel) { constructor(parcel: Parcel) {
this.enabled = parcel.readByte().toInt() != 0 this.enabled = parcel.readByte().toInt() != 0
this.obfuscationOptions = UnsignedInt(parcel.readInt()) this.obfuscationOptions = UnsignedInt(parcel.readInt())
this.defaultSequence = parcel.readString() ?: defaultSequence this.defaultSequence = parcel.readString() ?: defaultSequence
this.windowSeqPairs = ParcelableUtil.readStringParcelableMap(parcel) parcel.readTypedList(this.windowSeqPairs, AutoTypeItem.CREATOR)
} }
override fun describeContents(): Int { override fun describeContents(): Int {
@@ -58,15 +55,43 @@ class AutoType : Parcelable {
dest.writeByte((if (enabled) 1 else 0).toByte()) dest.writeByte((if (enabled) 1 else 0).toByte())
dest.writeInt(obfuscationOptions.toKotlinInt()) dest.writeInt(obfuscationOptions.toKotlinInt())
dest.writeString(defaultSequence) dest.writeString(defaultSequence)
ParcelableUtil.writeStringParcelableMap(dest, windowSeqPairs) dest.writeTypedList(windowSeqPairs)
} }
fun put(key: String, value: String) { fun add(key: String, value: String) {
windowSeqPairs[key] = value windowSeqPairs.add(AutoTypeItem(key, value))
} }
fun entrySet(): Set<MutableMap.MutableEntry<String, String>> { fun doForEachAutoTypeItem(action: (key: String, value: String) -> Unit) {
return windowSeqPairs.entries windowSeqPairs.forEach {
action.invoke(it.key, it.value)
}
}
private data class AutoTypeItem(var key: String, var value: String): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readString() ?: "") {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(key)
parcel.writeString(value)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<AutoTypeItem> {
override fun createFromParcel(parcel: Parcel): AutoTypeItem {
return AutoTypeItem(parcel)
}
override fun newArray(size: Int): Array<AutoTypeItem?> {
return arrayOfNulls(size)
}
}
} }
companion object { companion object {

View File

@@ -21,15 +21,16 @@ package com.kunzisoft.keepass.database.element.entry
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.Attachment
import java.util.* import java.util.*
import kotlin.collections.ArrayList
/** /**
* Structure containing information about one entry. * Structure containing information about one entry.
@@ -56,7 +57,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
/** A string describing what is in binaryData */ /** A string describing what is in binaryData */
var binaryDescription = "" var binaryDescription = ""
var binaryData: BinaryAttachment? = null private var binaryDataId: Int? = null
// Determine if this is a MetaStream entry // Determine if this is a MetaStream entry
val isMetaStream: Boolean val isMetaStream: Boolean
@@ -68,7 +69,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
if (username.isEmpty()) return false if (username.isEmpty()) return false
if (username != PMS_ID_USER) return false if (username != PMS_ID_USER) return false
if (url.isEmpty()) return false if (url.isEmpty()) return false
return if (url != PMS_ID_URL) false else icon.isMetaStreamIcon if (url != PMS_ID_URL) return false
return icon.standard.id == KEY_ID
} }
override fun initNodeId(): NodeId<UUID> { override fun initNodeId(): NodeId<UUID> {
@@ -88,7 +90,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
url = parcel.readString() ?: url url = parcel.readString() ?: url
notes = parcel.readString() ?: notes notes = parcel.readString() ?: notes
binaryDescription = parcel.readString() ?: binaryDescription binaryDescription = parcel.readString() ?: binaryDescription
binaryData = parcel.readParcelable(BinaryAttachment::class.java.classLoader) val rawBinaryDataId = parcel.readInt()
binaryDataId = if (rawBinaryDataId == -1) null else rawBinaryDataId
} }
override fun readParentParcelable(parcel: Parcel): GroupKDB? { override fun readParentParcelable(parcel: Parcel): GroupKDB? {
@@ -107,7 +110,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
dest.writeString(url) dest.writeString(url)
dest.writeString(notes) dest.writeString(notes)
dest.writeString(binaryDescription) dest.writeString(binaryDescription)
dest.writeParcelable(binaryData, flags) dest.writeInt(binaryDataId ?: -1)
} }
fun updateWith(source: EntryKDB) { fun updateWith(source: EntryKDB) {
@@ -118,7 +121,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
url = source.url url = source.url
notes = source.notes notes = source.notes
binaryDescription = source.binaryDescription binaryDescription = source.binaryDescription
binaryData = source.binaryData binaryDataId = source.binaryDataId
} }
override var username = "" override var username = ""
@@ -137,26 +140,39 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
override val type: Type override val type: Type
get() = Type.ENTRY get() = Type.ENTRY
fun getAttachment(): Attachment? { fun getAttachment(attachmentPool: AttachmentPool): Attachment? {
val binary = binaryData binaryDataId?.let { poolId ->
return if (binary != null) attachmentPool[poolId]?.let { binary ->
Attachment(binaryDescription, binary) return Attachment(binaryDescription, binary)
else null }
}
return null
} }
fun containsAttachment(): Boolean { fun containsAttachment(): Boolean {
return binaryData != null return binaryDataId != null
} }
fun putAttachment(attachment: Attachment) { fun getBinary(attachmentPool: AttachmentPool): BinaryData? {
this.binaryDataId?.let {
return attachmentPool[it]
}
return null
}
fun putBinary(binaryData: BinaryData, attachmentPool: AttachmentPool) {
this.binaryDataId = attachmentPool.put(binaryData)
}
fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
this.binaryDescription = attachment.name this.binaryDescription = attachment.name
this.binaryData = attachment.binaryAttachment this.binaryDataId = attachmentPool.put(attachment.binaryData)
} }
fun removeAttachment(attachment: Attachment? = null) { fun removeAttachment(attachment: Attachment? = null) {
if (attachment == null || this.binaryDescription == attachment.name) { if (attachment == null || this.binaryDescription == attachment.name) {
this.binaryDescription = "" this.binaryDescription = ""
this.binaryData = null this.binaryDataId = null
} }
} }

View File

@@ -20,15 +20,16 @@
package com.kunzisoft.keepass.database.element.entry package com.kunzisoft.keepass.database.element.entry
import android.os.Parcel import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment 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.DateInstant
import com.kunzisoft.keepass.database.element.database.BinaryPool 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.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
@@ -48,93 +49,66 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
@Transient @Transient
private var mDecodeRef = false private var mDecodeRef = false
override var icon: IconImage override var usageCount = UnsignedLong(0)
get() { override var locationChanged = DateInstant()
return when { override var customData = CustomData()
iconCustom.isUnknown -> super.icon
else -> iconCustom
}
}
set(value) {
if (value is IconImageStandard)
iconCustom = IconImageCustom.UNKNOWN_ICON
super.icon = value
}
var iconCustom = IconImageCustom.UNKNOWN_ICON
var customData = LinkedHashMap<String, String>()
var fields = LinkedHashMap<String, ProtectedString>() var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId> var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
var foregroundColor = "" var foregroundColor = ""
var backgroundColor = "" var backgroundColor = ""
var overrideURL = "" var overrideURL = ""
override var tags = Tags()
override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
var qualityCheck = true
var autoType = AutoType() var autoType = AutoType()
var history = ArrayList<EntryKDBX>() var history = ArrayList<EntryKDBX>()
var additional = "" var additional = ""
var tags = ""
fun getSize(binaryPool: BinaryPool): Long {
var size = FIXED_LENGTH_SIZE
for (entry in fields.entries) {
size += entry.key.length.toLong()
size += entry.value.length().toLong()
}
size += getAttachmentsSize(binaryPool)
size += autoType.defaultSequence.length.toLong()
for ((key, value) in autoType.entrySet()) {
size += key.length.toLong()
size += value.length.toLong()
}
for (entry in history) {
size += entry.getSize(binaryPool)
}
size += overrideURL.length.toLong()
size += tags.length.toLong()
return size
}
override var expires: Boolean = false override var expires: Boolean = false
constructor() : super() constructor() : super()
constructor(parcel: Parcel) : super(parcel) { constructor(parcel: Parcel) : super(parcel) {
iconCustom = parcel.readParcelable(IconImageCustom::class.java.classLoader) ?: iconCustom
usageCount = UnsignedLong(parcel.readLong()) usageCount = UnsignedLong(parcel.readLong())
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
customData = ParcelableUtil.readStringParcelableMap(parcel) customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData()
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java) fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
binaries = ParcelableUtil.readStringIntMap(parcel) binaries = ParcelableUtil.readStringIntMap(parcel)
foregroundColor = parcel.readString() ?: foregroundColor foregroundColor = parcel.readString() ?: foregroundColor
backgroundColor = parcel.readString() ?: backgroundColor backgroundColor = parcel.readString() ?: backgroundColor
overrideURL = parcel.readString() ?: overrideURL 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 autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType
parcel.readTypedList(history, CREATOR) parcel.readTypedList(history, CREATOR)
url = parcel.readString() ?: url url = parcel.readString() ?: url
additional = parcel.readString() ?: additional additional = parcel.readString() ?: additional
tags = parcel.readString() ?: tags }
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) { override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags) super.writeToParcel(dest, flags)
dest.writeParcelable(iconCustom, flags)
dest.writeLong(usageCount.toKotlinLong()) dest.writeLong(usageCount.toKotlinLong())
dest.writeParcelable(locationChanged, flags) dest.writeParcelable(locationChanged, flags)
ParcelableUtil.writeStringParcelableMap(dest, customData) dest.writeParcelable(customData, flags)
ParcelableUtil.writeStringParcelableMap(dest, flags, fields) ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
ParcelableUtil.writeStringIntMap(dest, binaries) ParcelableUtil.writeStringIntMap(dest, binaries)
dest.writeString(foregroundColor) dest.writeString(foregroundColor)
dest.writeString(backgroundColor) dest.writeString(backgroundColor)
dest.writeString(overrideURL) dest.writeString(overrideURL)
dest.writeParcelable(tags, flags)
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
dest.writeParcelable(autoType, flags) dest.writeParcelable(autoType, flags)
dest.writeTypedList(history) dest.writeTypedList(history)
dest.writeString(url) dest.writeString(url)
dest.writeString(additional) dest.writeString(additional)
dest.writeString(tags)
} }
/** /**
@@ -143,12 +117,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
*/ */
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) { fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
super.updateWith(source) super.updateWith(source)
iconCustom = IconImageCustom(source.iconCustom)
usageCount = source.usageCount usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged) locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map customData = CustomData(source.customData)
customData.clear()
customData.putAll(source.customData)
fields.clear() fields.clear()
fields.putAll(source.fields) fields.putAll(source.fields)
binaries.clear() binaries.clear()
@@ -156,13 +127,14 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
foregroundColor = source.foregroundColor foregroundColor = source.foregroundColor
backgroundColor = source.backgroundColor backgroundColor = source.backgroundColor
overrideURL = source.overrideURL overrideURL = source.overrideURL
tags = source.tags
previousParentGroup = source.previousParentGroup
autoType = AutoType(source.autoType) autoType = AutoType(source.autoType)
history.clear() history.clear()
if (copyHistory) if (copyHistory)
history.addAll(source.history) history.addAll(source.history)
url = source.url url = source.url
additional = source.additional additional = source.additional
tags = source.tags
} }
fun startToManageFieldReferences(database: DatabaseKDBX) { fun startToManageFieldReferences(database: DatabaseKDBX) {
@@ -183,13 +155,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return NodeIdUUID(nodeId.id) return NodeIdUUID(nodeId.id)
} }
override fun readParentParcelable(parcel: Parcel): GroupKDBX? { override val type: Type
return parcel.readParcelable(GroupKDBX::class.java.classLoader) get() = Type.ENTRY
}
override fun writeParentParcelable(parent: GroupKDBX?, parcel: Parcel, flags: Int) {
parcel.writeParcelable(parent, flags)
}
/** /**
* Decode a reference key with the FieldReferencesEngine * Decode a reference key with the FieldReferencesEngine
@@ -197,55 +164,94 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
* @param key * @param key
* @return * @return
*/ */
private fun decodeRefKey(decodeRef: Boolean, key: String): String { private fun decodeRefKey(decodeRef: Boolean, key: String, recursionLevel: Int): String {
return fields[key]?.toString()?.let { text -> return fields[key]?.toString()?.let { text ->
return if (decodeRef) { return if (decodeRef) {
if (mDatabase == null) text else FieldReferencesEngine().compile(text, this, mDatabase!!) mDatabase?.getFieldReferenceValue(text, recursionLevel) ?: text
} else text } else text
} ?: "" } ?: ""
} }
fun decodeTitleKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_TITLE, recursionLevel)
}
override var title: String override var title: String
get() = decodeRefKey(mDecodeRef, STR_TITLE) get() = decodeTitleKey(0)
set(value) { set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectTitle val protect = mDatabase != null && mDatabase!!.memoryProtection.protectTitle
fields[STR_TITLE] = ProtectedString(protect, value) fields[STR_TITLE] = ProtectedString(protect, value)
} }
override val type: Type fun decodeUsernameKey(recursionLevel: Int): String {
get() = Type.ENTRY return decodeRefKey(mDecodeRef, STR_USERNAME, recursionLevel)
}
override var username: String override var username: String
get() = decodeRefKey(mDecodeRef, STR_USERNAME) get() = decodeUsernameKey(0)
set(value) { set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUserName val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUserName
fields[STR_USERNAME] = ProtectedString(protect, value) fields[STR_USERNAME] = ProtectedString(protect, value)
} }
fun decodePasswordKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_PASSWORD, recursionLevel)
}
override var password: String override var password: String
get() = decodeRefKey(mDecodeRef, STR_PASSWORD) get() = decodePasswordKey(0)
set(value) { set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectPassword val protect = mDatabase != null && mDatabase!!.memoryProtection.protectPassword
fields[STR_PASSWORD] = ProtectedString(protect, value) fields[STR_PASSWORD] = ProtectedString(protect, value)
} }
fun decodeUrlKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_URL, recursionLevel)
}
override var url override var url
get() = decodeRefKey(mDecodeRef, STR_URL) get() = decodeUrlKey(0)
set(value) { set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUrl val protect = mDatabase != null && mDatabase!!.memoryProtection.protectUrl
fields[STR_URL] = ProtectedString(protect, value) fields[STR_URL] = ProtectedString(protect, value)
} }
fun decodeNotesKey(recursionLevel: Int): String {
return decodeRefKey(mDecodeRef, STR_NOTES, recursionLevel)
}
override var notes: String override var notes: String
get() = decodeRefKey(mDecodeRef, STR_NOTES) get() = decodeNotesKey(0)
set(value) { set(value) {
val protect = mDatabase != null && mDatabase!!.memoryProtection.protectNotes val protect = mDatabase != null && mDatabase!!.memoryProtection.protectNotes
fields[STR_NOTES] = ProtectedString(protect, value) fields[STR_NOTES] = ProtectedString(protect, value)
} }
override var usageCount = UnsignedLong(0) fun getSize(attachmentPool: AttachmentPool): Long {
var size = FIXED_LENGTH_SIZE
override var locationChanged = DateInstant() for (entry in fields.entries) {
size += entry.key.length.toLong()
size += entry.value.length().toLong()
}
size += getAttachmentsSize(attachmentPool)
size += autoType.defaultSequence.length.toLong()
autoType.doForEachAutoTypeItem { key, value ->
size += key.length.toLong()
size += value.length.toLong()
}
for (entry in history) {
size += entry.getSize(attachmentPool)
}
size += overrideURL.length.toLong()
size += tags.toString().length
return size
}
fun afterChangeParent() { fun afterChangeParent() {
locationChanged = DateInstant() locationChanged = DateInstant()
@@ -259,38 +265,45 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|| key == STR_NOTES) || key == STR_NOTES)
} }
var customFields = LinkedHashMap<String, ProtectedString>() fun doForEachDecodedCustomField(action: (key: String, value: ProtectedString) -> Unit) {
get() { val iterator = fields.entries.iterator()
field.clear() while (iterator.hasNext()) {
for ((key, value) in fields) { val mapEntry = iterator.next()
if (!isStandardField(key)) { if (!isStandardField(mapEntry.key)) {
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key)) action.invoke(mapEntry.key,
} ProtectedString(mapEntry.value.isProtected,
decodeRefKey(mDecodeRef, mapEntry.key, 0)
)
)
} }
return field
} }
}
fun getField(key: String): ProtectedString? {
return fields[key]
}
fun putField(label: String, value: ProtectedString) {
fields[label] = value
}
fun removeAllFields() { fun removeAllFields() {
fields.clear() fields.clear()
} }
fun putExtraField(label: String, value: ProtectedString) {
fields[label] = value
}
/** /**
* It's a list because history labels can be defined multiple times * It's a list because history labels can be defined multiple times
*/ */
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> { fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
val entryAttachmentList = ArrayList<Attachment>() val entryAttachmentList = ArrayList<Attachment>()
for ((label, poolId) in binaries) { for ((label, poolId) in binaries) {
binaryPool[poolId]?.let { binary -> attachmentPool[poolId]?.let { binary ->
entryAttachmentList.add(Attachment(label, binary)) entryAttachmentList.add(Attachment(label, binary))
} }
} }
if (inHistory) { if (inHistory) {
history.forEach { history.forEach {
entryAttachmentList.addAll(it.getAttachments(binaryPool, false)) entryAttachmentList.addAll(it.getAttachments(attachmentPool, false))
} }
} }
return entryAttachmentList return entryAttachmentList
@@ -300,8 +313,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return binaries.isNotEmpty() return binaries.isNotEmpty()
} }
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) { fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment) binaries[attachment.name] = attachmentPool.put(attachment.binaryData)
} }
fun removeAttachment(attachment: Attachment) { fun removeAttachment(attachment: Attachment) {
@@ -312,28 +325,20 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
binaries.clear() binaries.clear()
} }
private fun getAttachmentsSize(binaryPool: BinaryPool): Long { private fun getAttachmentsSize(attachmentPool: AttachmentPool): Long {
var size = 0L var size = 0L
for ((label, poolId) in binaries) { for ((label, poolId) in binaries) {
size += label.length.toLong() size += label.length.toLong()
size += binaryPool[poolId]?.length ?: 0 size += attachmentPool[poolId]?.getSize() ?: 0
} }
return size return size
} }
override fun putCustomData(key: String, value: String) {
customData[key] = value
}
override fun containsCustomData(): Boolean {
return customData.isNotEmpty()
}
fun addEntryToHistory(entry: EntryKDBX) { fun addEntryToHistory(entry: EntryKDBX) {
history.add(entry) history.add(entry)
} }
fun removeEntryFromHistory(position: Int): EntryKDBX? { fun removeEntryFromHistory(position: Int): EntryKDBX {
return history.removeAt(position) return history.removeAt(position)
} }
@@ -357,8 +362,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override fun touch(modified: Boolean, touchParents: Boolean) { override fun touch(modified: Boolean, touchParents: Boolean) {
super.touch(modified, touchParents) super.touch(modified, touchParents)
// TODO unsigned long usageCount.plusOne()
usageCount = UnsignedLong(usageCount.toKotlinLong() + 1)
} }
companion object { companion object {
@@ -369,6 +373,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
const val STR_URL = "URL" const val STR_URL = "URL"
const val STR_NOTES = "Notes" const val STR_NOTES = "Notes"
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
fun newCustomNameAllowed(name: String): Boolean { fun newCustomNameAllowed(name: String): Boolean {
return !(name.equals(STR_TITLE, true) return !(name.equals(STR_TITLE, true)
|| name.equals(STR_USERNAME, true) || name.equals(STR_USERNAME, true)
@@ -387,7 +393,5 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
return arrayOfNulls(size) return arrayOfNulls(size)
} }
} }
private const val FIXED_LENGTH_SIZE: Long = 128 // Approximate fixed length size
} }
} }

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