Compare commits

...

328 Commits

Author SHA1 Message Date
J-Jamet
32d235e8c7 Merge branch 'release/2.5beta30' 2020-03-27 18:13:21 +01:00
J-Jamet
cb982b3513 Fix strong tag 2020-03-27 17:53:42 +01:00
J-Jamet
d7ed6c26dd Merge branch 'develop' into translations 2020-03-27 17:49:29 +01:00
J-Jamet
8ff19f7e68 First string pass according to #460 2020-03-27 17:43:31 +01:00
J-Jamet
729e062c3a Add error when create database file 2020-03-27 17:12:54 +01:00
J-Jamet
7d0340ac07 Upgrade CHANGELOG 2020-03-27 16:57:35 +01:00
J-Jamet
01960e74c1 Fix check file ANR #494 2020-03-27 16:36:57 +01:00
anonymous
8e40250985 Translated using Weblate (German)
Currently translated at 96.4% (411 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-27 00:46:37 +01:00
C. Rüdinger
8ae2edb61a Translated using Weblate (German)
Currently translated at 96.4% (411 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-27 00:46:37 +01:00
J-Jamet
0baa7bcbf1 Change discard button 2020-03-26 16:36:17 +01:00
J-Jamet
fffee48918 Upgrade version to 2.5beta30 2020-03-26 14:50:20 +01:00
J-Jamet
515abb6e14 Better UUID view 2020-03-26 14:45:51 +01:00
J-Jamet
6c1c3ff87f New method to wait 1.5 seconds after screen turns off 2020-03-26 13:38:20 +01:00
J-Jamet
5b65575c7a Add comment to ignore autocomplete off 2020-03-26 12:57:24 +01:00
J-Jamet
0f258fc5f8 Upgrade autofill algorithm 2020-03-26 12:47:32 +01:00
anonymous
206bc661dc Translated using Weblate (German)
Currently translated at 96.2% (410 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-25 23:44:32 +01:00
J-Jamet
d0e35b109e Revert : Fix Nextcloud File Upload conflict #497 2020-03-25 22:48:15 +01:00
J-Jamet
61769c4f20 Fix Nextcloud File Upload conflict #497 2020-03-25 21:07:36 +01:00
Hosted Weblate
95778ee5f4 Merge branch 'origin/master' into Weblate. 2020-03-25 12:21:35 +01:00
anonymous
155030fdca Translated using Weblate (Italian)
Currently translated at 79.5% (339 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-25 12:21:35 +01:00
Filippo De Bortoli
98237ef76c Translated using Weblate (Italian)
Currently translated at 79.5% (339 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-25 12:21:34 +01:00
J-Jamet
f0a7b38199 Merge tag '2.5beta29' into develop
2.5beta29
2020-03-25 12:06:20 +01:00
J-Jamet
9fc5e6751b Merge branch 'release/2.5beta29' 2020-03-25 12:06:08 +01:00
J-Jamet
4c1630312b Add UUID color 2020-03-25 11:46:43 +01:00
J-Jamet
d397c5c996 Add UUID ref for entry 2020-03-25 11:22:19 +01:00
J-Jamet
f6c41b5a60 Replace the strong nodes 2020-03-24 20:49:33 +01:00
J-Jamet
06eb5c01c3 Update versions 2020-03-24 20:44:50 +01:00
J-Jamet
7df49f91e8 Merge branch 'release/2.5beta29' of github.com:Kunzisoft/KeePassDX into release/2.5beta29 2020-03-24 20:10:27 +01:00
J-Jamet
96dcbb0ce7 Merge branch 'develop' into release/2.5beta29 2020-03-24 20:09:47 +01:00
J-Jamet
5f828fb986 Fix setting 2020-03-24 20:09:30 +01:00
J-Jamet
533d663938 Merge branch 'translations' into develop 2020-03-24 19:44:38 +01:00
J-Jamet
ae788503a9 Fix html_about licence 2020-03-24 19:44:15 +01:00
anonymous
cf0acd9c73 Translated using Weblate (Italian)
Currently translated at 78.4% (334 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:42:45 +01:00
Filippo De Bortoli
0857f2f1cf Translated using Weblate (Italian)
Currently translated at 78.4% (334 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:42:43 +01:00
Ema Panz
c05d412bdb Translated using Weblate (Italian)
Currently translated at 71.1% (303 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:18:14 +01:00
anonymous
c8e0ce717d Translated using Weblate (Italian)
Currently translated at 71.1% (303 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:18:14 +01:00
J-Jamet
cc3485b201 Update Readme 2020-03-23 21:10:28 +01:00
J-Jamet
81ea7080c2 Show UUID as KeePass UUID 2020-03-21 20:25:37 +01:00
J-Jamet
76ff6f5ae0 Fix reference loop 2020-03-21 19:01:05 +01:00
J-Jamet
0c0d0b7a6f Encapsulate readonly for launch method 2020-03-21 17:23:17 +01:00
J-Jamet
ec8cf1f6b7 Move autofill setting up 2020-03-21 17:16:49 +01:00
J-Jamet
da44310d1b Fix focus validation button 2020-03-21 17:03:36 +01:00
J-Jamet
4bd9c84bb0 Add dialog to validate discard entry changes 2020-03-21 16:52:50 +01:00
J-Jamet
3b1269a770 Fix bind listeners 2020-03-21 16:12:07 +01:00
J-Jamet
7c986ccee8 Fix card_view_margin issue 2020-03-21 16:08:23 +01:00
J-Jamet
903bad8f36 Fix launch of open db service exception 2020-03-21 14:36:33 +01:00
J-Jamet
4b9577437c Hide add button when nodes are selected 2020-03-21 12:27:18 +01:00
J-Jamet
c316011fbc Show toast on invalid key exception 2020-03-21 12:16:45 +01:00
J-Jamet
3fb1f18c22 Education screen for OTP and entry edit menu 2020-03-19 19:47:51 +01:00
J-Jamet
53935058f5 Add OTP icon 2020-03-19 19:16:38 +01:00
J-Jamet
a3860c9581 Generate password icon as dice 2020-03-19 18:34:14 +01:00
J-Jamet
dc20899d26 Update CHANGELOG 2020-03-19 17:48:56 +01:00
J-Jamet
62ac3ddb75 Merge branch 'feature/Autofill_Improvement' into develop 2020-03-19 17:23:24 +01:00
J-Jamet
b792a61bf9 Improve hint "on" and "off" recognition 2020-03-19 15:18:06 +01:00
J-Jamet
aae9f9e1cb Warning when cancel autofill 2020-03-19 14:59:33 +01:00
J-Jamet
d098bf5e6a Upgrade Autofill algorithm 2020-03-19 14:35:22 +01:00
J-Jamet
b0e8a3ecd9 Add expiration datetime 2020-03-18 12:03:12 +01:00
J-Jamet
4efa684022 Merge branch 'feature/Edit_Expired_Date' into develop 2020-03-18 11:53:46 +01:00
J-Jamet
f2c8082990 Fix date picker kitkat issue 2020-03-18 11:53:21 +01:00
J-Jamet
1abba80045 Fix dialog theme 2020-03-18 11:45:52 +01:00
J-Jamet
68564a2b75 Merge branch 'develop' into feature/Edit_Expired_Date 2020-03-18 09:46:28 +01:00
J-Jamet
385b701b38 Fix cardview margin 2020-03-18 09:42:36 +01:00
Éfrit
11c9a1d707 Translated using Weblate (French)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-03-17 00:42:55 +01:00
J-Jamet
c5aef6b561 New entry edit tool menu style 2020-03-15 12:21:26 +01:00
J-Jamet
dfcf73cfd0 Merge branch 'develop' into feature/Edit_Expired_Date 2020-03-14 21:15:58 +01:00
J-Jamet
7fc9389700 Always show menu 2020-03-14 21:15:37 +01:00
J-Jamet
d1af7349bc Edit button as validation 2020-03-14 20:51:47 +01:00
J-Jamet
0fd955197d Expires selection 2020-03-14 20:16:36 +01:00
Destiny Li
cbf33507d1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-14 17:37:29 +01:00
WaldiS
bc60a5d97e Translated using Weblate (Polish)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-03-14 17:37:27 +01:00
zeritti
57596b2991 Translated using Weblate (Czech)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-03-14 17:37:23 +01:00
solokot
43b3602a52 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-14 17:37:22 +01:00
J-Jamet
c09ec961b8 Merge branch 'feature/Entry_Edit_BottomBar' into develop 2020-03-14 11:00:47 +01:00
J-Jamet
d140b453b2 Merge branch 'feature/Entry_Edit_BottomBar' into develop 2020-03-14 10:36:52 +01:00
J-Jamet
9eb42636ec Change read icons 2020-03-13 21:58:32 +01:00
J-Jamet
ee2d663fce New icons for entry edit tools 2020-03-13 21:33:38 +01:00
J-Jamet
8e83615a22 Add Edit Toolbar menu on top 2020-03-13 17:06:09 +01:00
J-Jamet
0ff129c5ca Edit Entry with Card View 2020-03-13 11:06:56 +01:00
WaldiS
e49858439f Translated using Weblate (Polish)
Currently translated at 99.5% (424 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-03-12 20:33:30 +01:00
zeritti
3df07f7f47 Translated using Weblate (Czech)
Currently translated at 99.5% (424 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-03-12 20:33:30 +01:00
Aurel F
ff9e179593 Translated using Weblate (Romanian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-12 20:33:28 +01:00
J-Jamet
7539fee04b Add BottomBar in EntryEdit 2020-03-12 19:15:03 +01:00
J-Jamet
71a339a58f Suppress small warning 2020-03-12 17:15:17 +01:00
J-Jamet
9ef2ea016b Suppress deprecation for Keyboard 2020-03-12 17:14:46 +01:00
J-Jamet
de4936a16d Set nullable getSystemService 2020-03-12 17:05:53 +01:00
J-Jamet
574d2b8904 Wait 3 seconds before the lock after the screen turns off #59 2020-03-12 16:56:46 +01:00
J-Jamet
1f3f7634e7 Allow empty title in entries #423 2020-03-12 14:41:02 +01:00
J-Jamet
3c0725baff Fix remember key file option 2020-03-12 14:24:33 +01:00
J-Jamet
b0e1411012 Update CHANGELOG 2020-03-12 14:20:48 +01:00
J-Jamet
39daf4714d Merge branch 'feature/Delete_Registered_Keyfile_New_Credentials' into develop 2020-03-12 14:17:35 +01:00
J-Jamet
4706afa823 Fix credentials options 2020-03-12 14:16:30 +01:00
J-Jamet
25977d389d Add DAO command to delete registered keyfile 2020-03-12 13:18:36 +01:00
J-Jamet
f760110569 default_database_path_key in strings.xml 2020-03-12 09:28:46 +01:00
J-Jamet
133e78fe97 Update CHANGELOG 2020-03-12 09:20:29 +01:00
J-Jamet
d92e0c8620 Fix magikeyboard lock 2020-03-11 19:17:51 +01:00
J-Jamet
62fdb69d6b Fix small element 2020-03-11 18:56:45 +01:00
J-Jamet
def57c9fb2 Merge branch 'feature/Lock_Database' issue 483 2020-03-11 18:42:34 +01:00
J-Jamet
21c9c898c3 Add lock timer in service, notification remains lock to capture the broadcast 2020-03-11 18:30:11 +01:00
J-Jamet
1f03c922c2 Encapsulate lock broadcast 2020-03-11 16:46:44 +01:00
J-Jamet
3f6ae6bdac Fix node update #487 2020-03-11 12:42:18 +01:00
jan madsen
60615ee1eb Translated using Weblate (Danish)
Currently translated at 98.5% (420 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-03-10 20:23:08 +01:00
J-Jamet
92b0d1bfa9 Upgrade to version 2.5beta29 2020-03-10 19:02:37 +01:00
J-Jamet
237988dc1f Merge tag '2.5beta28' into develop
2.5beta28
2020-03-10 19:00:08 +01:00
J-Jamet
a846ec29ca Merge branch 'release/2.5beta28' 2020-03-10 18:59:59 +01:00
J-Jamet
4533e96bff Upgrade CHANGELOG 2020-03-10 18:48:57 +01:00
J-Jamet
0a401c3ac9 Fix strings 2020-03-10 18:41:35 +01:00
J-Jamet
468abaf077 Fix strings 2020-03-10 18:29:19 +01:00
J-Jamet
4ccf2f641c Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into develop 2020-03-10 18:14:14 +01:00
Dominik Baláž
34eb2785cf Translated using Weblate (Slovak)
Currently translated at 21.5% (92 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2020-03-10 17:21:26 +01:00
anonymous
09dbfe323e Translated using Weblate (Slovak)
Currently translated at 21.5% (92 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2020-03-10 17:21:26 +01:00
Destiny Li
1f06c5b425 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-10 11:33:21 +01:00
Destiny Li
b98e089f7a Translated using Weblate (Chinese (Traditional))
Currently translated at 48.3% (206 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-03-10 11:33:20 +01:00
Aurel F
a0ad06ed0a Translated using Weblate (Romanian)
Currently translated at 40.8% (174 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-10 11:33:18 +01:00
solokot
ec63365429 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-10 11:33:10 +01:00
J-Jamet
2cb85e4346 Update items after appearance change #452 2020-03-10 08:58:22 +01:00
J-Jamet
0d7c479c51 Merge branch 'feature/Migration_SDK_29' into develop 2020-03-10 08:40:53 +01:00
J-Jamet
5a6c21e662 Try to fix read only bug #480 2020-03-10 08:38:49 +01:00
J-Jamet
d6cadac98f Fix small warning 2020-03-08 13:22:11 +01:00
J-Jamet
dac2fc2c37 Use PreferenceManager from AndroidX 2020-03-08 13:10:16 +01:00
J-Jamet
0fb45cef0d Encapsulate preferences 2020-03-08 13:04:25 +01:00
anonymous
5ebdbd4003 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-08 12:08:21 +01:00
Destiny Li
b30f1023cb Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-08 12:08:21 +01:00
J-Jamet
e5f65a4d1e Fix % issue in string 2020-03-08 11:48:47 +01:00
J-Jamet
ab42a65aa4 Fix null issues 2020-03-08 11:41:13 +01:00
J-Jamet
e351456bfe First pass migration SDK 29 2020-03-08 11:03:21 +01:00
J-Jamet
452e68b08f Fix C cast warning 2020-03-08 10:52:29 +01:00
J-Jamet
d65beed7a1 Fix small warnings 2020-03-08 10:49:06 +01:00
J-Jamet
f5a5a0e8cb Add generated JSON for database 2020-03-08 10:41:52 +01:00
solokot
98380a0906 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-08 10:20:37 +01:00
J-Jamet
22fe7508f3 Fix doForEachChild warning 2020-03-08 10:16:31 +01:00
J-Jamet
c8e241fc76 Fix Room incremental warning 2020-03-08 09:50:32 +01:00
J-Jamet
2c943e00d0 Fix writeEnum warning 2020-03-08 09:50:07 +01:00
J-Jamet
7ddb83b72d Fix DateFormatter warning 2020-03-08 09:36:45 +01:00
Kunzisoft
50bac01699 Translated using Weblate (French)
Currently translated at 99.7% (425 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-03-08 09:33:23 +01:00
Aurel F
e0a92dfadd Translated using Weblate (Romanian)
Currently translated at 12.2% (52 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-08 09:33:22 +01:00
solokot
b50c951091 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-08 09:33:17 +01:00
Kunzisoft
2f589a95a9 Translated using Weblate (English)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-03-08 09:33:15 +01:00
J-Jamet
53eac86a95 Update AndroidX dependencies and fix small warning 2020-03-08 09:17:18 +01:00
J-Jamet
9412f8955e Update kotlin to 1.3.61 2020-03-08 09:07:14 +01:00
J-Jamet
71c98d82b1 Update gradle to 5.6.4 2020-03-08 09:00:56 +01:00
J-Jamet
42c1a925b4 Upgrade app to 2.54beta28 2020-03-08 08:59:41 +01:00
J-Jamet
1e84534ffd Merge tag '2.5beta27' into develop
2.5beta27
2020-03-07 14:41:37 +01:00
J-Jamet
b087733e37 Merge branch 'release/2.5beta27' 2020-03-07 14:41:29 +01:00
J-Jamet
675efe29ac Fix unused string 2020-03-07 14:31:08 +01:00
J-Jamet
9833af8225 Update french translations 2020-03-07 14:13:10 +01:00
J-Jamet
790c624571 Fix strong html 2020-03-07 14:06:50 +01:00
J-Jamet
67a70a8453 Merge branch 'translations' into develop 2020-03-07 13:57:47 +01:00
J-Jamet
64bb05e2dd Update CHANGELOG 2020-03-07 13:29:09 +01:00
J-Jamet
c111db6e73 Rename preference keys 2020-03-07 13:25:53 +01:00
J-Jamet
24c5915bd3 Setting to request a search when opening a database #220 2020-03-07 13:18:45 +01:00
J-Jamet
04c3717618 Remove screenshot from bug report 2020-03-07 12:41:57 +01:00
J-Jamet
78275a0984 Fix search empty list 2020-03-07 12:37:46 +01:00
J-Jamet
bd908ed10d Update CHANGELOG 2020-03-07 12:01:03 +01:00
J-Jamet
5473deec95 Show URL before username when title is empty #463 2020-03-07 11:58:47 +01:00
Ettore Atalan
653258afd2 Translated using Weblate (German)
Currently translated at 99.7% (418 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-07 03:33:06 +01:00
J-Jamet
00a2210eea Fix button menu color 2020-03-06 17:36:02 +01:00
J-Jamet
ec5688a013 Remove autofill setting for incompatible devices 2020-03-06 17:29:48 +01:00
J-Jamet
fc0c7b5708 Update CHANGELOG 2020-03-06 15:54:02 +01:00
J-Jamet
464f7ac486 Sorted list and comparator improvement 2020-03-06 15:46:08 +01:00
solokot
fb2457146c Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-29 19:33:06 +01:00
solokot
d90870838e Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-28 19:15:38 +01:00
Andrew
8c45f23291 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-28 19:15:37 +01:00
Kunzisoft
315a3efa52 Added translation using Weblate (Romanian) 2020-02-27 14:01:57 +01:00
J-Jamet
28abb5ae6f Delete password from starting event #461 2020-02-27 13:26:41 +01:00
J-Jamet
bb78d89682 Try to fix aliased text #414 2020-02-27 13:17:01 +01:00
J-Jamet
fd36e19168 Update CHANGELOG 2020-02-27 12:57:31 +01:00
J-Jamet
58db516e44 Fix refresh appearance settings #452 #451 2020-02-27 12:54:47 +01:00
J-Jamet
2b875e94dc Add GZip string to attachment view if database is compressed 2020-02-27 12:38:16 +01:00
J-Jamet
0ba2447f55 Change about page 2020-02-27 10:16:43 +01:00
solokot
8e684d0d3a Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-25 23:32:46 +01:00
MrEnderfall
ce3c1d4685 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-25 23:32:46 +01:00
J-Jamet
2cd77d47eb Move up remember keyfile locations setting 2020-02-23 13:43:09 +01:00
J-Jamet
ac9366e351 Update CHANGELOGS 2020-02-23 13:39:57 +01:00
J-Jamet
af356586f8 Merge branch 'feature/Check_Broken_Link' into develop 2020-02-23 13:33:56 +01:00
J-Jamet
7650db81a4 Hide broken database links 2020-02-23 13:15:09 +01:00
J-Jamet
8925c86afd Fix remember key file after press back 2020-02-23 12:49:40 +01:00
J-Jamet
8f1836009e Fix database and keyfile locations settings 2020-02-23 12:39:39 +01:00
J-Jamet
5fcf3f9b95 Fix erasing file 2020-02-23 10:47:28 +01:00
Kunzisoft
1c466d9e40 Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-22 17:33:01 +01:00
WaldiS
b97c2d9cbc Translated using Weblate (Polish)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-02-22 17:33:01 +01:00
zeritti
3d970e4967 Translated using Weblate (Czech)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-02-22 17:33:01 +01:00
J-Jamet
765c8f53dd Remove permission variable and rename preference 2020-02-20 19:53:49 +01:00
J-Jamet
4e81caeadf Remove unused external storage permission for Sdk > 18 2020-02-20 19:41:48 +01:00
J-Jamet
08b10e4d58 Update string 2020-02-20 19:35:52 +01:00
J-Jamet
f8594f72e8 Fix small elements 2020-02-20 19:31:44 +01:00
J-Jamet
24ebee07cd Fix small elements 2020-02-20 19:26:03 +01:00
J-Jamet
a06ebb0991 Force read only when file is not writable 2020-02-20 19:20:00 +01:00
J-Jamet
cd5d4498e7 Fix small element 2020-02-20 16:11:50 +01:00
J-Jamet
0bd62780c6 Check if uri is broken 2020-02-20 14:13:11 +01:00
J-Jamet
896f9327d6 Fix error messages #431 2020-02-20 11:58:00 +01:00
J-Jamet
cc83a99efe Try to fix #465 2020-02-20 09:06:11 +01:00
J Smith
b6da20fef7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-02-19 23:53:44 +01:00
Kunzisoft
2912678559 Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-19 23:53:38 +01:00
WaldiS
409e870ef0 Translated using Weblate (Polish)
Currently translated at 99.5% (417 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-02-19 23:53:38 +01:00
Kunzisoft
3d84074a0a Translated using Weblate (English)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-02-19 23:53:37 +01:00
Oğuz Ersen
a717fdfed4 Translated using Weblate (Turkish)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-02-19 23:53:36 +01:00
J-Jamet
be1f68015b Merge branch 'master' into develop 2020-02-17 19:05:23 +01:00
WaldiS
2c29dcf1f6 Translated using Weblate (Polish)
Currently translated at 99.0% (415 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-02-17 00:50:57 +01:00
Filip Miletic
2b1173177f Translated using Weblate (Croatian)
Currently translated at 79.4% (333 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-02-17 00:50:55 +01:00
jan madsen
3c0f7dc79c Translated using Weblate (Danish)
Currently translated at 98.8% (414 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-02-17 00:50:42 +01:00
marzzzello
39d813bf3a Translated using Weblate (German)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-02-17 00:50:40 +01:00
Filip Miletic
5e2bc0d05b Translated using Weblate (Croatian)
Currently translated at 51.3% (215 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-02-14 04:18:25 +01:00
iLocIT!
ec751159ae Translated using Weblate (German)
Currently translated at 89.5% (375 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-02-14 04:18:02 +01:00
Filip Miletic
dda8b95f83 Translated using Weblate (Croatian)
Currently translated at 11.0% (46 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-02-13 19:31:33 +01:00
Filip Miletic
286012fe2a Added translation using Weblate (Croatian) 2020-02-13 11:04:02 +01:00
Jérémy JAMET
ab27299789 Update description for other KeePass app 2020-02-12 17:09:14 +01:00
Éfrit
dfcdd5aa88 Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-11 21:50:45 +01:00
Allan Nordhøy
a05ea52689 Translated using Weblate (Italian)
Currently translated at 72.8% (305 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-02-11 21:50:45 +01:00
zeritti
de12b5de5b Translated using Weblate (Czech)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-02-11 21:50:44 +01:00
solokot
57b03eaca4 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-11 21:50:43 +01:00
Retrial
36bd00b760 Translated using Weblate (Greek)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-02-11 21:50:42 +01:00
Oğuz Ersen
4578a9974a Translated using Weblate (Turkish)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-02-11 21:50:33 +01:00
J-Jamet
d0371f58c6 Capture exception to avoid crash after clear clipboard 2020-02-08 17:39:08 +01:00
solokot
9f80457351 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-08 16:39:00 +01:00
J-Jamet
56daf6f676 Remove JVMOverload in ClipboardHelper 2020-02-08 12:09:12 +01:00
J-Jamet
7f7b8d423b Merge branch 'translations' into develop 2020-02-08 11:34:35 +01:00
J-Jamet
031afc80cb Fix contribution strings 2020-02-08 11:32:56 +01:00
J-Jamet
b77e28b04d Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-02-08 11:27:36 +01:00
Allan Nordhøy
2f8c3fdcfe Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.4% (341 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-02-08 11:27:15 +01:00
solokot
f501a87099 Translated using Weblate (Russian)
Currently translated at 99.8% (418 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-08 11:27:14 +01:00
Kunzisoft
d0ec5f26dd Translated using Weblate (English)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-02-08 11:27:13 +01:00
Mesut Akcan
97776e9329 Translated using Weblate (Turkish)
Currently translated at 98.3% (412 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-02-08 11:27:11 +01:00
Kunzisoft
a19356c49e Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-08 11:21:45 +01:00
J-Jamet
4f16918cf0 Remove unused strings 2020-02-08 11:19:45 +01:00
J-Jamet
1af9761144 Merge branch 'master' into develop 2020-02-07 22:14:20 +01:00
J-Jamet
d74e814c79 Fix fastlane data 2020-02-07 22:05:58 +01:00
J-Jamet
16d09bca6c Merge tag '2.5.0.0beta26' into develop
2.5.0.0beta26
2020-02-07 21:45:51 +01:00
J-Jamet
462c29b769 Merge branch 'release/2.5.0.0beta26' 2020-02-07 21:45:40 +01:00
J-Jamet
3939276d58 Fix translations 2020-02-07 19:44:21 +01:00
J-Jamet
ff85f18c4c Update ReadMe.md 2020-02-07 19:24:12 +01:00
J-Jamet
dd14fe4123 Fix exception after changing theme 2020-02-07 19:20:43 +01:00
J-Jamet
d804659ee2 Change blue color of classic dark style 2020-02-07 19:10:48 +01:00
J-Jamet
e3152cf901 Remove AMOLED string for Black theme 2020-02-07 18:57:07 +01:00
J-Jamet
de236f321f Upgrade CHANGELOG 2020-02-07 18:26:27 +01:00
J-Jamet
66f4611866 Restore and delete entry history #335 2020-02-07 18:22:37 +01:00
heta1121
6a6ef052af Translated using Weblate (Swedish)
Currently translated at 94.9% (388 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2020-02-06 19:07:41 +01:00
J-Jamet
4e4606dabd Fix strings #436 2020-01-31 20:14:53 +01:00
J-Jamet
0ce11103ab Update CHANGELOG 2020-01-31 19:33:24 +01:00
J-Jamet
899d0e0557 Fix expired entries views 2020-01-31 19:29:40 +01:00
J-Jamet
4aefeff41f Hide expired entries from quick search when setting is activated #453 2020-01-31 17:26:36 +01:00
J-Jamet
08d59e50e8 Add android:resizeableActivity="true" for DeX mode 2020-01-31 16:48:15 +01:00
J-Jamet
e6c06aba8c Merge branch 'qtwyeuritoiy-feature/amoled_black' into develop 2020-01-31 13:12:37 +01:00
J-Jamet
e5c2a04922 Merge branch 'feature/amoled_black' of git://github.com/qtwyeuritoiy/KeePassDX into qtwyeuritoiy-feature/amoled_black 2020-01-31 13:12:18 +01:00
J-Jamet
c134ccf8d9 Encapsulate filters 2020-01-29 20:20:39 +01:00
J-Jamet
4f959d1ff6 Merge branch 'feature/hide_expired_entries' of git://github.com/qtwyeuritoiy/KeePassDX into qtwyeuritoiy-feature/hide_expired_entries 2020-01-29 17:22:40 +01:00
J-Jamet
1c06d93951 Fix custom fields in Magikeyboard 2020-01-29 17:10:26 +01:00
J-Jamet
b2dff29baa Auto performed "Go" key in Magikeyboard 2020-01-29 14:13:13 +01:00
J-Jamet
72633e9472 Change KEYCODE_ENTER by IME_ACTION_GO #369 2020-01-29 13:34:32 +01:00
J-Jamet
898a88f7d8 Fix app description 2020-01-27 12:52:46 +01:00
J-Jamet
c2f09f6666 Change "KeePass DX" name to "KeePassDX" and fix small issues #411 2020-01-27 12:43:45 +01:00
J-Jamet
d45bcbc347 Fix empty space in entry content 2020-01-27 11:18:54 +01:00
Jihoon Kim
1f834567f8 [Feature/HideExpiredEntries] Move 'Hide Expired Entries' Switch to 'Other' Category
Signed-off-by: Jihoon Kim <imsesaok@tuta.io>
2020-01-27 17:39:26 +08:00
Jihoon Kim
0b62c04867 [Feature/AmoledBlack] Add AMOLED Black Theme
Signed-off-by: Jihoon Kim <imsesaok@tuta.io>
2020-01-27 17:00:24 +08:00
J-Jamet
c71bb2a570 Password display box multiline #225 2020-01-26 18:02:11 +01:00
J-Jamet
0a575b5bf8 Prevent capture for all screens 2020-01-26 15:47:50 +01:00
J-Jamet
6f2bf903c5 Fix storage permission 2020-01-26 13:26:09 +01:00
Rodrigo Saldaña
210ad4b8db Translated using Weblate (Spanish)
Currently translated at 88.5% (362 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2020-01-25 19:21:30 +01:00
Allan Nordhøy
b42cf0e204 Translated using Weblate (Norwegian Bokmål)
Currently translated at 87.5% (358 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-01-23 09:21:33 +01:00
solokot
2045d3aab8 Translated using Weblate (Russian)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-01-23 09:21:32 +01:00
Allan Nordhøy
e20cb9431d Translated using Weblate (Danish)
Currently translated at 95.6% (391 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-01-23 09:21:31 +01:00
J-Jamet
7161eaea8b Allow write permission for specific file managers 2020-01-22 16:37:38 +01:00
J-Jamet
b786da52f5 Add error snackbar for error in entry edit view #431 2020-01-22 13:01:15 +01:00
J-Jamet
ccbfec838d Fix disclaimer formal string 2020-01-22 12:18:19 +01:00
J-Jamet
f5c1872225 Fix show read only warning 2020-01-22 10:53:24 +01:00
heta1121
3001013bad Translated using Weblate (Swedish)
Currently translated at 85.6% (350 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2020-01-21 17:21:30 +01:00
Allan Nordhøy
1280803e5e Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.9% (343 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-01-19 15:22:00 +01:00
heta1121
f84fc07fe0 Translated using Weblate (Swedish)
Currently translated at 85.1% (348 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2020-01-19 15:22:00 +01:00
J-Jamet
d83e53f589 Fix read only for recycle bin 2020-01-19 12:17:27 +01:00
J-Jamet
52ba2c53f1 Fix progress for Android < 5 2020-01-18 15:23:51 +01:00
J-Jamet
f266e1de4c Add string to open the attachment downloaded 2020-01-18 12:24:21 +01:00
J-Jamet
5a0aafd3ce Update CHANGELOG 2020-01-18 12:19:56 +01:00
J-Jamet
c7ce07a43c Change file size format 2020-01-18 12:15:14 +01:00
J-Jamet
a73644c285 Merge branch 'feature/Stream_Binaries' into develop 2020-01-18 12:03:55 +01:00
J-Jamet
e28f1ffc99 Fix attachment orientation change 2020-01-18 11:33:17 +01:00
Deleted User
afc691b2f4 Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.9% (343 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-01-16 06:27:50 +01:00
J-Jamet
00f6eb83c3 Fix close notification 2020-01-15 05:52:32 +01:00
J-Jamet
bbf51ebbec Multiple attachments download and add file compression 2020-01-14 19:44:10 +01:00
Jihoon Kim
7816c8f16e [Feature/HideExpiredEntries] Apply 'Hide Expired Entries' Setting And Adjust Item Count Accordingly
Signed-off-by: Jihoon Kim <imsesaok@tuta.io>
2020-01-14 17:28:27 +08:00
Jihoon Kim
fdbcba2412 [Feature/HideExpiredEntries] Add Preference Entry for 'Hide Expired Entries'
Signed-off-by: Jihoon Kim <imsesaok@tuta.io>
2020-01-14 17:28:27 +08:00
J-Jamet
50dc6cb0aa Fix download attachment 2020-01-13 10:05:34 +01:00
J-Jamet
e413198d12 Download attachment dynamic string 2020-01-10 19:40:00 +01:00
solokot
72592772f9 Translated using Weblate (Russian)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-01-09 09:21:27 +01:00
solokot
f918206bcd Translated using Weblate (Russian)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-01-07 22:27:33 +01:00
J-Jamet
6b34d67da8 Remove WRITE_EXTERNAL_STORAGE for Android > 18
Download attachment as service
2020-01-07 19:43:41 +01:00
Retrial
04fb093d4d Translated using Weblate (Greek)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-01-04 18:21:28 +01:00
J-Jamet
7de0e0cc4a Add Attachment view 2019-12-31 14:34:51 +01:00
J-Jamet
5cf7688e38 Merge branch 'develop' into feature/Stream_Binaries 2019-12-31 12:05:48 +01:00
J-Jamet
97cd06b52b Merge branch 'feature/Duplicate_UUID_Database1' into develop 2019-12-31 11:23:44 +01:00
J-Jamet
6d522ead1d Fix illegal argument exception and refactor KDB parser code 2019-12-31 08:26:15 +01:00
J-Jamet
8e0fae62f3 Merge branch 'develop' into feature/Stream_Binaries 2019-12-30 14:09:05 +01:00
J-Jamet
af72098d60 Refactor bytes utility methods 2019-12-30 12:59:50 +01:00
Taeko Hasegawa
62da582f4e Translated using Weblate (Japanese)
Currently translated at 36.4% (149 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2019-12-23 14:21:24 +01:00
J-Jamet
f3efa6eddc Add parameter to fix duplicate UUID in DatabaseV1 2019-12-22 18:26:35 +01:00
J-Jamet
c85fb7bd0a Better element output KDB implementation 2019-12-22 18:13:26 +01:00
J-Jamet
fc7b183461 Update CHANGELOG 2019-12-22 17:51:48 +01:00
J-Jamet
a378810f88 Improvement code for ClipboardHelper 2019-12-21 21:36:33 +01:00
J-Jamet
2f5b322fad Fix crash during clean clipboard in Android 9 2019-12-21 21:31:12 +01:00
J-Jamet
efb9b50f85 Fix issue #127 KDBX binaries 2019-12-21 21:18:58 +01:00
J-Jamet
84fdef8eb6 Merge branch 'develop' into feature/Stream_Binaries 2019-12-21 19:46:05 +01:00
J-Jamet
76050a261b Update to version 2.5.0.0beta26 2019-12-21 19:16:30 +01:00
Éfrit
2667361450 Translated using Weblate (French)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-12-19 17:18:38 +01:00
Éfrit
a2b3313cc0 Translated using Weblate (French)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-12-18 22:50:52 +01:00
J-Jamet
d343446235 Check file size to not corrupt a database v1 if too high 2019-12-18 18:58:26 +01:00
J-Jamet
cbce70f8a4 Save KDB binaries as file 2019-12-18 18:46:50 +01:00
WaldiS
4573d6b6fb Translated using Weblate (Polish)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-12-18 14:21:21 +01:00
Chen Wang
973305d13c Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (406 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2019-12-16 08:21:13 +01:00
solokot
4637016372 Translated using Weblate (Russian)
Currently translated at 99.8% (408 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2019-12-16 08:21:12 +01:00
Chen Wang
698ba4f7f1 Translated using Weblate (English)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-12-16 08:21:11 +01:00
Oğuz Ersen
502ebabf1f Translated using Weblate (Turkish)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-12-16 08:21:10 +01:00
WaldiS
13f88626ca Translated using Weblate (Polish)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-12-08 15:13:26 +01:00
zeritti
a39f58f7b5 Translated using Weblate (Czech)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2019-12-08 15:13:25 +01:00
nautilusx
aeb36468ce Translated using Weblate (German)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-12-08 15:13:23 +01:00
J-Jamet
fdd196526d Better InputStream / OutputStream close 2019-12-05 19:28:54 +01:00
Mattias Münster
ecca892b16 Translated using Weblate (Swedish)
Currently translated at 25.9% (106 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2019-12-04 02:05:37 +01:00
Rudah Ximenes Alvarenga
03343d0301 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2019-12-04 02:05:36 +01:00
Allan Nordhøy
a61c8b0cb6 Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.6% (342 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-12-02 03:33:26 +01:00
Kunzisoft
a92129da00 Translated using Weblate (French)
Currently translated at 99.5% (407 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-12-02 03:33:24 +01:00
Allan Nordhøy
0b783d6390 Translated using Weblate (English)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-12-02 03:33:23 +01:00
jan madsen
9aa1a2e30e Translated using Weblate (Danish)
Currently translated at 95.8% (392 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2019-12-02 03:33:23 +01:00
C. Rüdinger
15b3c69514 Translated using Weblate (German)
Currently translated at 99.5% (407 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-12-02 03:33:21 +01:00
Oğuz Ersen
f2722e09ec Translated using Weblate (Turkish)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-12-02 03:33:21 +01:00
Deleted User
b442859be0 Translated using Weblate (German)
Currently translated at 100.0% (409 of 409 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-12-01 17:45:31 +01:00
J-Jamet
1496a2efb1 Fix database decryption with attachment 2019-11-30 19:26:26 +01:00
J-Jamet
a0edb111f0 Merge branch 'develop' into feature/Stream_Binaries 2019-11-30 18:27:21 +01:00
J-Jamet
6bcf54fe29 Merge tag '2.5.0.0beta25' into develop
2.5.0.0beta25
2019-11-30 17:43:19 +01:00
J-Jamet
eb0e496cfd Try to decrypt DatabaseV1 by InputStream 2019-11-30 17:02:09 +01:00
J-Jamet
12c2a6e99c Refactor bytes utility methods 2019-11-29 14:19:13 +01:00
J-Jamet
714433b62d Refactor stream methods 2019-11-28 16:32:16 +01:00
470 changed files with 12461 additions and 6810 deletions

View File

@@ -20,9 +20,6 @@ Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
** Keepass Database **
- Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2]

View File

@@ -1,4 +1,43 @@
KeepassDX (2.5.0.0beta25)
KeePassDX(2.5beta30)
* Fix Lock after screen off (wait 1.5 seconds)
* Upgrade autofill algorithm
* Fix ANR during file verifications
KeePassDX(2.5beta29)
* Upgrade autofill algorithm
* Delete registered KeyFile after save new credentials
* Fix title and username entry view refresh after an update
* Fix database lock request (open notification always active)
* Allow empty title in entries
* Add expiration datetime
KeePassDX(2.5beta28)
* Fix read only database
* Upgrade to Android SDK 29
KeePassDX (2.5beta27)
* New setting to hide broken links
* Show URL when title is empty
* Setting to open search field at database opening
* Fix settings for database locations
* Fix error message when database file not writable
* Fix appearance refresh settings
* Sort optimization
KeePassDX (2.5.0.0beta26)
* Download attachments
* Change file size string format
* Prevent screenshot for all screen
* Auto performed "Go" key in Magikeyboard
* Restore and delete entry history
* Setting to hide expired entries
* New Black theme
* Fix crash when clearing clipboard
* Fix attachments compressions
* Fix dates
* Fix UUID message for Database v1
KeePassDX (2.5.0.0beta25)
* Setting for Recycle Bin
* Fix Recycle bin issues
* Fix TOTP
@@ -6,7 +45,7 @@ KeepassDX (2.5.0.0beta25)
* Fix update group
* Fix OOM
KeepassDX (2.5.0.0beta24)
KeePassDX (2.5.0.0beta24)
* Add OTP (HOTP / TOTP)
* Add settings (Color, Security, Master Key)
* Show history of each entry
@@ -16,7 +55,7 @@ KeepassDX (2.5.0.0beta24)
* Open/Save database as service / Add persistent notification
* Fix settings / edit group / small bugs
KeepassDX (2.5.0.0beta23)
KeePassDX (2.5.0.0beta23)
* New, more secure database creation workflow
* Recognize more database files
* Add alias for history files (WARNING: history is erased)
@@ -25,14 +64,14 @@ KeepassDX (2.5.0.0beta23)
* Fix OOM with KeyFile
* Fix small issues
KeepassDX (2.5.0.0beta22)
KeePassDX (2.5.0.0beta22)
* Rebuild code for actions
* Add UUID as entry view
* Fix bug with natural order
* Fix number of entries in databaseV1
* New entry views
KeepassDX (2.5.0.0beta21)
KeePassDX (2.5.0.0beta21)
* Fix nested groups no longer visible in V1 databases
* Improved data import algorithm for V1 databases
* Add natural database sort
@@ -40,10 +79,10 @@ KeepassDX (2.5.0.0beta21)
* Fix button disabled with only KeyFile
* Show the number of entries in a group
KeepassDX (2.5.0.0beta20)
KeePassDX (2.5.0.0beta20)
* Fix a major bug that displays an entry history
KeepassDX (2.5.0.0beta19)
KeePassDX (2.5.0.0beta19)
* Add lock button always visible
* New connection workflow
* Code refactored in Kotlin
@@ -54,7 +93,7 @@ KeepassDX (2.5.0.0beta19)
* Fix memory when load database
* Fix small bugs
KeepassDX (2.5.0.0beta18)
KeePassDX (2.5.0.0beta18)
* New recent databases views
* New information dialog
* Custom fields for the Magikeyboard
@@ -63,10 +102,10 @@ KeepassDX (2.5.0.0beta18)
* Fix memory when opening the database
* Memory management for attachments
KeepassDX (2.5.0.0beta17)
KeePassDX (2.5.0.0beta17)
* Fix font and search
KeepassDX (2.5.0.0beta16)
KeePassDX (2.5.0.0beta16)
* New search in a single fragment
* Search suggestions
* Added the display of usernames
@@ -74,20 +113,20 @@ KeepassDX (2.5.0.0beta16)
* Fix read-only mode
* Fix parcelable / toolbar / back
KeepassDX (2.5.0.0beta15)
KeePassDX (2.5.0.0beta15)
* Read only mode
* Best group recovery for the navigation fragment
* Fix copies in notifications
* Fix orientation
* Added translations
KeepassDX (2.5.0.0beta14)
KeePassDX (2.5.0.0beta14)
* Optimize all the memory with parcelables / fix search
KeepassDX (2.5.0.0beta13)
KeePassDX (2.5.0.0beta13)
* Fix memory issue with parcelable (crash in beta12 version)
KeepassDX (2.5.0.0beta12)
KeePassDX (2.5.0.0beta12)
* Added the Magikeyboard to fill the forms (settings still in development)
* Added move and copy for groups and entries
* New navigation in a single screen / new animations between activities
@@ -100,10 +139,10 @@ KeepassDX (2.5.0.0beta12)
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
* Fix small bugs
KeepassDX (2.5.0.0beta11)
KeePassDX (2.5.0.0beta11)
* Fix crash in beta10 version
KeepassDX (2.5.0.0beta10)
KeePassDX (2.5.0.0beta10)
* Dynamically change Algorithm and Key Derivation Function in settings
* Upgrade translations
* New red volcano theme, fix classic dark theme
@@ -111,7 +150,7 @@ KeepassDX (2.5.0.0beta10)
* Update fingerprint state with checkbox
* Fix bugs
KeepassDX (2.5.0.0beta9)
KeePassDX (2.5.0.0beta9)
* Education Screens to learn how to use the app
* New designs
* New custom font for character visibility
@@ -120,9 +159,9 @@ KeepassDX (2.5.0.0beta9)
* Change setting organisation
* Pro version
KeepassDX (2.5.0.0beta8)
KeePassDX (2.5.0.0beta8)
* Hide custom entries protected
* Best management of field references (https://keepass.info/help/base/fieldrefs.html)
* Best management of field references (https://KeePass.info/help/base/fieldrefs.html)
* Change database / default settings
* Add Autofill for search
* Add sorting by last access and by creation time
@@ -130,7 +169,7 @@ KeepassDX (2.5.0.0beta8)
* Refactor old code
* Fix bugs
KeepassDX (2.5.0.0beta7)
KeePassDX (2.5.0.0beta7)
* Rebuild Notifications
* Change links to https
* Add extended Ascii (ñæËÌÂÝÜ...)
@@ -139,10 +178,10 @@ KeepassDX (2.5.0.0beta7)
* Add setting to prevent the password copy
* Fix bugs
KeepassDX (2.5.0.0beta6)
KeePassDX (2.5.0.0beta6)
* Fix crash
KeepassDX (2.5.0.0beta5)
KeePassDX (2.5.0.0beta5)
* Autofill (Android O)
* Deletion for group
* New sorts with (Asc/Dsc, Groups before or after)
@@ -163,7 +202,7 @@ KeepassDX (2.5.0.0beta5)
* Fix many small bugs
* Add recycle bin setting (not yet accessible)
KeepassDX (2.5.0.0beta4)
KeePassDX (2.5.0.0beta4)
* Show only file name
* Setting for full path
* Add information for each database file
@@ -172,7 +211,7 @@ KeepassDX (2.5.0.0beta4)
* Delete view assignment for fingerprint opening
* Merge KeePassDroid 2.2.1
KeepassDX (2.5.0.0beta3)
KeePassDX (2.5.0.0beta3)
* New database workflow with new screens and folder selection
* Settings for default password generation
* Fingerprint dialog for explanations
@@ -183,17 +222,17 @@ KeepassDX (2.5.0.0beta3)
* Merge KeePassDroid 2.2.0.9
* Add corruption fix mode
KeepassDX (2.5.0.0beta2)
KeePassDX (2.5.0.0beta2)
* Remove libs for F-Droid
KeepassDX (2.5.0.0beta1)
* Fork KeepassDroid
KeePassDX (2.5.0.0beta1)
* Fork KeePassDroid
* Add Material Design
* Add Light and Night theme
* Min API is 14
* Solve bug for fingerprint
* Update French translation
* Change donation (see KeepassDroid to contribute on both projects)
* Change donation (see KeePassDroid to contribute on both projects)
KeePassDroid (2.2.1)
* Fix kdbx4 date corruption
@@ -454,7 +493,7 @@ KeePassDroid (1.9.10)
KeePassDroid (1.9.9)
* Go back to explicitly storing blank fields in the database
(works around bug in keepassx)
(works around bug in KeePassx)
* Add support for native code on MIPS architectures
* Adding Vibrate permission. On some devices notifications fail
without the vibrate permission.

View File

@@ -11,7 +11,7 @@
* **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC...)
* Allows **fast copy** of fields and opening of URI / URL
* **Biometric recognition** for fast unlocking *(Fingerprint / Face unlock / ...)*
* **One-Time Password** management *(HOTP / TOTP)*
* **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA)
* Material design with **themes**
* **AutoFill** and Integration
* Field filling **keyboard**
@@ -30,7 +30,7 @@ KeePass DX is a **free open source password manager for Android**, which helps y
Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
*Note : If you access the application from a store, visual styles may not be available to encourage contribution to the work of open source projects. These optional styles are accessible after a contribution (and a congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
## Contributions
@@ -59,13 +59,15 @@ Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassD
## Other devices
- [KeePass XC](https://keepassxc.org/) (https://keepassxc.org/) works with **GNU/Linux**, **Mac** and **Windows**, is updated regularly and under the terms of the GNU General Public License. This is the recommended version for computers.
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
- [KeePass](https://keepass.info/) (https://keepass.info/) is the historical project, with good technical documentation for standardized database files but only running on **Windows**.
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration to KeePass written in C++.
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version also compatible with KeePass files.
## License
Copyright (c) 2019 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
Copyright (c) 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePassDX.

View File

@@ -4,21 +4,28 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
buildToolsVersion '28.0.3'
compileSdkVersion 29
buildToolsVersion '29.0.3'
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 28
versionCode = 25
versionName = "2.5.0.0beta25"
targetSdkVersion 29
versionCode = 30
versionName = "2.5beta30"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
kapt {
arguments {
arg("room.incremental", "true")
arg("room.schemaLocation", "$projectDir/schemas".toString())
}
}
}
externalNativeBuild {
@@ -79,7 +86,7 @@ android {
}
def spongycastleVersion = "1.58.0.0"
def room_version = "2.2.1"
def room_version = "2.2.5"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@@ -88,7 +95,9 @@ dependencies {
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.biometric:biometric:1.0.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.0.1'
// To upgrade with style
implementation 'com.google.android.material:material:1.0.0'
implementation "androidx.room:room-runtime:$room_version"

View File

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

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.tests
import junit.framework.TestCase
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import org.junit.Assert
class PwDateTest : TestCase() {
fun testDate() {
val jDate = DateInstant(System.currentTimeMillis())
val intermediate = DateInstant(jDate)
val cDate = DatabaseInputOutputUtils.readCDate(DatabaseInputOutputUtils.writeCDate(intermediate.date)!!, 0)
Assert.assertTrue("jDate and intermediate not equal", jDate == intermediate)
Assert.assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -19,19 +19,15 @@
*/
package com.kunzisoft.keepass.tests
import org.junit.Assert.assertArrayEquals
import java.io.ByteArrayOutputStream
import java.util.Calendar
import java.util.Random
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.ULONG_MAX_VALUE
import com.kunzisoft.keepass.stream.*
import junit.framework.TestCase
import org.junit.Assert.assertArrayEquals
import java.io.ByteArrayOutputStream
import java.util.*
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
class DatabaseInputOutputUtilsTest : TestCase() {
class StringDatabaseKDBUtilsTest : TestCase() {
fun testReadWriteLongZero() {
testReadWriteLong(0.toByte())
@@ -55,15 +51,9 @@ class DatabaseInputOutputUtilsTest : TestCase() {
private fun testReadWriteLong(value: Byte) {
val orig = ByteArray(8)
val dest = ByteArray(8)
setArray(orig, value, 0, 8)
val one = LEDataInputStream.readLong(orig, 0)
LEDataOutputStream.writeLong(one, dest, 0)
assertArrayEquals(orig, dest)
setArray(orig, value, 8)
assertArrayEquals(orig, longTo8Bytes(bytes64ToLong(orig)))
}
fun testReadWriteIntZero() {
@@ -80,24 +70,22 @@ class DatabaseInputOutputUtilsTest : TestCase() {
private fun testReadWriteInt(value: Byte) {
val orig = ByteArray(4)
val dest = ByteArray(4)
for (i in 0..3) {
orig[i] = 0
}
setArray(orig, value, 0, 4)
setArray(orig, value, 4)
val one = LEDataInputStream.readInt(orig, 0)
LEDataOutputStream.writeInt(one, dest, 0)
val one = bytes4ToInt(orig)
val dest = intTo4Bytes(one)
assertArrayEquals(orig, dest)
}
private fun setArray(buf: ByteArray, value: Byte, offset: Int, size: Int) {
for (i in offset until offset + size) {
private fun setArray(buf: ByteArray, value: Byte, size: Int) {
for (i in 0 until size) {
buf[i] = value
}
}
@@ -108,11 +96,10 @@ class DatabaseInputOutputUtilsTest : TestCase() {
orig[0] = 0
orig[1] = 1
val one = LEDataInputStream.readUShort(orig, 0)
val dest = LEDataOutputStream.writeUShortBuf(one)
val one = bytes2ToUShort(orig)
val dest = uShortTo2Bytes(one)
assertArrayEquals(orig, dest)
}
fun testReadWriteShortMin() {
@@ -125,15 +112,12 @@ class DatabaseInputOutputUtilsTest : TestCase() {
private fun testReadWriteShort(value: Byte) {
val orig = ByteArray(2)
val dest = ByteArray(2)
setArray(orig, value, 2)
setArray(orig, value, 0, 2)
val one = LEDataInputStream.readUShort(orig, 0)
LEDataOutputStream.writeUShort(one, dest, 0)
val one = bytes2ToUShort(orig)
val dest = uShortTo2Bytes(one)
assertArrayEquals(orig, dest)
}
fun testReadWriteByteZero() {
@@ -149,16 +133,8 @@ class DatabaseInputOutputUtilsTest : TestCase() {
}
private fun testReadWriteByte(value: Byte) {
val orig = ByteArray(1)
val dest = ByteArray(1)
setArray(orig, value, 0, 1)
val one = DatabaseInputOutputUtils.readUByte(orig, 0)
DatabaseInputOutputUtils.writeUByte(one, dest, 0)
assertArrayEquals(orig, dest)
val dest: Byte = uIntToByte(byteToUInt(value))
assert(value == dest)
}
fun testDate() {
@@ -168,27 +144,33 @@ class DatabaseInputOutputUtilsTest : TestCase() {
expected.set(2008, 1, 2, 3, 4, 5)
val actual = Calendar.getInstance()
DatabaseInputOutputUtils.writeCDate(expected.time, cal)?.let { buf ->
actual.time = DatabaseInputOutputUtils.readCDate(buf, 0, cal).date
dateTo5Bytes(expected.time, cal)?.let { buf ->
actual.time = bytes5ToDate(buf, cal).date
}
val jDate = DateInstant(System.currentTimeMillis())
val intermediate = DateInstant(jDate)
val cDate = bytes5ToDate(dateTo5Bytes(intermediate.date)!!)
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
assertEquals("Day mismatch: ", 2, actual.get(Calendar.DAY_OF_MONTH))
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY))
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE))
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND))
assertTrue("jDate and intermediate not equal", jDate == intermediate)
assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
}
fun testUUID() {
val bUUID = ByteArray(16)
Random().nextBytes(bUUID)
val uuid = DatabaseInputOutputUtils.bytesToUuid(bUUID)
val eUUID = DatabaseInputOutputUtils.uuidToBytes(uuid)
val uuid = bytes16ToUuid(bUUID)
val eUUID = uuidTo16Bytes(uuid)
val lUUID = LEDataInputStream.readUuid(bUUID, 0)
val leUUID = DatabaseInputOutputUtils.uuidToBytes(lUUID)
val lUUID = bytes16ToUuid(bUUID)
val leUUID = uuidTo16Bytes(lUUID)
assertArrayEquals("UUID match failed", bUUID, eUUID)
assertArrayEquals("UUID match failed", bUUID, leUUID)
@@ -202,8 +184,8 @@ class DatabaseInputOutputUtilsTest : TestCase() {
}
val bos = ByteArrayOutputStream()
val leos = LEDataOutputStream(bos)
leos.writeLong(DatabaseInputOutputUtils.ULONG_MAX_VALUE)
val leos = LittleEndianDataOutputStream(bos)
leos.writeLong(ULONG_MAX_VALUE)
leos.close()
val uLongMax = bos.toByteArray()

View File

@@ -39,9 +39,8 @@ import junit.framework.TestCase
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.engine.AesEngine
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.stream.BetterCipherInputStream
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
class CipherTest : TestCase() {
private val rand = Random()
@@ -93,7 +92,7 @@ class CipherTest : TestCase() {
val bis = ByteArrayInputStream(secrettext)
val cis = BetterCipherInputStream(bis, decrypt)
val lis = LEDataInputStream(cis)
val lis = LittleEndianDataInputStream(cis)
val decrypttext = lis.readBytes(MESSAGE_LENGTH)

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.kunzisoft.keepass"
android:installLocation="auto">
<supports-screens
@@ -7,10 +8,15 @@
android:normalScreens="true"
android:largeScreens="true"
android:anyDensity="true" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.USE_BIOMETRIC" />
<uses-permission
android:name="android.permission.VIBRATE"/>
<uses-permission
android:maxSdkVersion="18"
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:label="@string/app_name"
@@ -21,7 +27,9 @@
android:fullBackupContent="@xml/backup"
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
android:largeHeap="true"
android:theme="@style/KeepassDXStyle.Night">
android:resizeableActivity="true"
android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="n">
<!-- TODO backup API Key -->
<meta-data
android:name="com.google.android.backup.api_key"
@@ -125,7 +133,7 @@
<activity
android:name="com.kunzisoft.keepass.activities.AboutActivity"
android:launchMode="singleTask"
android:label="@string/menu_about" />
android:label="@string/about" />
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
android:configChanges="keyboardHidden" />
@@ -150,6 +158,10 @@
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name=".notifications.AttachmentFileNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
android:enabled="true"

View File

@@ -21,15 +21,15 @@ package com.kunzisoft.keepass.activities
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.MenuItem
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.core.text.HtmlCompat
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import org.joda.time.DateTime
class AboutActivity : StylishActivity() {
@@ -40,7 +40,7 @@ class AboutActivity : StylishActivity() {
setContentView(R.layout.activity_about)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = getString(R.string.menu_about)
toolbar.title = getString(R.string.about)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
@@ -64,9 +64,17 @@ class AboutActivity : StylishActivity() {
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
buildTextView.text = build
findViewById<TextView>(R.id.activity_about_licence_text).apply {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
HtmlCompat.FROM_HTML_MODE_LEGACY)
}
val disclaimerText = findViewById<TextView>(R.id.disclaimer)
disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year)
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View File

@@ -22,6 +22,7 @@ import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.util.Log
@@ -33,28 +34,40 @@ import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.createDocument
import com.kunzisoft.keepass.utils.onCreateDocumentResult
import com.kunzisoft.keepass.view.EntryContentsView
import com.kunzisoft.keepass.view.showActionError
import java.util.*
import kotlin.collections.HashMap
class EntryActivity : LockingHideActivity() {
class EntryActivity : LockingActivity() {
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var titleIconView: ImageView? = null
private var historyView: View? = null
@@ -65,10 +78,16 @@ class EntryActivity : LockingHideActivity() {
private var mDatabase: Database? = null
private var mEntry: Entry? = null
private var mIsHistory: Boolean = false
private var mEntryLastVersion: Entry? = null
private var mEntryHistoryPosition: Int = -1
private var mShowPassword: Boolean = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
private var clipboardHelper: ClipboardHelper? = null
private var firstLaunchOfActivity: Boolean = false
@@ -98,6 +117,7 @@ class EntryActivity : LockingHideActivity() {
invalidateOptionsMenu()
// Get views
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container)
@@ -108,6 +128,21 @@ class EntryActivity : LockingHideActivity() {
// Init the clipboard helper
clipboardHelper = ClipboardHelper(this)
firstLaunchOfActivity = true
// Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
mProgressDialogThread?.onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
// Close the current activity after an history action
if (result.isSuccess)
finish()
}
}
coordinatorLayout?.showActionError(result)
}
}
override fun onResume() {
@@ -115,13 +150,17 @@ class EntryActivity : LockingHideActivity() {
// Get Entry from UUID
try {
val keyEntry: NodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
if (keyEntry != null) {
mEntry = mDatabase?.getEntryById(keyEntry)
mEntryLastVersion = mEntry
}
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
mEntryHistoryPosition = historyPosition
if (historyPosition >= 0) {
mIsHistory = true
mEntry = mEntry?.getHistory()?.get(historyPosition)
@@ -155,9 +194,24 @@ class EntryActivity : LockingHideActivity() {
}
}
mAttachmentFileBinderManager?.apply {
registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
entryContentsView?.updateAttachmentDownloadProgress(attachment)
}
}
}
firstLaunchOfActivity = false
}
override fun onPause() {
mAttachmentFileBinderManager?.unregisterProgressTask()
super.onPause()
}
private fun fillEntryDataInContentsView(entry: Entry) {
val database = Database.getInstance()
@@ -265,6 +319,27 @@ class EntryActivity : LockingHideActivity() {
}
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
// Manage attachments
val attachments = entry.getAttachments()
val showAttachmentsView = attachments.isNotEmpty()
entryContentsView?.showAttachments(showAttachmentsView)
if (showAttachmentsView) {
entryContentsView?.assignAttachments(attachments)
entryContentsView?.onAttachmentClick { attachmentItem, _ ->
when (attachmentItem.downloadState) {
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
else -> {
// TODO Stop download
}
}
}
}
entryContentsView?.refreshAttachments()
// Assign dates
entryContentsView?.assignCreationDate(entry.creationTime)
entryContentsView?.assignModificationDate(entry.lastModificationTime)
@@ -276,9 +351,6 @@ class EntryActivity : LockingHideActivity() {
entryContentsView?.assignExpiresDate(getString(R.string.never))
}
// Assign special data
entryContentsView?.assignUUID(entry.nodeId.id)
// Manage history
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
if (mIsHistory) {
@@ -287,23 +359,26 @@ class EntryActivity : LockingHideActivity() {
taColorAccent.recycle()
}
val entryHistory = entry.getHistory()
// isMainEntry = not an history
// TODO isMainEntry = not an history
val showHistoryView = entryHistory.isNotEmpty()
entryContentsView?.showHistory(showHistoryView)
if (showHistoryView) {
entryContentsView?.assignHistory(entryHistory)
entryContentsView?.onHistoryClick { historyItem, position ->
launch(this, historyItem, true, position)
launch(this, historyItem, mReadOnly, position)
}
}
entryContentsView?.refreshHistory()
// Assign special data
entryContentsView?.assignUUID(entry.nodeId.id)
database.stopManageEntry(entry)
entryContentsView?.refreshHistory()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
// Not directly get the entry from intent data but from database
@@ -311,6 +386,15 @@ class EntryActivity : LockingHideActivity() {
fillEntryDataInContentsView(it)
}
}
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
if (createdFileUri != null) {
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
mAttachmentFileBinderManager
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
}
}
}
}
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
@@ -330,7 +414,10 @@ class EntryActivity : LockingHideActivity() {
MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database, menu)
if (mReadOnly) {
if (mIsHistory && !mReadOnly) {
inflater.inflate(R.menu.entry_history, menu)
}
if (mIsHistory || mReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false
menu.findItem(R.id.menu_edit)?.isVisible = false
}
@@ -423,6 +510,22 @@ class EntryActivity : LockingHideActivity() {
UriUtil.gotoUrl(this, url)
return true
}
R.id.menu_restore_entry_history -> {
mEntryLastVersion?.let { mainEntry ->
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
}
}
R.id.menu_delete_entry_history -> {
mEntryLastVersion?.let { mainEntry ->
mProgressDialogThread?.startDatabaseDeleteEntryHistory(
mainEntry,
mEntryHistoryPosition,
!mReadOnly && mAutoSaveEnable)
}
}
R.id.menu_lock -> {
lockAndExit()
return true
@@ -438,12 +541,10 @@ class EntryActivity : LockingHideActivity() {
override fun finish() {
// Transit data in previous Activity after an update
/*
TODO Slowdown when add entry as result
Intent intent = new Intent();
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
*/
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
}
super.finish()
}

View File

@@ -19,6 +19,8 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent
import android.os.Bundle
import android.os.Handler
@@ -26,14 +28,20 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ScrollView
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryEditActivityEducation
@@ -47,12 +55,16 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
import com.kunzisoft.keepass.view.showActionError
import org.joda.time.DateTime
import java.util.*
class EntryEditActivity : LockingHideActivity(),
class EntryEditActivity : LockingActivity(),
IconPickerDialogFragment.IconPickerListener,
GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener {
SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener {
private var mDatabase: Database? = null
@@ -64,8 +76,10 @@ class EntryEditActivity : LockingHideActivity(),
private var mIsNew: Boolean = false
// Views
private var scrollView: ScrollView? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: NestedScrollView? = null
private var entryEditContentsView: EntryEditContentsView? = null
private var entryEditAddToolBar: ActionMenuView? = null
private var saveView: View? = null
// Education
@@ -81,11 +95,23 @@ class EntryEditActivity : LockingHideActivity(),
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
scrollView = findViewById(R.id.entry_edit_scroll)
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
entryEditContentsView = findViewById(R.id.entry_edit_contents)
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryEditContentsView?.onDateClickListener = View.OnClickListener {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
val defaultMonth = dateTime.monthOfYear-1
val defaultDay = dateTime.dayOfMonth
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(supportFragmentManager, "DatePickerFragment")
}
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
@@ -159,17 +185,46 @@ class EntryEditActivity : LockingHideActivity(),
// Add listener to the icon
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
// Generate password button
entryEditContentsView?.setOnPasswordGeneratorClickListener { openPasswordGenerator() }
// Bottom Bar
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
entryEditAddToolBar?.apply {
menuInflater.inflate(R.menu.entry_edit, menu)
menu.findItem(R.id.menu_add_field).apply {
val allowCustomField = mNewEntry?.allowCustomFields() == true
isEnabled = allowCustomField
isVisible = allowCustomField
}
menu.findItem(R.id.menu_add_otp).apply {
val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP
isVisible = allowOTP
}
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_generate_password -> {
openPasswordGenerator()
true
}
R.id.menu_add_field -> {
addNewCustomField()
true
}
R.id.menu_add_otp -> {
setupOTP()
true
}
else -> true
}
}
}
// Save button
saveView = findViewById(R.id.entry_edit_save)
saveView = findViewById(R.id.entry_edit_validate)
saveView?.setOnClickListener { saveEntry() }
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) {
addNewCustomField()
}
// Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this)
@@ -182,6 +237,7 @@ class EntryEditActivity : LockingHideActivity(),
finish()
}
}
coordinatorLayout?.showActionError(result)
}
}
@@ -198,6 +254,9 @@ class EntryEditActivity : LockingHideActivity(),
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
url = newEntry.url
password = newEntry.password
expires = newEntry.expires
if (expires)
expiresDate = newEntry.expiryTime
notes = newEntry.notes
for (entry in newEntry.customFields.entries) {
post {
@@ -219,6 +278,10 @@ class EntryEditActivity : LockingHideActivity(),
username = entryView.username
url = entryView.url
password = entryView.password
expires = entryView.expires
if (entryView.expires) {
expiryTime = entryView.expiresDate
}
notes = entryView. notes
entryView.customFields.forEach { customField ->
putExtraField(customField.name, customField.protectedValue)
@@ -250,6 +313,13 @@ class EntryEditActivity : LockingHideActivity(),
entryEditContentsView?.addEmptyCustomField()
}
private fun setupOTP() {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
.show(supportFragmentManager, "addOTPDialog")
}
/**
* Saves the new entry or update an existing entry in the database
*/
@@ -298,8 +368,6 @@ class EntryEditActivity : LockingHideActivity(),
// Save database not needed here
menu.findItem(R.id.menu_save_database)?.isVisible = false
MenuUtil.contributionMenuInflater(inflater, menu)
if (mDatabase?.allowOTP == true)
inflater.inflate(R.menu.entry_otp, menu)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
@@ -309,12 +377,10 @@ class EntryEditActivity : LockingHideActivity(),
}
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordView = entryEditContentsView?.generatePasswordView
val addNewFieldView = entryEditContentsView?.addNewFieldButton
val generatePasswordEducationPerformed = passwordView != null
val passwordGeneratorView: View? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
val generatePasswordEducationPerformed = passwordGeneratorView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
passwordView,
passwordGeneratorView,
{
openPasswordGenerator()
},
@@ -323,16 +389,30 @@ class EntryEditActivity : LockingHideActivity(),
}
)
if (!generatePasswordEducationPerformed) {
// entryNewFieldEducationPerformed
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
val addNewFieldEducationPerformed = mNewEntry != null
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView,
{
addNewCustomField()
},
{
performedNextEducation(entryEditActivityEducation)
}
)
if (!addNewFieldEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView,
{
setupOTP()
})
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
@@ -347,14 +427,9 @@ class EntryEditActivity : LockingHideActivity(),
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_add_otp -> {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
.show(supportFragmentManager, "addOTPDialog")
return true
android.R.id.home -> {
onBackPressed()
}
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
@@ -374,6 +449,39 @@ class EntryEditActivity : LockingHideActivity(),
}
}
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
// To fix android 4.4 issue
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
if (datePicker?.isShown == true) {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
// Save the date
entryEditContentsView?.expiresDate =
DateInstant(DateTime(expiresDate)
.withYear(year)
.withMonthOfYear(month + 1)
.withDayOfMonth(day)
.toDate())
// Launch the time picker
val dateTime = DateTime(expiresDate)
val defaultHour = dateTime.hourOfDay
val defaultMinute = dateTime.minuteOfHour
TimePickerFragment.getInstance(defaultHour, defaultMinute)
.show(supportFragmentManager, "TimePickerFragment")
}
}
}
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
// Save the date
entryEditContentsView?.expiresDate =
DateInstant(DateTime(expiresDate)
.withHourOfDay(hours)
.withMinuteOfHour(minutes)
.toDate())
}
}
override fun onSaveInstanceState(outState: Bundle) {
mNewEntry?.let {
populateEntryWithViews(it)
@@ -397,6 +505,15 @@ class EntryEditActivity : LockingHideActivity(),
// Do nothing here
}
override fun onBackPressed() {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ ->
super@EntryEditActivity.onBackPressed()
}.create().show()
}
override fun finish() {
// Assign entry callback as a result in all case
try {

View File

@@ -28,7 +28,6 @@ import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log
import android.view.Menu
import android.view.MenuItem
@@ -36,13 +35,13 @@ import android.view.View
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
@@ -53,8 +52,8 @@ import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_file_selection.*
import java.io.FileNotFoundException
@@ -63,6 +62,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
// Views
private var coordinatorLayout: CoordinatorLayout? = null
private var fileListContainer: View? = null
private var createButtonView: View? = null
private var openDatabaseButtonView: View? = null
@@ -84,6 +84,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
setContentView(R.layout.activity_file_selection)
coordinatorLayout = findViewById(R.id.activity_file_selection_coordinator_layout)
fileListContainer = findViewById(R.id.container_file_list)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
@@ -92,17 +93,14 @@ class FileDatabaseSelectActivity : StylishActivity(),
// Create button
createButtonView = findViewById(R.id.create_database_button)
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/x-keepass"
}.resolveActivity(packageManager) == null) {
// No Activity found that can handle this intent.
createButtonView?.visibility = View.GONE
}
else{
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
// There is an activity which can handle this intent.
createButtonView?.visibility = View.VISIBLE
}
else{
// No Activity found that can handle this intent.
createButtonView?.visibility = View.GONE
}
createButtonView?.setOnClickListener { createNewFile() }
@@ -146,8 +144,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
if (!(savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_STAY)
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
UriUtil.parse(databasePath)?.let { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
@@ -167,9 +164,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
onActionFinish = { actionTask, _ ->
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
// TODO Check
// mAdapterDatabaseHistory?.notifyDataSetChanged()
// updateFileListVisibility()
GroupActivity.launch(this@FileDatabaseSelectActivity)
}
}
@@ -182,23 +176,15 @@ class FileDatabaseSelectActivity : StylishActivity(),
*/
@SuppressLint("InlinedApi")
private fun createNewFile() {
try {
startActivityForResult(Intent(
Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/x-keepass"
putExtra(Intent.EXTRA_TITLE, getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default))
},
CREATE_FILE_REQUEST_CODE)
} catch (e: Exception) {
BrowserDialogFragment().show(supportFragmentManager, "browserDialog")
}
createDocument(this, getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default), "application/x-keepass")
}
private fun fileNoFoundAction(e: FileNotFoundException) {
val error = getString(R.string.file_not_found_content)
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
coordinatorLayout?.let {
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
Log.e(TAG, error, e)
}
@@ -289,13 +275,28 @@ class FileDatabaseSelectActivity : StylishActivity(),
updateExternalStorageWarning()
// Construct adapter with listeners
if (PreferencesUtil.showRecentFiles(this)) {
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
databaseFileHistoryList?.let {
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
updateFileListVisibility()
databaseFileHistoryList?.let { historyList ->
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(this@FileDatabaseSelectActivity)
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(
// Show only uri accessible
historyList.filter {
if (hideBrokenLocations) {
FileDatabaseInfo(this@FileDatabaseSelectActivity,
it.databaseUri).exists
} else
true
})
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
}
}
} else {
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
}
// Register progress task
mProgressDialogThread?.registerProgressTask()
@@ -367,15 +368,18 @@ class FileDatabaseSelectActivity : StylishActivity(),
}
// Retrieve the created URI from the file manager
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
mDatabaseFileUri = data?.data
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
coordinatorLayout?.let {
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
Log.e(TAG, error)
}
// else {
// TODO Show error
// }
}
}
@@ -427,8 +431,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
private const val EXTRA_STAY = "EXTRA_STAY"
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
private const val CREATE_FILE_REQUEST_CODE = 3853
/*
* -------------------------
* No Standard Launch, pass by PasswordActivity

View File

@@ -42,14 +42,19 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentManager
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
@@ -71,6 +76,7 @@ import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.AddNodeButtonView
import com.kunzisoft.keepass.view.ToolbarAction
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionError
class GroupActivity : LockingActivity(),
GroupEditDialogFragment.EditGroupListener,
@@ -96,6 +102,7 @@ class GroupActivity : LockingActivity(),
private var mListNodesFragment: ListNodesFragment? = null
private var mCurrentGroupIsASearch: Boolean = false
private var mRequestStartupSearch = true
// Nodes
private var mRootGroup: Group? = null
@@ -136,6 +143,8 @@ class GroupActivity : LockingActivity(),
// Retrieve elements after an orientation change
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(REQUEST_STARTUP_SEARCH_KEY))
mRequestStartupSearch = savedInstanceState.getBoolean(REQUEST_STARTUP_SEARCH_KEY)
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
}
@@ -246,15 +255,7 @@ class GroupActivity : LockingActivity(),
}
}
if (!result.isSuccess) {
coordinatorLayout?.let { coordinatorLayout ->
result.exception?.errorId?.let { errorId ->
Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show()
} ?: result.message?.let { message ->
Snackbar.make(coordinatorLayout, message, Snackbar.LENGTH_LONG).asError().show()
}
}
}
coordinatorLayout?.showActionError(result)
finishNodeAction()
@@ -334,6 +335,7 @@ class GroupActivity : LockingActivity(),
mOldGroupToUpdate?.let {
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
}
outState.putBoolean(REQUEST_STARTUP_SEARCH_KEY, mRequestStartupSearch)
super.onSaveInstanceState(outState)
}
@@ -344,7 +346,8 @@ class GroupActivity : LockingActivity(),
// If it's a search
if (Intent.ACTION_SEARCH == intent.action) {
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
return mDatabase?.search(searchString)
}
// else a real group
else {
@@ -430,17 +433,12 @@ class GroupActivity : LockingActivity(),
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch
mCurrentGroup?.let {
val isRoot = it == mRootGroup
if (!it.allowAddEntryIfIsRoot())
addEntryEnabled = !isRoot && addEntryEnabled
if (isRoot) {
showWarnings()
}
addEntryEnabled = it != mRootGroup && addEntryEnabled
}
enableAddGroup(addGroupEnabled)
enableAddEntry(addEntryEnabled)
if (isEnable)
showButton()
}
}
@@ -448,7 +446,7 @@ class GroupActivity : LockingActivity(),
private fun refreshNumberOfChildren() {
numberChildrenView?.apply {
if (PreferencesUtil.showNumberEntries(context)) {
text = mCurrentGroup?.getChildEntries(true)?.size?.toString() ?: ""
text = mCurrentGroup?.getNumberOfChildEntries(*Group.ChildFilter.getDefaults(context))?.toString() ?: ""
visibility = View.VISIBLE
} else {
visibility = View.GONE
@@ -505,6 +503,7 @@ class GroupActivity : LockingActivity(),
private fun finishNodeAction() {
actionNodeMode?.finish()
actionNodeMode = null
addNodeButtonView?.showButton()
}
override fun onNodeSelected(nodes: List<Node>): Boolean {
@@ -516,6 +515,7 @@ class GroupActivity : LockingActivity(),
} else {
actionNodeMode?.invalidate()
}
addNodeButtonView?.hideButton()
} else {
finishNodeAction()
}
@@ -658,19 +658,22 @@ class GroupActivity : LockingActivity(),
}
// Menu for recycle bin
if (mDatabase?.isRecycleBinEnabled == true
if (!mReadOnly
&& mDatabase?.isRecycleBinEnabled == true
&& mDatabase?.recycleBin == mCurrentGroup) {
inflater.inflate(R.menu.recycle_bin, menu)
}
// Get the SearchView and set the searchable configuration
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
menu.findItem(R.id.menu_search)?.let {
val searchView = it.actionView as SearchView?
searchView?.apply {
setSearchableInfo(searchManager.getSearchableInfo(
ComponentName(this@GroupActivity, GroupActivity::class.java)))
(searchManager?.getSearchableInfo(
ComponentName(this@GroupActivity, GroupActivity::class.java)))?.let { searchableInfo ->
setSearchableInfo(searchableInfo)
}
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
suggestionsAdapter = mSearchSuggestionAdapter
setOnSuggestionListener(object : SearchView.OnSuggestionListener {
@@ -688,6 +691,13 @@ class GroupActivity : LockingActivity(),
}
})
}
// Expand the search view if defined in settings
if (mRequestStartupSearch
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
// To request search only one time
mRequestStartupSearch = false
it.expandActionView()
}
}
super.onCreateOptionsMenu(menu)
@@ -853,16 +863,8 @@ class GroupActivity : LockingActivity(),
.iconPicked(bundle)
}
private fun showWarnings() {
if (Database.getInstance().isReadOnly) {
if (PreferencesUtil.showReadOnlyWarning(this)) {
ReadOnlyDialog().show(supportFragmentManager, "readOnlyDialog")
}
}
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
}
override fun startActivity(intent: Intent) {
@@ -906,8 +908,8 @@ class GroupActivity : LockingActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
// Not directly get the entry from intent data but from database
mListNodesFragment?.rebuildList()
// Directly used the onActivityResult in fragment
mListNodesFragment?.onActivityResult(requestCode, resultCode, data)
}
private fun removeSearchInIntent(intent: Intent) {
@@ -948,18 +950,14 @@ class GroupActivity : LockingActivity(),
private val TAG = GroupActivity::class.java.name
private const val REQUEST_STARTUP_SEARCH_KEY = "REQUEST_STARTUP_SEARCH_KEY"
private const val GROUP_ID_KEY = "GROUP_ID_KEY"
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
private fun buildAndLaunchIntent(context: Context, group: Group?, readOnly: Boolean,
private fun buildIntent(context: Context, group: Group?, readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
val checkTime = if (context is Activity)
TimeoutHelper.checkTimeAndLockIfTimeout(context)
else
TimeoutHelper.checkTime(context)
if (checkTime) {
val intent = Intent(context, GroupActivity::class.java)
if (group != null) {
intent.putExtra(GROUP_ID_KEY, group.nodeId)
@@ -967,6 +965,19 @@ class GroupActivity : LockingActivity(),
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
intentBuildLauncher.invoke(intent)
}
private fun checkTimeAndBuildIntent(activity: Activity, group: Group?, readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
buildIntent(activity, group, readOnly, intentBuildLauncher)
}
}
private fun checkTimeAndBuildIntent(context: Context, group: Group?, readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
if (TimeoutHelper.checkTime(context)) {
buildIntent(context, group, readOnly, intentBuildLauncher)
}
}
/*
@@ -976,9 +987,9 @@ class GroupActivity : LockingActivity(),
*/
@JvmOverloads
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
TimeoutHelper.recordTime(context)
buildAndLaunchIntent(context, null, readOnly) { intent ->
fun launch(context: Context,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
context.startActivity(intent)
}
}
@@ -990,9 +1001,9 @@ class GroupActivity : LockingActivity(),
*/
// TODO implement pre search to directly open the direct group
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
TimeoutHelper.recordTime(context)
buildAndLaunchIntent(context, null, readOnly) { intent ->
fun launchForKeyboardSelection(context: Context,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
}
}
@@ -1005,9 +1016,10 @@ class GroupActivity : LockingActivity(),
// TODO implement pre search to directly open the direct group
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure, readOnly: Boolean) {
TimeoutHelper.recordTime(activity)
buildAndLaunchIntent(activity, null, readOnly) { intent ->
fun launchForAutofillResult(activity: Activity,
assistStructure: AssistStructure,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure)
}
}

View File

@@ -1,10 +1,27 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.util.Log
@@ -35,7 +52,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private var nodeClickListener: NodeClickListener? = null
private var onScrollListener: OnScrollListener? = null
private var listView: RecyclerView? = null
private var mNodesRecyclerView: RecyclerView? = null
var mainGroup: Group? = null
private set
private var mAdapter: NodeAdapter? = null
@@ -50,8 +67,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private var notFoundView: View? = null
private var isASearchResult: Boolean = false
// Preferences for sorting
private var prefs: SharedPreferences? = null
private var readOnly: Boolean = false
get() {
@@ -136,7 +151,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
})
}
}
prefs = PreferenceManager.getDefaultSharedPreferences(context)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -150,11 +164,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
// To apply theme
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_list_nodes, container, false)
listView = rootView.findViewById(R.id.nodes_list)
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
notFoundView = rootView.findViewById(R.id.not_found_container)
mNodesRecyclerView?.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
layoutManager = LinearLayoutManager(context)
adapter = mAdapter
}
onScrollListener?.let { onScrollListener ->
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
onScrollListener.onScrolled(dy)
@@ -162,8 +182,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
})
}
rebuildList()
return rootView
}
@@ -175,14 +193,14 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
// Refresh data
mAdapter?.notifyDataSetChanged()
rebuildList()
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
listView?.visibility = View.GONE
mNodesRecyclerView?.visibility = View.GONE
notFoundView?.visibility = View.VISIBLE
} else {
listView?.visibility = View.VISIBLE
mNodesRecyclerView?.visibility = View.VISIBLE
notFoundView?.visibility = View.GONE
}
}
@@ -190,30 +208,27 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
fun rebuildList() {
// Add elements to the list
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
mAdapter?.apply {
rebuildList(mainGroup)
// To visually change the elements
if (PreferencesUtil.APPEARANCE_CHANGED) {
notifyDataSetChanged()
PreferencesUtil.APPEARANCE_CHANGED = false
}
}
listView?.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
layoutManager = LinearLayoutManager(context)
adapter = mAdapter
}
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
// Toggle setting
prefs?.edit()?.apply {
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
putBoolean(getString(R.string.sort_ascending_key), ascending)
putBoolean(getString(R.string.sort_group_before_key), groupsBefore)
putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom)
apply()
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
// Save setting
context?.let {
PreferencesUtil.saveNodeSort(it, sortNodeEnum, sortNodeParameters)
}
// Tell the adapter to refresh it's list
mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore)
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
rebuildList()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -354,15 +369,11 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
mAdapter?.addNode(newNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
//mAdapter.updateLastNodeRegister(newNode);
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
}
addNode(changedNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
mAdapter?.notifyDataSetChanged()
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
}
}

View File

@@ -23,19 +23,14 @@ import android.app.Activity
import android.app.assist.AssistStructure
import android.app.backup.BackupManager
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.*
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.Button
import android.widget.CompoundButton
@@ -75,11 +70,10 @@ import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException
class PasswordActivity : StylishActivity() {
open class PasswordActivity : StylishActivity() {
// Views
private var toolbar: Toolbar? = null
private var containerView: View? = null
private var filenameView: TextView? = null
private var passwordView: EditText? = null
@@ -89,17 +83,26 @@ class PasswordActivity : StylishActivity() {
private var checkboxKeyFileView: CompoundButton? = null
private var checkboxDefaultDatabaseView: CompoundButton? = null
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
private var infoContainerView: ViewGroup? = null
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
private var prefs: SharedPreferences? = null
private var mRememberKeyFile: Boolean = false
private var mOpenFileHelper: OpenFileHelper? = null
private var readOnly: Boolean = false
private var mForceReadOnly: Boolean = false
set(value) {
infoContainerView?.visibility = if (value) {
readOnly = true
View.VISIBLE
} else {
View.GONE
}
field = value
}
private var mProgressDialogThread: ProgressDialogThread? = null
@@ -108,10 +111,6 @@ class PasswordActivity : StylishActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = PreferenceManager.getDefaultSharedPreferences(this)
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
setContentView(R.layout.activity_password)
toolbar = findViewById(R.id.toolbar)
@@ -121,7 +120,7 @@ class PasswordActivity : StylishActivity() {
supportActionBar?.setDisplayShowHomeEnabled(true)
containerView = findViewById(R.id.container)
confirmButtonView = findViewById(R.id.pass_ok)
confirmButtonView = findViewById(R.id.activity_password_open_button)
filenameView = findViewById(R.id.filename)
passwordView = findViewById(R.id.password)
keyFileView = findViewById(R.id.pass_keyfile)
@@ -129,6 +128,7 @@ class PasswordActivity : StylishActivity() {
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
advancedUnlockInfoView = findViewById(R.id.biometric_info)
infoContainerView = findViewById(R.id.activity_password_info_container)
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
@@ -163,6 +163,12 @@ class PasswordActivity : StylishActivity() {
enableOrNotTheConfirmationButton()
}
// If is a view intent
getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
}
mProgressDialogThread = ProgressDialogThread(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
@@ -175,10 +181,9 @@ class PasswordActivity : StylishActivity() {
}
}
// Remove the password in view in all cases
removePassword()
if (result.isSuccess) {
mDatabaseKeyFileUri = null
clearCredentialsViews(true)
launchGroupActivity()
} else {
var resultError = ""
@@ -234,6 +239,24 @@ class PasswordActivity : StylishActivity() {
}
}
private fun getUriFromIntent(intent: Intent?) {
// If is a view intent
val action = intent?.action
if (action != null
&& action == VIEW_INTENT) {
mDatabaseFileUri = intent.data
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
} else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
getUriFromIntent(intent)
}
private fun launchGroupActivity() {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
@@ -262,13 +285,15 @@ class PasswordActivity : StylishActivity() {
}
override fun onResume() {
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
if (Database.getInstance().loaded)
launchGroupActivity()
// If the database isn't accessible make sure to clear the password field, if it
// was saved in the instance state
if (Database.getInstance().loaded) {
setEmptyViews()
clearCredentialsViews()
}
// For check shutdown
@@ -280,44 +305,37 @@ class PasswordActivity : StylishActivity() {
}
override fun onSaveInstanceState(outState: Bundle) {
mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString())
}
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
super.onSaveInstanceState(outState)
}
private fun initUriFromIntent() {
val databaseUri: Uri?
val keyFileUri: Uri?
// If is a view intent
val action = intent.action
if (action != null
&& action == VIEW_INTENT) {
databaseUri = intent.data
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
} else {
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
}
/*
// "canXrite" doesn't work with Google Drive, don't really know why?
mForceReadOnly = mDatabaseFileUri?.let {
!FileDatabaseInfo(this, it).canWrite
} ?: false
*/
mForceReadOnly = false
// Post init uri with KeyFile if needed
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
// Retrieve KeyFile in a thread
databaseUri?.let { databaseUriNotNull ->
mDatabaseFileUri?.let { databaseUri ->
FileDatabaseHistoryAction.getInstance(applicationContext)
.getKeyFileUriByDatabaseUri(databaseUriNotNull) {
.getKeyFileUriByDatabaseUri(databaseUri) {
onPostInitUri(databaseUri, it)
}
}
} else {
onPostInitUri(databaseUri, keyFileUri)
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
}
}
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
mDatabaseFileUri = databaseFileUri
mDatabaseKeyFileUri = keyFileUri
// Define title
databaseFileUri?.let {
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
@@ -326,26 +344,18 @@ class PasswordActivity : StylishActivity() {
}
// Define Key File text
val keyUriString = keyFileUri?.toString() ?: ""
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18
populateKeyFileTextView(keyUriString)
if (mRememberKeyFile) {
populateKeyFileTextView(keyFileUri?.toString())
}
// Define listeners for default database checkbox and validate button
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
var newDefaultFileName: Uri? = null
var newDefaultFileUri: Uri? = null
if (isChecked) {
newDefaultFileName = databaseFileUri ?: newDefaultFileName
newDefaultFileUri = databaseFileUri ?: newDefaultFileUri
}
prefs?.edit()?.apply {
newDefaultFileName?.let {
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
} ?: kotlin.run {
remove(KEY_DEFAULT_DATABASE_PATH)
}
apply()
}
PreferencesUtil.saveDefaultDatabasePath(this, newDefaultFileUri)
val backupManager = BackupManager(this@PasswordActivity)
backupManager.dataChanged()
@@ -353,7 +363,7 @@ class PasswordActivity : StylishActivity() {
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
// Retrieve settings for default database
val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "")
val defaultFilename = PreferencesUtil.getDefaultDatabasePath(this)
if (databaseFileUri != null
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
@@ -363,6 +373,8 @@ class PasswordActivity : StylishActivity() {
// If Activity is launch with a password and want to open directly
val intent = intent
val password = intent.getStringExtra(KEY_PASSWORD)
// Consume the intent extra password
intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) {
populatePasswordTextView(password)
@@ -428,10 +440,9 @@ class PasswordActivity : StylishActivity() {
}
}
private fun setEmptyViews() {
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
populatePasswordTextView(null)
// Bug KeepassDX #18
if (!mRememberKeyFile) {
if (clearKeyFile) {
populateKeyFileTextView(null)
}
}
@@ -497,18 +508,13 @@ class PasswordActivity : StylishActivity() {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
}
private fun removePassword() {
passwordView?.setText("")
checkboxPasswordView?.isChecked = false
}
private fun loadDatabase(databaseFileUri: Uri?,
password: String?,
keyFileUri: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
removePassword()
clearCredentialsViews()
}
databaseFileUri?.let { databaseUri ->
@@ -545,14 +551,16 @@ class PasswordActivity : StylishActivity() {
}.show(supportFragmentManager, "duplicateUUIDDialog")
}
// To fix multiple view education
private var performedEductionInProgress = false
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
// Read menu
inflater.inflate(R.menu.open_file, menu)
if (mForceReadOnly) {
menu.removeItem(R.id.menu_open_file_read_mode_key)
} else {
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
}
MenuUtil.defaultMenuInflater(inflater, menu)
@@ -563,51 +571,65 @@ class PasswordActivity : StylishActivity() {
super.onCreateOptionsMenu(menu)
if (!performedEductionInProgress) {
performedEductionInProgress = true
// Show education views
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
}
launchEducation(menu)
return true
}
// To fix multiple view education
private var performedEductionInProgress = false
private fun launchEducation(menu: Menu, onEducationFinished: (()-> Unit)? = null) {
if (!performedEductionInProgress) {
performedEductionInProgress = true
// Show education views
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu, onEducationFinished) }
}
}
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
menu: Menu) {
val educationContainerView = containerView
val unlockEducationPerformed = educationContainerView != null
menu: Menu,
onEducationFinished: (()-> Unit)? = null) {
val educationToolbar = toolbar
val unlockEducationPerformed = educationToolbar != null
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
educationContainerView,
educationToolbar,
{
performedNextEducation(passwordActivityEducation, menu)
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
},
{
performedNextEducation(passwordActivityEducation, menu)
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
})
if (!unlockEducationPerformed) {
val educationToolbar = toolbar
val readOnlyEducationPerformed =
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
{
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
performedNextEducation(passwordActivityEducation, menu)
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
},
{
performedNextEducation(passwordActivityEducation, menu)
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
})
if (!readOnlyEducationPerformed) {
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
// EducationPerformed
val biometricEducationPerformed =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!)
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
{
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
},
{
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
})
if (!biometricEducationPerformed) {
onEducationFinished?.invoke()
}
}
}
}
@@ -655,6 +677,7 @@ class PasswordActivity : StylishActivity() {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri.toString())
}
}
@@ -663,7 +686,7 @@ class PasswordActivity : StylishActivity() {
// this block if not a key file response
when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
setEmptyViews()
clearCredentialsViews()
Database.getInstance().closeAndClear(applicationContext.filesDir)
}
}
@@ -674,8 +697,6 @@ class PasswordActivity : StylishActivity() {
private val TAG = PasswordActivity::class.java.name
const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"

View File

@@ -0,0 +1,61 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.DatePickerDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.DialogFragment
class DatePickerFragment : DialogFragment() {
private var mDefaultYear: Int = 2000
private var mDefaultMonth: Int = 1
private var mDefaultDay: Int = 1
private var mListener: DatePickerDialog.OnDateSetListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as DatePickerDialog.OnDateSetListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Create a new instance of DatePickerDialog and return it
return context?.let {
arguments?.apply {
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
}
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
} ?: super.onCreateDialog(savedInstanceState)
}
companion object {
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
fun getInstance(defaultYear: Int,
defaultMonth: Int,
defaultDay: Int): DatePickerFragment {
return DatePickerFragment().apply {
arguments = Bundle().apply {
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
}
}
}
}
}

View File

@@ -112,7 +112,7 @@ class IconPickerDialogFragment : DialogFragment() {
// 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()
ta.recycle()
}
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
class ReadOnlyDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = androidx.appcompat.app.AlertDialog.Builder(activity)
var warning = getString(R.string.read_only_warning)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
warning = warning + "\n\n" + getString(R.string.read_only_kitkat_warning)
}
builder.setMessage(warning)
builder.setPositiveButton(getString(android.R.string.ok)) { _, _ -> dismiss() }
builder.setNegativeButton(getString(R.string.beta_dontask)) { _, _ ->
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val edit = prefs.edit()
edit.putBoolean(getString(R.string.show_read_only_warning), false)
edit.apply()
dismiss()
}
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
}

View File

@@ -1,3 +1,22 @@
/*
* 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.annotation.SuppressLint

View File

@@ -82,7 +82,9 @@ class SortDialogFragment : DialogFragment() {
builder.setView(rootView)
// Add action buttons
.setPositiveButton(android.R.string.ok
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum,
SortNodeEnum.SortNodeParameters(mAscending, mGroupsBefore, mRecycleBinBottom))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
@@ -150,10 +152,7 @@ class SortDialogFragment : DialogFragment() {
}
interface SortSelectionListener {
fun onSortSelected(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean)
fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters)
}
companion object {

View File

@@ -0,0 +1,57 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.DatePickerDialog
import android.app.Dialog
import android.app.TimePickerDialog
import android.content.Context
import android.os.Bundle
import android.text.format.DateFormat
import androidx.fragment.app.DialogFragment
class TimePickerFragment : DialogFragment() {
private var defaultHour: Int = 0
private var defaultMinute: Int = 0
private var mListener: TimePickerDialog.OnTimeSetListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as TimePickerDialog.OnTimeSetListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Create a new instance of DatePickerDialog and return it
return context?.let {
arguments?.apply {
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
}
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
} ?: super.onCreateDialog(savedInstanceState)
}
companion object {
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
fun getInstance(defaultHour: Int,
defaultMinute: Int): TimePickerFragment {
return TimePickerFragment().apply {
arguments = Bundle().apply {
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
}
}
}
}
}

View File

@@ -1,3 +1,22 @@
/*
* 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.app.assist.AssistStructure

View File

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

View File

@@ -20,11 +20,7 @@
package com.kunzisoft.keepass.activities.lock
import android.app.Activity
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.util.Log
import android.view.View
@@ -34,12 +30,9 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.*
abstract class LockingActivity : StylishActivity() {
@@ -81,12 +74,10 @@ abstract class LockingActivity : StylishActivity() {
}
if (mTimeoutEnable) {
mLockReceiver = LockReceiver()
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(LOCK_ACTION)
mLockReceiver = LockReceiver {
lockAndExit()
}
registerReceiver(mLockReceiver, intentFilter)
registerLockReceiver(mLockReceiver)
}
mExitLock = false
@@ -151,29 +142,12 @@ abstract class LockingActivity : StylishActivity() {
}
override fun onDestroy() {
unregisterLockReceiver(mLockReceiver)
super.onDestroy()
if (mLockReceiver != null)
unregisterReceiver(mLockReceiver)
}
inner class LockReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// If allowed, lock and exit
if (!TimeoutHelper.temporarilyDisableTimeout) {
intent.action?.let {
when (it) {
Intent.ACTION_SCREEN_OFF -> if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this@LockingActivity)) {
lockAndExit()
}
LOCK_ACTION -> lockAndExit()
}
}
}
}
}
protected fun lockAndExit() {
sendBroadcast(Intent(LOCK_ACTION))
lock()
}
@@ -208,20 +182,8 @@ abstract class LockingActivity : StylishActivity() {
}
fun Activity.lock() {
// Stop the Magikeyboard service
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
MagikIME.removeEntry(this)
closeDatabase()
// Stop the notification service
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
" after inactivity or manual lock")
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
cancelAll()
}
// Clear data
Database.getInstance().closeAndClear(applicationContext.filesDir)
// Add onActivityForResult response
setResult(LockingActivity.RESULT_EXIT_LOCK)
finish()

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.lock
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.WindowManager
/**
* Locking Hide Activity that sets FLAG_SECURE to prevent screenshots, and from
* appearing in the recent app preview
*/
abstract class LockingHideActivity : LockingActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}
/* (non-Javadoc) Workaround for HTC Linkify issues
* @see android.app.Activity#startActivity(android.content.Intent)
*/
override fun startActivity(intent: Intent) {
try {
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
intent.component = null
}
super.startActivity(intent)
} catch (e: ActivityNotFoundException) {
/* Catch the bad HTC implementation case */
super.startActivity(Intent.createChooser(intent, null))
}
}
}

View File

@@ -61,6 +61,7 @@ object Stylish {
return when (themeString) {
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
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_red) -> R.style.KeepassDXStyle_Red

View File

@@ -19,20 +19,45 @@
*/
package com.kunzisoft.keepass.activities.stylish
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import android.util.Log
import android.view.WindowManager
/**
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
* appearing in the recent app preview
*/
abstract class StylishActivity : AppCompatActivity() {
@StyleRes
private var themeId: Int = 0
/* (non-Javadoc) Workaround for HTC Linkify issues
* @see android.app.Activity#startActivity(android.content.Intent)
*/
override fun startActivity(intent: Intent) {
try {
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
intent.component = null
}
super.startActivity(intent)
} catch (e: ActivityNotFoundException) {
/* Catch the bad HTC implementation case */
super.startActivity(Intent.createChooser(intent, null))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.themeId = Stylish.getThemeId(this)
setTheme(themeId)
// Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}
override fun onResume() {

View File

@@ -0,0 +1,100 @@
/*
* 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.adapters
import android.content.Context
import android.text.format.Formatter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<EntryAttachmentsAdapter.EntryBinariesViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var entryAttachmentsList: MutableList<EntryAttachment> = ArrayList()
var onItemClickListener: ((item: EntryAttachment, position: Int)->Unit)? = null
private val mDatabase = Database.getInstance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
}
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
val entryAttachment = entryAttachmentsList[position]
holder.binaryFileTitle.text = entryAttachment.name
holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachment.binaryAttachment.length())
holder.binaryFileCompression.apply {
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|| entryAttachment.binaryAttachment.isCompressed == true) {
text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE
} else {
text = ""
visibility = View.GONE
}
}
holder.binaryFileProgress.apply {
visibility = when (entryAttachment.downloadState) {
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
}
progress = entryAttachment.downloadProgression
}
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryAttachment, position)
}
}
override fun getItemCount(): Int {
return entryAttachmentsList.size
}
fun updateProgress(entryAttachment: EntryAttachment) {
val indexEntryAttachment = entryAttachmentsList.indexOfLast { current -> current.name == entryAttachment.name }
if (indexEntryAttachment != -1) {
entryAttachmentsList[indexEntryAttachment] = entryAttachment
notifyItemChanged(indexEntryAttachment)
}
}
fun clear() {
entryAttachmentsList.clear()
}
inner class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
}
}

View File

@@ -1,3 +1,22 @@
/*
* 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.adapters
import android.content.Context

View File

@@ -1,3 +1,22 @@
/*
* 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.adapters
import android.content.Context
@@ -15,7 +34,7 @@ import java.util.ArrayList
class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.FieldViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var fields: MutableList<Field> = ArrayList()
private var fields: MutableList<Field> = ArrayList()
var onItemClickListener: OnItemClickListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
@@ -33,6 +52,11 @@ class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.Field
return fields.size
}
fun setFields(fieldsToAdd: List<Field>) {
fields.clear()
fields.addAll(fieldsToAdd)
}
fun clear() {
fields.clear()
}

View File

@@ -20,6 +20,8 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
import android.util.TypedValue
@@ -82,15 +84,27 @@ class FileDatabaseHistoryAdapter(private val context: Context)
// File path
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
holder.filePreciseInfoContainer.visibility = if (fileDatabaseInfo.found()) {
// Modification
holder.fileModification.text = fileDatabaseInfo.getModificationString()
// Size
holder.fileSize.text = fileDatabaseInfo.getSizeString()
if (fileDatabaseInfo.exists) {
holder.fileInformation.clearColorFilter()
} else {
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
}
View.VISIBLE
} else
View.GONE
// Modification
fileDatabaseInfo.getModificationString()?.let {
holder.fileModification.text = it
holder.fileModification.visibility = View.VISIBLE
} ?: run {
holder.fileModification.visibility = View.GONE
}
// Size
fileDatabaseInfo.getSizeString()?.let {
holder.fileSize.text = it
holder.fileSize.visibility = View.VISIBLE
} ?: run {
holder.fileSize.visibility = View.GONE
}
// Click on information
val isExpanded = position == mExpandedPosition
@@ -142,6 +156,10 @@ class FileDatabaseHistoryAdapter(private val context: Context)
return listDatabaseFiles.size
}
fun clearDatabaseFileHistoryList() {
listDatabaseFiles.clear()
}
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
listDatabaseFiles.clear()
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
@@ -178,7 +196,6 @@ class FileDatabaseHistoryAdapter(private val context: Context)
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
var filePath: TextView = itemView.findViewById(R.id.file_path)
var filePreciseInfoContainer: ViewGroup = itemView.findViewById(R.id.file_precise_info_container)
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
var fileSize: TextView = itemView.findViewById(R.id.file_size)
}

View File

@@ -21,51 +21,52 @@ package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut
import java.util.*
class NodeAdapter
/**
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
(private val context: Context)
class NodeAdapter (private val context: Context)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
private var nodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val nodeSortedListCallback: NodeSortedListCallback
private val nodeSortedList: SortedList<Node>
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var prefTextSize: Float = 0F
private var subtextSize: Float = 0F
private var infoTextSize: Float = 0F
private var numberChildrenTextSize: Float = 0F
private var iconSize: Float = 0F
private var listSort: SortNodeEnum = SortNodeEnum.DB
private var ascendingSort: Boolean = true
private var groupsBeforeSort: Boolean = true
private var recycleBinBottomSort: Boolean = true
private var prefSizeMultiplier: Float = 0F
private var subtextDefaultDimension: Float = 0F
private var infoTextDefaultDimension: Float = 0F
private var numberChildrenTextDefaultDimension: Float = 0F
private var iconDefaultDimension: Float = 0F
private var showUserNames: Boolean = true
private var showNumberEntries: Boolean = true
private var entryFilters = arrayOf<Group.ChildFilter>()
private var actionNodesList = LinkedList<Node>()
private var nodeClickCallback: NodeClickCallback? = null
@@ -83,23 +84,15 @@ class NodeAdapter
get() = nodeSortedList.size() <= 0
init {
this.infoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
this.subtextDefaultDimension = context.resources.getDimension(R.dimen.list_small_size_default)
this.numberChildrenTextDefaultDimension = context.resources.getDimension(R.dimen.list_tiny_size_default)
this.iconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
assignPreferences()
this.nodeSortedList = SortedList(Node::class.java, object : SortedListAdapterCallback<Node>(this) {
override fun compare(item1: Node, item2: Node): Int {
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2)
}
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
return oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
}
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
return item1 == item2
}
})
this.nodeSortedListCallback = NodeSortedListCallback()
this.nodeSortedList = SortedList(Node::class.java, nodeSortedListCallback)
// Database
this.mDatabase = Database.getInstance()
@@ -114,20 +107,23 @@ class NodeAdapter
taTextColor.recycle()
}
private fun assignPreferences() {
this.prefTextSize = PreferencesUtil.getListTextSize(context)
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
this.subtextSize = context.resources.getDimension(R.dimen.list_small_size_default) * prefTextSize
this.numberChildrenTextSize = context.resources.getDimension(R.dimen.list_tiny_size_default) * prefTextSize
this.iconSize = context.resources.getDimension(R.dimen.list_icon_size_default) * prefTextSize
fun assignPreferences() {
this.prefSizeMultiplier = PreferencesUtil.getListTextSize(context)
notifyChangeSort(
PreferencesUtil.getListSort(context),
SortNodeEnum.SortNodeParameters(
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context)
)
)
this.listSort = PreferencesUtil.getListSort(context)
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
this.entryFilters = Group.ChildFilter.getDefaults(context)
// Reinit textSize for all view type
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
}
@@ -136,15 +132,25 @@ class NodeAdapter
* Rebuild the list by clear and build children from the group
*/
fun rebuildList(group: Group) {
this.nodeSortedList.clear()
assignPreferences()
try {
this.nodeSortedList.addAll(group.getChildren())
} catch (e: Exception) {
Log.e(TAG, "Can't add node elements to the list", e)
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
nodeSortedList.replaceAll(group.getFilteredChildren(*entryFilters)
)
}
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
override fun compare(item1: Node, item2: Node): Int {
return nodeComparator!!.compare(item1, item2)
}
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
return oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
}
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
return item1 == item2
}
notifyDataSetChanged()
}
fun contains(node: Node): Boolean {
@@ -255,10 +261,9 @@ class NodeAdapter
/**
* Notify a change sort of the list
*/
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) {
this.listSort = sortNodeEnum
this.ascendingSort = ascending
this.groupsBeforeSort = groupsBefore
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
this.nodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
}
override fun getItemViewType(position: Int): Int {
@@ -285,19 +290,57 @@ class NodeAdapter
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
// Relative size of the icon
layoutParams?.apply {
height = iconSize.toInt()
width = iconSize.toInt()
height = (iconDefaultDimension * prefSizeMultiplier).toInt()
width = (iconDefaultDimension * prefSizeMultiplier).toInt()
}
}
// Assign text
holder.text.apply {
text = subNode.title
setTextSize(textSizeUnit, infoTextSize)
paintFlags = if (subNode.isCurrentlyExpires)
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
else
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
setTextSize(textSizeUnit, infoTextDefaultDimension, prefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires)
}
// Add subText with username
holder.subText.apply {
text = ""
strikeOut(subNode.isCurrentlyExpires)
visibility = View.GONE
}
// Specific elements for entry
if (subNode.type == Type.ENTRY) {
val entry = subNode as Entry
mDatabase.startManageEntry(entry)
holder.text.text = entry.getVisualTitle()
holder.subText.apply {
val username = entry.username
if (showUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE
text = username
setTextSize(textSizeUnit, subtextDefaultDimension, prefSizeMultiplier)
}
}
mDatabase.stopManageEntry(entry)
}
// Add number of entries in groups
if (subNode.type == Type.GROUP) {
if (showNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as Group)
.getNumberOfChildEntries(*entryFilters)
.toString()
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
visibility = View.VISIBLE
}
} else {
holder.numberChildren?.visibility = View.GONE
}
}
// Assign click
holder.container.setOnClickListener {
nodeClickCallback?.onNodeClick(subNode)
@@ -307,45 +350,6 @@ class NodeAdapter
}
holder.container.isSelected = actionNodesList.contains(subNode)
// Add subText with username
holder.subText.apply {
text = ""
paintFlags = if (subNode.isCurrentlyExpires)
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
else
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
visibility = View.GONE
if (subNode.type == Type.ENTRY) {
val entry = subNode as Entry
mDatabase.startManageEntry(entry)
holder.text.text = entry.getVisualTitle()
val username = entry.username
if (showUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE
text = username
setTextSize(textSizeUnit, subtextSize)
}
mDatabase.stopManageEntry(entry)
}
}
// Add number of entries in groups
if (subNode.type == Type.GROUP) {
if (showNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as Group).getChildEntries(true).size.toString()
setTextSize(textSizeUnit, numberChildrenTextSize)
visibility = View.VISIBLE
}
} else {
holder.numberChildren?.visibility = View.GONE
}
}
}
override fun getItemCount(): Int {

View File

@@ -28,19 +28,18 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.cursor.EntryCursor
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
import com.kunzisoft.keepass.view.strikeOut
class SearchEntryCursorAdapter(context: Context, private val database: Database)
class SearchEntryCursorAdapter(private val context: Context,
private val database: Database)
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private val cursorInflater: LayoutInflater? = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
private var displayUsername: Boolean = false
private val iconColor: Int
@@ -59,7 +58,7 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false)
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
val viewHolder = ViewHolder()
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
@@ -71,34 +70,31 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
override fun bindView(view: View, context: Context, cursor: Cursor) {
// Retrieve elements from cursor
val uuid = UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))
val iconFactory = database.iconFactory
var icon: IconImage = iconFactory.getIcon(
UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
if (icon.isUnknown) {
icon = iconFactory.getIcon(cursor.getInt(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_STANDARD)))
if (icon.isUnknown)
icon = iconFactory.keyIcon
}
val title = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_TITLE))
val username = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_USERNAME))
val url = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_URL))
database.getEntryFrom(cursor)?.let { currentEntry ->
val viewHolder = view.tag as ViewHolder
// Assign image
viewHolder.imageViewIcon?.assignDatabaseIcon(database.drawFactory, icon, iconColor)
viewHolder.imageViewIcon?.assignDatabaseIcon(
database.drawFactory,
currentEntry.icon,
iconColor)
// Assign title
val showTitle = Entry.getVisualTitle(false, title, username, url, uuid.toString())
viewHolder.textViewTitle?.text = showTitle
if (displayUsername && username.isNotEmpty()) {
viewHolder.textViewSubTitle?.text = String.format("(%s)", username)
viewHolder.textViewTitle?.apply {
text = currentEntry.getVisualTitle()
strikeOut(currentEntry.isCurrentlyExpires)
}
// Assign subtitle
viewHolder.textViewSubTitle?.apply {
val entryUsername = currentEntry.username
text = if (displayUsername && entryUsername.isNotEmpty()) {
String.format("(%s)", entryUsername)
} else {
viewHolder.textViewSubTitle?.text = ""
""
}
strikeOut(currentEntry.isCurrentlyExpires)
}
}
}
@@ -109,7 +105,7 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
}
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
return database.searchEntries(constraint.toString())
return database.searchEntries(context, constraint.toString())
}
fun getEntryFromPosition(position: Int): Entry? {

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import android.os.AsyncTask

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import androidx.room.Database

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import android.content.Context

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import androidx.room.*

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import android.os.Parcel

View File

@@ -112,6 +112,14 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
).execute()
}
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
ActionDatabaseAsyncTask(
{
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
}
).execute()
}
fun deleteAllKeyFiles() {
ActionDatabaseAsyncTask(
{

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import androidx.room.*
@@ -19,6 +38,9 @@ interface FileDatabaseHistoryDao {
@Delete
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
@Query("UPDATE file_database_history SET keyfile_uri=null WHERE database_uri = :databaseUriString")
fun deleteKeyFileByDatabaseUri(databaseUriString: String)
@Query("UPDATE file_database_history SET keyfile_uri=null")
fun deleteAllKeyFiles()

View File

@@ -1,3 +1,22 @@
/*
* 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.app.database
import androidx.room.ColumnInfo

View File

@@ -26,15 +26,14 @@ import android.content.Intent
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.FillResponse
import androidx.annotation.RequiresApi
import android.util.Log
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.model.EntryInfo
import java.util.*
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -56,10 +55,10 @@ object AutofillHelper {
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
if (entryInfo.title.isNotEmpty())
return entryInfo.title
if (entryInfo.username.isNotEmpty())
return entryInfo.username
if (entryInfo.url.isNotEmpty())
return entryInfo.url
if (entryInfo.username.isNotEmpty())
return entryInfo.username
return ""
}
@@ -71,12 +70,12 @@ object AutofillHelper {
val builder = Dataset.Builder(views)
builder.setId(entryInfo.id)
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) }
val ids = ArrayList(struct.username)
if (entryInfo.username.contains("@") || struct.username.isEmpty())
ids.addAll(struct.email)
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) }
struct.usernameId?.let { usernameId ->
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
}
struct.passwordId?.let { password ->
builder.setValue(password, AutofillValue.forText(entryInfo.password))
}
return try {
builder.build()

View File

@@ -31,7 +31,6 @@ import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -42,9 +41,11 @@ class AutofillLauncherActivity : AppCompatActivity() {
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
if (assistStructure != null) {
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this))
GroupActivity.launchForAutofillResult(this,
assistStructure)
else {
FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure)
FileDatabaseSelectActivity.launchForAutofillResult(this,
assistStructure)
}
} else {
setResult(Activity.RESULT_CANCELED)

View File

@@ -35,13 +35,13 @@ class KeeAutofillService : AutofillService() {
val fillContexts = request.fillContexts
val latestStructure = fillContexts[fillContexts.size - 1].structure
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") }
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
val responseBuilder = FillResponse.Builder()
// Check user's settings for authenticating Responses and Datasets.
val parseResult = StructureParser(latestStructure).parse()
parseResult?.allAutofillIds()?.let { autofillIds ->
if (listOf(*autofillIds).isNotEmpty()) {
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this)

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
import android.os.Build
import androidx.annotation.RequiresApi
import android.text.InputType
import android.util.Log
import android.view.View
import android.view.autofill.AutofillId
@@ -40,69 +39,130 @@ internal class StructureParser(private val structure: AssistStructure) {
result = Result()
result?.apply {
usernameCandidate = null
for (i in 0 until structure.windowNodeCount) {
mainLoop@ for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i)
/*
title.add(windowNode.title)
windowNode.rootViewNode.webDomain?.let {
webDomain.add(it)
}
parseViewNode(windowNode.rootViewNode)
*/
if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
}
// If not explicit username field found, add the field just before password field.
if (username.isEmpty() && email.isEmpty()
&& password.isNotEmpty() && usernameCandidate != null)
username.add(usernameCandidate!!)
if (usernameId == null && passwordId != null && usernameCandidate != null)
usernameId = usernameCandidate
}
return result
// Return the result only if password field is retrieved
return if (result?.passwordId != null)
result
else
null
}
private fun parseViewNode(node: AssistStructure.ViewNode) {
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
if (node.autofillId != null) {
val hints = node.autofillHints
val autofillId = node.autofillId
if (autofillId != null) {
if (hints != null && hints.isNotEmpty()) {
when {
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId)
else -> Log.d(TAG, "unsupported hints")
}
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) {
val inputType = node.inputType
when {
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId)
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId)
result?.password?.isEmpty() == true -> usernameCandidate = autofillId
if (parseNodeByAutofillHint(node))
return true
} else {
if (parseNodeByHtmlAttributes(node))
return true
}
}
// Recursive method to process each node
for (i in 0 until node.childCount) {
if (parseViewNode(node.getChildAt(i)))
return true
}
return false
}
for (i in 0 until node.childCount)
parseViewNode(node.getChildAt(i))
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
node.autofillHints?.forEach {
when {
it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_USERNAME
|| it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_EMAIL_ADDRESS
|| it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_PHONE -> {
result?.usernameId = autofillId
Log.d(TAG, "Autofill username hint")
}
it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_PASSWORD
|| it.toLowerCase(Locale.ENGLISH).contains("password") -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password hint")
return true
}
// Ignore autocomplete="off"
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
it.toLowerCase(Locale.ENGLISH) == "off" ||
it.toLowerCase(Locale.ENGLISH) == "on" -> {
Log.d(TAG, "Autofill web hint")
return parseNodeByHtmlAttributes(node)
}
else -> Log.d(TAG, "Autofill unsupported hint $it")
}
}
return false
}
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
val nodHtml = node.htmlInfo
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
"input" -> {
nodHtml.attributes?.forEach { pairAttribute ->
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
"type" -> {
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
"tel", "email" -> {
result?.usernameId = autofillId
Log.d(TAG, "Autofill username type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"text" -> {
usernameCandidate = autofillId
Log.d(TAG, "Autofill type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"password" -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
return true
}
}
}
}
}
}
}
return false
}
@RequiresApi(api = Build.VERSION_CODES.O)
internal class Result {
val title: MutableList<CharSequence>
val webDomain: MutableList<String>
val username: MutableList<AutofillId>
val email: MutableList<AutofillId>
val password: MutableList<AutofillId>
var usernameId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
init {
title = ArrayList()
webDomain = ArrayList()
username = ArrayList()
email = ArrayList()
password = ArrayList()
var passwordId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>()
all.addAll(username)
all.addAll(email)
all.addAll(password)
usernameId?.let {
all.add(it)
}
passwordId?.let {
all.add(it)
}
return all.toTypedArray()
}
}

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.biometric
import android.content.Intent
@@ -294,8 +313,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
override fun onBiometricException(e: Exception) {
if (e.localizedMessage != null)
setAdvancedUnlockedMessageView(e.localizedMessage)
e.localizedMessage?.let {
setAdvancedUnlockedMessageView(it)
}
}
private fun showFingerPrintViews(show: Boolean) {

View File

@@ -95,7 +95,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
// really not much to do when no fingerprint support found
isKeyManagerInit = false
} else {
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
try {
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)

View File

@@ -19,15 +19,13 @@
*/
package com.kunzisoft.keepass.crypto
import com.kunzisoft.keepass.stream.LEDataOutputStream
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.Arrays
import java.util.*
import javax.crypto.Mac
import kotlin.math.min
@@ -60,7 +58,7 @@ object CryptoUtil {
throw RuntimeException(e)
}
val pbR = LEDataOutputStream.writeLongBuf(r)
val pbR = longTo8Bytes(r)
val part = hmac.doFinal(pbR)
val copy = min(cbOut - pos, part.size)

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.crypto.engine
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
@@ -47,7 +47,22 @@ class AesEngine : CipherEngine() {
companion object {
val CIPHER_UUID: UUID = DatabaseInputOutputUtils.bytesToUuid(
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()))
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

@@ -20,7 +20,7 @@
package com.kunzisoft.keepass.crypto.engine
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import com.kunzisoft.keepass.stream.bytes16ToUuid
import org.spongycastle.jce.provider.BouncyCastleProvider
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
@@ -50,7 +50,22 @@ class ChaCha20Engine : CipherEngine() {
companion object {
val CIPHER_UUID: UUID = DatabaseInputOutputUtils.bytesToUuid(
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()))
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

@@ -21,13 +21,11 @@ package com.kunzisoft.keepass.crypto.engine
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.UUID
import java.util.*
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
@@ -53,7 +51,22 @@ class TwofishEngine : CipherEngine() {
companion object {
val CIPHER_UUID: UUID = DatabaseInputOutputUtils.bytesToUuid(
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()))
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

@@ -23,7 +23,7 @@ import android.content.res.Resources
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CryptoUtil
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.io.IOException
import java.security.SecureRandom
import java.util.*
@@ -88,7 +88,7 @@ class AesKdf internal constructor() : KdfEngine() {
private const val DEFAULT_ROUNDS = 6000
val CIPHER_UUID: UUID = DatabaseInputOutputUtils.bytesToUuid(
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0xC9.toByte(),
0xD9.toByte(),
0xF3.toByte(),

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.crypto.keyDerivation
import android.content.res.Resources
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import com.kunzisoft.keepass.stream.bytes16ToUuid
import java.io.IOException
import java.security.SecureRandom
import java.util.*
@@ -126,7 +126,7 @@ class Argon2Kdf internal constructor() : KdfEngine() {
companion object {
val CIPHER_UUID: UUID = DatabaseInputOutputUtils.bytesToUuid(
val CIPHER_UUID: UUID = bytes16ToUuid(
byteArrayOf(0xEF.toByte(),
0x63.toByte(),
0x6D.toByte(),

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -19,20 +19,20 @@
*/
package com.kunzisoft.keepass.crypto.keyDerivation
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
import com.kunzisoft.keepass.stream.bytes16ToUuid
import com.kunzisoft.keepass.stream.uuidTo16Bytes
import com.kunzisoft.keepass.utils.VariantDictionary
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.UUID
import java.util.*
class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
fun setParamUUID() {
setByteArray(PARAM_UUID, DatabaseInputOutputUtils.uuidToBytes(uuid))
setByteArray(PARAM_UUID, uuidTo16Bytes(uuid))
}
companion object {
@@ -42,11 +42,11 @@ class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
@Throws(IOException::class)
fun deserialize(data: ByteArray): KdfParameters? {
val bis = ByteArrayInputStream(data)
val lis = LEDataInputStream(bis)
val lis = LittleEndianDataInputStream(bis)
val d = deserialize(lis) ?: return null
val uuid = DatabaseInputOutputUtils.bytesToUuid(d.getByteArray(PARAM_UUID))
val uuid = bytes16ToUuid(d.getByteArray(PARAM_UUID))
val kdfP = KdfParameters(uuid)
kdfP.copyTo(d)
@@ -56,7 +56,7 @@ class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
@Throws(IOException::class)
fun serialize(kdf: KdfParameters): ByteArray {
val bos = ByteArrayOutputStream()
val los = LEDataOutputStream(bos)
val los = LittleEndianDataOutputStream(bos)
serialize(kdf, los)

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil
@@ -70,6 +71,9 @@ open class AssignPasswordInDatabaseRunnable (
// Erase the biometric
CipherDatabaseAction.getInstance(context)
.deleteByDatabaseUri(mDatabaseUri)
// Erase the register keyfile
FileDatabaseHistoryAction.getInstance(context)
.deleteKeyFileByDatabaseUri(mDatabaseUri)
if (!result.isSuccess) {
// Erase the current master key

View File

@@ -24,6 +24,7 @@ import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
class CreateDatabaseRunnable(context: Context,
private val mDatabase: Database,
@@ -57,8 +58,11 @@ class CreateDatabaseRunnable(context: Context,
if (result.isSuccess) {
// Add database to recent files
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri, mKeyFile)
.addOrUpdateDatabaseUri(mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFile else null)
}
} else {
Log.e("CreateDatabaseRunnable", "Unable to create the database")
}

View File

@@ -74,14 +74,10 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onFinishRun() {
if (result.isSuccess) {
// Save keyFile in app database
val rememberKeyFile = PreferencesUtil.rememberKeyFiles(context)
if (rememberKeyFile) {
var keyUri = mKey
if (!rememberKeyFile) {
keyUri = null
}
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri, keyUri)
.addOrUpdateDatabaseUri(mUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null)
}
// Register the biometric
@@ -90,8 +86,11 @@ class LoadDatabaseRunnable(private val context: Context,
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
}
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
// Start the opening notification
DatabaseOpenNotificationService.startIfAllowed(context)
DatabaseOpenNotificationService.start(context)
} else {
mDatabase.closeAndClear(cacheDirectory)
}

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action
import android.content.*
@@ -8,6 +27,7 @@ import android.os.Build
import android.os.Bundle
import android.os.IBinder
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.*
@@ -16,15 +36,18 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
@@ -64,12 +87,17 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout(activity)
TimeoutHelper.temporarilyDisableTimeout()
// Stop the opening notification
DatabaseOpenNotificationService.stop(activity)
startOrUpdateDialog(titleId, messageId, warningId)
}
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout(activity)
TimeoutHelper.temporarilyDisableTimeout()
// Stop the opening notification
DatabaseOpenNotificationService.stop(activity)
startOrUpdateDialog(titleId, messageId, warningId)
}
@@ -77,7 +105,18 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
onActionFinish?.invoke(actionTask, result)
// Remove the progress task
ProgressTaskDialogFragment.stop(activity)
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
TimeoutHelper.releaseTemporarilyDisableTimeout()
val inTime = if (activity is LockingActivity) {
TimeoutHelper.checkTimeAndLockIfTimeout(activity)
} else {
TimeoutHelper.checkTime(activity)
}
// Start the opening notification if in time
// (databaseOpenService is open manually in Action Open Task)
if (actionTask != ACTION_DATABASE_LOAD_TASK && inTime) {
DatabaseOpenNotificationService.start(activity)
}
}
}
@@ -105,7 +144,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
if (serviceConnection == null) {
serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder).apply {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addActionTaskListener(actionTaskListener)
getService().checkAction()
}
@@ -178,7 +217,11 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
unBindService()
try {
activity.unregisterReceiver(databaseTaskBroadcastReceiver)
} catch (e: IllegalArgumentException) {
// If receiver not register, do nothing
}
}
@Synchronized
@@ -348,6 +391,34 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
}
/*
-----------------
Entry History Settings
-----------------
*/
fun startDatabaseRestoreEntryHistory(mainEntry: Entry,
entryHistoryPosition: Int,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
}
fun startDatabaseDeleteEntryHistory(mainEntry: Entry,
entryHistoryPosition: Int,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
}
/*
-----------------
Main Settings

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action.history
import android.content.Context
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
class DeleteEntryHistoryDatabaseRunnable (
context: Context,
database: Database,
private val mainEntry: Entry,
private val entryHistoryPosition: Int,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
override fun onStartRun() {
try {
mainEntry.removeEntryFromHistory(entryHistoryPosition)
} catch (e: Exception) {
setError(e)
}
super.onStartRun()
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.action.history
import android.content.Context
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.tasks.ActionRunnable
class RestoreEntryHistoryDatabaseRunnable (
private val context: Context,
private val database: Database,
private val mainEntry: Entry,
private val entryHistoryPosition: Int,
private val saveDatabase: Boolean)
: ActionRunnable() {
private var updateEntryRunnable: UpdateEntryRunnable? = null
override fun onStartRun() {
try {
val historyToRestore = Entry(mainEntry.getHistory()[entryHistoryPosition])
// Copy history of main entry in the restore entry
mainEntry.getHistory().forEach {
historyToRestore.addEntryToHistory(it)
}
// Update the entry with the fresh formatted entry to restore
updateEntryRunnable = UpdateEntryRunnable(context,
database,
mainEntry,
historyToRestore,
saveDatabase,
null)
updateEntryRunnable?.onStartRun()
} catch (e: Exception) {
setError(e)
}
}
override fun onActionRun() {
updateEntryRunnable?.onActionRun()
}
override fun onFinishRun() {
updateEntryRunnable?.onFinishRun()
}
}

View File

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

View File

@@ -1,10 +1,31 @@
/*
* 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.cursor
import android.database.MatrixCursor
import android.provider.BaseColumns
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
import com.kunzisoft.keepass.database.element.node.NodeId
import java.util.*
abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>> : MatrixCursor(arrayOf(
_ID,
@@ -17,7 +38,9 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
COLUMN_INDEX_USERNAME,
COLUMN_INDEX_PASSWORD,
COLUMN_INDEX_URL,
COLUMN_INDEX_NOTES
COLUMN_INDEX_NOTES,
COLUMN_INDEX_EXPIRY_TIME,
COLUMN_INDEX_EXPIRES
)) {
protected var entryId: Long = 0
@@ -37,6 +60,9 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))
pwEntry.url = getString(getColumnIndex(COLUMN_INDEX_URL))
pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES))
pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME)))
pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES))
.toLowerCase(Locale.ENGLISH) != "false"
}
companion object {
@@ -51,5 +77,7 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
const val COLUMN_INDEX_PASSWORD = "password"
const val COLUMN_INDEX_URL = "URL"
const val COLUMN_INDEX_NOTES = "notes"
const val COLUMN_INDEX_EXPIRY_TIME = "expiry_time"
const val COLUMN_INDEX_EXPIRES = "expires"
}
}

View File

@@ -1,3 +1,22 @@
/*
* 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.cursor
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
@@ -17,7 +36,9 @@ class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
entry.username,
entry.password,
entry.url,
entry.notes
entry.notes,
entry.expiryTime,
entry.expires
))
entryId++
}

View File

@@ -1,3 +1,22 @@
/*
* 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.cursor
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -21,7 +40,9 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
entry.username,
entry.password,
entry.url,
entry.notes
entry.notes,
entry.expiryTime,
entry.expires
))
for (element in entry.customFields.entries) {

View File

@@ -1,3 +1,22 @@
/*
* 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.cursor
import com.kunzisoft.keepass.database.element.entry.EntryVersioned

View File

@@ -1,3 +1,22 @@
/*
* 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.cursor
import android.database.MatrixCursor

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.element
import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.database.Cursor
import android.net.Uri
@@ -36,7 +37,10 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt
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.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
@@ -45,7 +49,7 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.stream.readBytes4ToInt
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil
@@ -324,47 +328,37 @@ class Database {
isReadOnly = !file.canWrite()
}
// Pass Uris as InputStreams
val inputStream: InputStream?
try {
inputStream = UriUtil.getUriInputStream(contentResolver, uri)
} catch (e: Exception) {
Log.e("KPD", "Database::loadData", e)
throw FileNotFoundDatabaseException()
}
// Pass KeyFile Uri as InputStreams
var databaseInputStream: InputStream? = null
var keyFileInputStream: InputStream? = null
keyfile?.let {
try {
// Get keyFile inputStream
keyfile?.let {
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
} catch (e: Exception) {
Log.e("KPD", "Database::loadData", e)
throw FileNotFoundDatabaseException()
}
}
// Load Data
val bufferedInputStream = BufferedInputStream(inputStream)
if (!bufferedInputStream.markSupported()) {
// Load Data, pass Uris as InputStreams
databaseInputStream = BufferedInputStream(UriUtil.getUriInputStream(contentResolver, uri))
if (!databaseInputStream.markSupported()) {
throw IOException("Input stream does not support mark.")
}
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
bufferedInputStream.mark(10)
databaseInputStream.mark(10)
// Get the file directory to save the attachments
val sig1 = LEDataInputStream.readInt(bufferedInputStream)
val sig2 = LEDataInputStream.readInt(bufferedInputStream)
val sig1 = databaseInputStream.readBytes4ToInt()
val sig2 = databaseInputStream.readBytes4ToInt()
// Return to the start
bufferedInputStream.reset()
databaseInputStream.reset()
when {
// Header of database KDB
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(DatabaseInputKDB()
.openDatabase(bufferedInputStream,
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(DatabaseInputKDB(
cacheDirectory,
fixDuplicateUUID)
.openDatabase(databaseInputStream,
password,
keyFileInputStream,
progressTaskUpdater))
@@ -373,7 +367,7 @@ class Database {
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(DatabaseInputKDBX(
cacheDirectory,
fixDuplicateUUID)
.openDatabase(bufferedInputStream,
.openDatabase(databaseInputStream,
password,
keyFileInputStream,
progressTaskUpdater))
@@ -384,6 +378,17 @@ class Database {
this.mSearchHelper = SearchHelper(omitBackup)
loaded = true
} catch (e: LoadDatabaseException) {
Log.e("KPD", "Database::loadData", e)
throw e
} catch (e: Exception) {
Log.e("KPD", "Database::loadData", e)
throw FileNotFoundDatabaseException()
} finally {
keyFileInputStream?.close()
databaseInputStream?.close()
}
}
fun isGroupSearchable(group: Group, isOmitBackup: Boolean): Boolean {
@@ -397,7 +402,7 @@ class Database {
return mSearchHelper?.search(this, str, max)
}
fun searchEntries(query: String): Cursor? {
fun searchEntries(context: Context, query: String): Cursor? {
var cursorKDB: EntryCursorKDB? = null
var cursorKDBX: EntryCursorKDBX? = null
@@ -409,7 +414,8 @@ class Database {
val searchResult = search(query, SearchHelper.MAX_SEARCH_ENTRY)
if (searchResult != null) {
for (entry in searchResult.getChildEntries(true)) {
// Search in hide entries but not meta-stream
for (entry in searchResult.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
entry.entryKDB?.let {
cursorKDB?.addEntry(it)
}
@@ -424,25 +430,21 @@ class Database {
fun getEntryFrom(cursor: Cursor): Entry? {
val iconFactory = mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
val entry = createEntry()
// TODO invert field reference manager
entry?.let { entryVersioned ->
startManageEntry(entryVersioned)
return createEntry()?.apply {
startManageEntry(this)
mDatabaseKDB?.let {
entryVersioned.entryKDB?.let { entryKDB ->
entryKDB?.let { entryKDB ->
(cursor as EntryCursorKDB).populateEntry(entryKDB, iconFactory)
}
}
mDatabaseKDBX?.let {
entryVersioned.entryKDBX?.let { entryKDBX ->
entryKDBX?.let { entryKDBX ->
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, iconFactory)
}
}
stopManageEntry(entryVersioned)
stopManageEntry(this)
}
return entry
}
@Throws(DatabaseOutputException::class)
@@ -491,10 +493,11 @@ class Database {
var outputStream: OutputStream? = null
try {
outputStream = contentResolver.openOutputStream(uri)
val pmo =
mDatabaseKDB?.let { DatabaseOutputKDB(it, outputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, outputStream) }
pmo?.output()
outputStream?.let { definedOutputStream ->
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
databaseOutput?.output()
}
} catch (e: Exception) {
throw IOException(e)
} finally {
@@ -721,7 +724,7 @@ class Database {
fun canRecycle(entry: Entry): Boolean {
var canRecycle: Boolean? = null
entry.entryKDB?.let {
canRecycle = mDatabaseKDB?.canRecycle(it)
canRecycle = mDatabaseKDB?.canRecycle()
}
entry.entryKDBX?.let {
canRecycle = mDatabaseKDBX?.canRecycle(it)
@@ -732,7 +735,7 @@ class Database {
fun canRecycle(group: Group): Boolean {
var canRecycle: Boolean? = null
group.groupKDB?.let {
canRecycle = mDatabaseKDB?.canRecycle(it)
canRecycle = mDatabaseKDB?.canRecycle()
}
group.groupKDBX?.let {
canRecycle = mDatabaseKDBX?.canRecycle(it)

View File

@@ -23,6 +23,7 @@ import android.content.res.Resources
import android.os.Parcel
import android.os.Parcelable
import androidx.core.os.ConfigurationCompat
import java.text.SimpleDateFormat
import java.util.*
class DateInstant : Parcelable {
@@ -44,6 +45,10 @@ class DateInstant : Parcelable {
jDate = Date(millis)
}
constructor(string: String) {
jDate = dateFormat.parse(string) ?: jDate
}
constructor() {
jDate = Date()
}
@@ -84,12 +89,13 @@ class DateInstant : Parcelable {
}
override fun toString(): String {
return jDate.toString()
return dateFormat.format(jDate)
}
companion object {
val NEVER_EXPIRE = neverExpire
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
private val neverExpire: DateInstant
get() {
@@ -115,7 +121,7 @@ class DateInstant : Parcelable {
}
}
private fun isSameDate(d1: Date?, d2: Date?): Boolean {
private fun isSameDate(d1: Date, d2: Date): Boolean {
val cal1 = Calendar.getInstance()
cal1.time = d1
cal1.set(Calendar.MILLISECOND, 0)
@@ -136,7 +142,7 @@ class DateInstant : Parcelable {
fun getDateTimeString(resources: Resources, date: Date): String {
return java.text.DateFormat.getDateTimeInstance(
java.text.DateFormat.MEDIUM,
java.text.DateFormat.MEDIUM,
java.text.DateFormat.SHORT,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(date)
}

View File

@@ -26,17 +26,25 @@ import java.util.UUID
class DeletedObject {
var uuid: UUID = DatabaseVersioned.UUID_ZERO
var deletionTime: Date? = null
get() = if (field == null) {
Date(System.currentTimeMillis())
} else field
private var mDeletionTime: Date? = null
fun getDeletionTime(): Date {
if (mDeletionTime == null) {
mDeletionTime = Date(System.currentTimeMillis())
}
return mDeletionTime!!
}
fun setDeletionTime(deletionTime: Date) {
this.mDeletionTime = deletionTime
}
constructor()
@JvmOverloads
constructor(uuid: UUID, deletionTime: Date = Date()) {
this.uuid = uuid
this.deletionTime = deletionTime
this.mDeletionTime = deletionTime
}
override fun equals(other: Any?): Boolean {

View File

@@ -1,3 +1,22 @@
/*
* 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
import android.os.Parcel
@@ -13,7 +32,9 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement
@@ -139,6 +160,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
return contained ?: false
}
override fun nodeIndexInParentForNaturalOrder(): Int {
return entryKDB?.nodeIndexInParentForNaturalOrder()
?: entryKDBX?.nodeIndexInParentForNaturalOrder()
?: -1
}
override var creationTime: DateInstant
get() = entryKDB?.creationTime ?: entryKDBX?.creationTime ?: DateInstant()
set(value) {
@@ -209,12 +236,27 @@ class Entry : Node, EntryVersionedInterface<Group> {
return title == PMS_TAN_ENTRY && username.isNotEmpty()
}
/**
* {@inheritDoc}
* Get the display title from an entry, <br></br>
* [.startManageEntry] and [.stopManageEntry] must be called
* before and after [.getVisualTitle]
*/
fun getVisualTitle(): String {
return getVisualTitle(isTan(),
title,
username,
url,
nodeId.toString())
return if (isTan()) {
"$PMS_TAN_ENTRY $username"
} else {
if (title.isEmpty())
if (url.isEmpty())
if (username.isEmpty())
nodeId.toString()
else
username
else
url
else
title
}
}
/*
@@ -284,10 +326,29 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.stopToManageFieldReferences()
}
fun getAttachments(): ArrayList<EntryAttachment> {
val attachments = ArrayList<EntryAttachment>()
val binaryDescriptionKDB = entryKDB?.binaryDescription ?: ""
val binaryKDB = entryKDB?.binaryData
if (binaryKDB != null) {
attachments.add(EntryAttachment(binaryDescriptionKDB, binaryKDB))
}
val actionEach = object : (Map.Entry<String, BinaryAttachment>)->Unit {
override fun invoke(mapEntry: Map.Entry<String, BinaryAttachment>) {
attachments.add(EntryAttachment(mapEntry.key, mapEntry.value))
}
}
entryKDBX?.binaries?.forEach(actionEach)
return attachments
}
fun getHistory(): ArrayList<Entry> {
val history = ArrayList<Entry>()
val entryV4History = entryKDBX?.history ?: ArrayList()
for (entryHistory in entryV4History) {
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
for (entryHistory in entryKDBXHistory) {
history.add(Entry(entryHistory))
}
return history
@@ -299,6 +360,10 @@ class Entry : Node, EntryVersionedInterface<Group> {
}
}
fun removeEntryFromHistory(position: Int) {
entryKDBX?.removeEntryFromHistory(position)
}
fun removeAllHistory() {
entryKDBX?.removeAllHistory()
}
@@ -379,28 +444,5 @@ class Entry : Node, EntryVersionedInterface<Group> {
}
const val PMS_TAN_ENTRY = "<TAN>"
/**
* {@inheritDoc}
* Get the display title from an entry, <br></br>
* [.startManageEntry] and [.stopManageEntry] must be called
* before and after [.getVisualTitle]
*/
fun getVisualTitle(isTan: Boolean, title: String, userName: String, url: String, id: String): String {
return if (isTan) {
"$PMS_TAN_ENTRY $userName"
} else {
if (title.isEmpty())
if (userName.isEmpty())
if (url.isEmpty())
id
else
url
else
userName
else
title
}
}
}
}

View File

@@ -1,5 +1,25 @@
/*
* 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
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.group.GroupKDB
@@ -8,6 +28,7 @@ import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
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.settings.PreferencesUtil
import java.util.*
import kotlin.collections.ArrayList
@@ -57,6 +78,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader)
}
enum class ChildFilter {
META_STREAM, EXPIRED;
companion object {
fun getDefaults(context: Context): Array<ChildFilter> {
return if (PreferencesUtil.showExpiredEntries(context)) {
arrayOf(META_STREAM)
} else {
arrayOf(META_STREAM, EXPIRED)
}
}
}
}
companion object CREATOR : Parcelable.Creator<Group> {
override fun createFromParcel(parcel: Parcel): Group {
return Group(parcel)
@@ -152,6 +187,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
return contained ?: false
}
override fun nodeIndexInParentForNaturalOrder(): Int {
return groupKDB?.nodeIndexInParentForNaturalOrder()
?: groupKDBX?.nodeIndexInParentForNaturalOrder()
?: -1
}
override var creationTime: DateInstant
get() = groupKDB?.creationTime ?: groupKDBX?.creationTime ?: DateInstant()
set(value) {
@@ -190,55 +231,58 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
override val isCurrentlyExpires: Boolean
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
override fun getChildGroups(): MutableList<Group> {
val children = ArrayList<Group>()
groupKDB?.getChildGroups()?.forEach {
children.add(Group(it))
}
groupKDBX?.getChildGroups()?.forEach {
children.add(Group(it))
override fun getChildGroups(): List<Group> {
return groupKDB?.getChildGroups()?.map {
Group(it)
} ?:
groupKDBX?.getChildGroups()?.map {
Group(it)
} ?:
ArrayList()
}
return children
override fun getChildEntries(): List<Entry> {
return groupKDB?.getChildEntries()?.map {
Entry(it)
} ?:
groupKDBX?.getChildEntries()?.map {
Entry(it)
} ?:
ArrayList()
}
override fun getChildEntries(): MutableList<Entry> {
return getChildEntries(false)
fun getFilteredChildEntries(vararg filter: ChildFilter): List<Entry> {
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)
return groupKDB?.getChildEntries()?.filter {
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
&& (!it.isCurrentlyExpires or showExpiredEntries)
}?.map {
Entry(it)
} ?:
groupKDBX?.getChildEntries()?.filter {
!it.isCurrentlyExpires or showExpiredEntries
}?.map {
Entry(it)
} ?:
ArrayList()
}
fun getChildEntries(withoutMetaStream: Boolean): MutableList<Entry> {
val children = ArrayList<Entry>()
groupKDB?.getChildEntries()?.forEach {
val entryToAddAsChild = Entry(it)
if (!withoutMetaStream || (withoutMetaStream && !entryToAddAsChild.isMetaStream))
children.add(entryToAddAsChild)
}
groupKDBX?.getChildEntries()?.forEach {
children.add(Entry(it))
}
return children
fun getNumberOfChildEntries(vararg filter: ChildFilter): Int {
return getFilteredChildEntries(*filter).size
}
/**
* Filter MetaStream entries and return children
* Filter entries and return children
* @return List of direct children (one level below) as NodeVersioned
*/
fun getChildren(withoutMetaStream: Boolean = true): List<Node> {
val children = ArrayList<Node>()
children.addAll(getChildGroups())
groupKDB?.let {
children.addAll(getChildEntries(withoutMetaStream))
}
groupKDBX?.let {
// No MetasStream in V4
children.addAll(getChildEntries(withoutMetaStream))
fun getChildren(): List<Node> {
return getChildGroups() + getChildEntries()
}
return children
fun getFilteredChildren(vararg filter: ChildFilter): List<Node> {
return getChildGroups() + getFilteredChildEntries(*filter)
}
override fun addChildGroup(group: Group) {

View File

@@ -20,149 +20,198 @@
package com.kunzisoft.keepass.database.element
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type
import java.util.*
enum class SortNodeEnum {
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
fun getNodeComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean): Comparator<Node> {
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(sortNodeParameters: SortNodeParameters)
: Comparator<NodeVersionedInterface<G>> {
return when (this) {
DB -> NodeNaturalComparator(ascending, groupsBefore, false) // Force false because natural order contains recycle bin
TITLE -> NodeTitleComparator(ascending, groupsBefore, recycleBinBottom)
USERNAME -> NodeUsernameComparator(ascending, groupsBefore, recycleBinBottom)
CREATION_TIME -> NodeCreationComparator(ascending, groupsBefore, recycleBinBottom)
LAST_MODIFY_TIME -> NodeLastModificationComparator(ascending, groupsBefore, recycleBinBottom)
LAST_ACCESS_TIME -> NodeLastAccessComparator(ascending, groupsBefore, recycleBinBottom)
DB -> NodeNaturalComparator(sortNodeParameters) // Force false because natural order contains recycle bin
TITLE -> NodeTitleComparator(sortNodeParameters)
USERNAME -> NodeUsernameComparator(sortNodeParameters)
CREATION_TIME -> NodeCreationComparator(sortNodeParameters)
LAST_MODIFY_TIME -> NodeLastModificationComparator(sortNodeParameters)
LAST_ACCESS_TIME -> NodeLastAccessComparator(sortNodeParameters)
}
}
abstract class NodeComparator(var ascending: Boolean, var groupsBefore: Boolean, var recycleBinBottom: Boolean) : Comparator<Node> {
data class SortNodeParameters(var ascending: Boolean = true,
var groupsBefore: Boolean = true,
var recycleBinBottom: Boolean = true)
abstract fun compareBySpecificOrder(object1: Node, object2: Node): Int
abstract class NodeComparator
<
G: GroupVersionedInterface<*, *>,
T: NodeVersionedInterface<G>
>(var sortNodeParameters: SortNodeParameters)
: Comparator<T> {
private fun specificOrderOrHashIfEquals(object1: Node, object2: Node): Int {
val database = Database.getInstance()
abstract fun compareBySpecificOrder(object1: T, object2: T): Int
private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int {
val specificOrderComp = compareBySpecificOrder(object1, object2)
return if (specificOrderComp == 0) {
object1.hashCode() - object2.hashCode()
} else if (!ascending) -specificOrderComp else specificOrderComp // If descending, revert
return when {
specificOrderComp == 0 -> object1.hashCode() - object2.hashCode()
sortNodeParameters.ascending -> specificOrderComp
else -> -specificOrderComp
}
}
override fun compare(object1: Node, object2: Node): Int {
override fun compare(object1: T, object2: T): Int {
if (object1 == object2)
return 0
if (object1.type == Type.GROUP) {
return if (object2.type == Type.GROUP) {
when (object1.type) {
Type.GROUP -> {
when (object2.type) {
Type.GROUP -> {
// RecycleBin at end of groups
val database = Database.getInstance()
if (database.isRecycleBinEnabled && recycleBinBottom) {
if (database.isRecycleBinEnabled && sortNodeParameters.recycleBinBottom) {
if (database.recycleBin == object1)
return 1
if (database.recycleBin == object2)
return -1
}
specificOrderOrHashIfEquals(object1, object2)
} else if (object2.type == Type.ENTRY) {
if (groupsBefore)
return specificOrderOrHashIfEquals(object1, object2)
}
Type.ENTRY -> {
return if (sortNodeParameters.groupsBefore)
-1
else
1
} else {
-1
}
} else if (object1.type == Type.ENTRY) {
return if (object2.type == Type.ENTRY) {
specificOrderOrHashIfEquals(object1, object2)
} else if (object2.type == Type.GROUP) {
if (groupsBefore)
}
}
Type.ENTRY -> {
return when (object2.type) {
Type.GROUP -> {
if (sortNodeParameters.groupsBefore)
1
else
-1
} else {
-1
}
Type.ENTRY -> {
specificOrderOrHashIfEquals(object1, object2)
}
}
}
}
// Type not known
return -1
}
}
/**
* Comparator of node by natural database placement
*/
class NodeNaturalComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.nodePositionInParent.compareTo(object2.nodePositionInParent)
override fun compareBySpecificOrder(object1: T, object2: T): Int {
return object1.nodeIndexInParentForNaturalOrder()
.compareTo(object2.nodeIndexInParentForNaturalOrder())
}
}
/**
* Comparator of Node by Title
*/
class NodeTitleComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.title.compareTo(object2.title, ignoreCase = true)
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
return if (titleCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
titleCompare
}
}
/**
* Comparator of Node by Username, Groups by title
*/
class NodeUsernameComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
// To get username if it's a ref
return (object1 as Entry).getEntryInfo(Database.getInstance()).username
.compareTo((object2 as Entry).getEntryInfo(Database.getInstance()).username,
val usernameCompare = (object1 as Entry).getEntryInfo(database).username
.compareTo((object2 as Entry).getEntryInfo(database).username,
ignoreCase = true)
if (usernameCompare == 0)
NodeTitleComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
usernameCompare
} else {
NodeTitleComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
}
return NodeTitleComparator(ascending, groupsBefore, recycleBinBottom).compare(object1, object2)
}
}
/**
* Comparator of node by creation
*/
class NodeCreationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.creationTime.date
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val creationCompare = object1.creationTime.date
.compareTo(object2.creationTime.date)
return if (creationCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
creationCompare
}
}
/**
* Comparator of node by last modification
*/
class NodeLastModificationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.lastModificationTime.date
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastModificationCompare = object1.lastModificationTime.date
.compareTo(object2.lastModificationTime.date)
return if (lastModificationCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
lastModificationCompare
}
}
/**
* Comparator of node by last access
*/
class NodeLastAccessComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.lastAccessTime.date
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastAccessCompare = object1.lastAccessTime.date
.compareTo(object2.lastAccessTime.date)
return if (lastAccessCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
lastAccessCompare
}
}
}

View File

@@ -22,12 +22,12 @@ package com.kunzisoft.keepass.database.element.database
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
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.IOException
import java.io.InputStream
@@ -231,8 +231,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
* @param node Node to remove
* @return true if node can be recycle, false elsewhere
*/
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
// TODO #394 Backup pw3
// TODO #394 Backup KDB
// fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
fun canRecycle(): Boolean {
return true
}
@@ -267,6 +268,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
private const val DEFAULT_ENCRYPTION_ROUNDS = 300
const val BUFFER_SIZE_BYTES = 3 * 128
/**
* Encrypt the master key a few times to make brute-force key-search harder
* @throws IOException

View File

@@ -181,7 +181,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
CompressionAlgorithm.GZip -> {
// To compress, create a new binary with file
binary.compress()
binary.compress(BUFFER_SIZE_BYTES)
}
}
}
@@ -189,7 +189,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
when (newCompression) {
CompressionAlgorithm.None -> {
// To decompress, create a new binary with file
binary.decompress()
binary.decompress(BUFFER_SIZE_BYTES)
}
CompressionAlgorithm.GZip -> {
}
@@ -562,7 +562,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private const val KeyElementName = "Key"
private const val KeyDataElementName = "Data"
const val BASE_64_FLAG = Base64.DEFAULT
const val BASE_64_FLAG = Base64.NO_WRAP
const val BUFFER_SIZE_BYTES = 3 * 128
}

View File

@@ -26,6 +26,7 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import java.util.*
/**
@@ -51,19 +52,15 @@ import java.util.*
*/
class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface {
/** A string describing what is in pBinaryData */
var binaryDesc = ""
/**
* @return the actual binaryData byte array.
*/
var binaryData: ByteArray = ByteArray(0)
/** A string describing what is in binaryData */
var binaryDescription = ""
var binaryData: BinaryAttachment? = null
// Determine if this is a MetaStream entry
val isMetaStream: Boolean
get() {
if (binaryData.contentEquals(ByteArray(0))) return false
if (notes.isEmpty()) return false
if (binaryDesc != PMS_ID_BINDESC) return false
if (binaryDescription != PMS_ID_BINDESC) return false
if (title.isEmpty()) return false
if (title != PMS_ID_TITLE) return false
if (username.isEmpty()) return false
@@ -88,9 +85,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
password = parcel.readString() ?: password
url = parcel.readString() ?: url
notes = parcel.readString() ?: notes
binaryDesc = parcel.readString() ?: binaryDesc
binaryData = ByteArray(parcel.readInt())
parcel.readByteArray(binaryData)
binaryDescription = parcel.readString() ?: binaryDescription
binaryData = parcel.readParcelable(BinaryAttachment::class.java.classLoader)
}
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
@@ -108,9 +104,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
dest.writeString(password)
dest.writeString(url)
dest.writeString(notes)
dest.writeString(binaryDesc)
dest.writeInt(binaryData.size)
dest.writeByteArray(binaryData)
dest.writeString(binaryDescription)
dest.writeParcelable(binaryData, flags)
}
fun updateWith(source: EntryKDB) {
@@ -120,11 +115,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
password = source.password
url = source.url
notes = source.notes
binaryDesc = source.binaryDesc
val descLen = source.binaryData.size
binaryData = ByteArray(descLen)
System.arraycopy(source.binaryData, 0, binaryData, 0, descLen)
binaryDescription = source.binaryDescription
binaryData = source.binaryData
}
override var username = ""

View File

@@ -304,6 +304,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
history.add(entry)
}
fun removeEntryFromHistory(position: Int) {
history.removeAt(position)
}
fun removeAllHistory() {
history.clear()
}

View File

@@ -1,3 +1,22 @@
/*
* 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.entry
import android.os.Parcel
@@ -17,4 +36,14 @@ abstract class EntryVersioned
constructor(parcel: Parcel) : super(parcel)
override fun nodeIndexInParentForNaturalOrder(): Int {
if (nodeIndexInParentForNaturalOrder == -1) {
val numberOfGroups = parent?.getChildGroups()?.size
val indexInEntries = parent?.getChildEntries()?.indexOf(this)
if (numberOfGroups != null && indexInEntries != null)
return numberOfGroups + indexInEntries
}
return nodeIndexInParentForNaturalOrder
}
}

View File

@@ -1,3 +1,22 @@
/*
* 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.entry
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface

View File

@@ -90,6 +90,7 @@ class FieldReferencesEngine {
if (result != null) {
val found = result.entry
found?.stopToManageFieldReferences()
val wanted = result.wanted
var data: String? = null
@@ -145,22 +146,15 @@ class FieldReferencesEngine {
searchParametersV4.setupNone()
searchParametersV4.searchString = ref.substring(4)
if (scan == 'T') {
searchParametersV4.searchInTitles = true
} else if (scan == 'U') {
searchParametersV4.searchInUserNames = true
} else if (scan == 'A') {
searchParametersV4.searchInUrls = true
} else if (scan == 'P') {
searchParametersV4.searchInPasswords = true
} else if (scan == 'N') {
searchParametersV4.searchInNotes = true
} else if (scan == 'I') {
searchParametersV4.searchInUUIDs = true
} else if (scan == 'O') {
searchParametersV4.searchInOther = true
} else {
return null
when (scan) {
'T' -> searchParametersV4.searchInTitles = true
'U' -> searchParametersV4.searchInUserNames = true
'A' -> searchParametersV4.searchInUrls = true
'P' -> searchParametersV4.searchInPasswords = true
'N' -> searchParametersV4.searchInNotes = true
'I' -> searchParametersV4.searchInUUIDs = true
'O' -> searchParametersV4.searchInOther = true
else -> return null
}
val list = ArrayList<EntryKDBX>()

View File

@@ -1,8 +1,28 @@
/*
* 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.group
import android.os.Parcel
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import java.util.*
abstract class GroupVersioned
<
@@ -15,9 +35,10 @@ abstract class GroupVersioned
private var titleGroup = ""
@Transient
private val childGroups = ArrayList<Group>()
private val childGroups = LinkedList<Group>()
@Transient
private val childEntries = ArrayList<Entry>()
private val childEntries = LinkedList<Entry>()
private var positionIndexChildren = 0
constructor() : super()
@@ -33,9 +54,8 @@ abstract class GroupVersioned
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>) {
super.updateWith(source)
titleGroup = source.titleGroup
childGroups.clear()
removeChildren()
childGroups.addAll(source.childGroups)
childEntries.clear()
childEntries.addAll(source.childEntries)
}
@@ -43,23 +63,27 @@ abstract class GroupVersioned
get() = titleGroup
set(value) { titleGroup = value }
override fun getChildGroups(): MutableList<Group> {
override fun getChildGroups(): List<Group> {
return childGroups
}
override fun getChildEntries(): MutableList<Entry> {
override fun getChildEntries(): List<Entry> {
return childEntries
}
override fun addChildGroup(group: Group) {
if (childGroups.contains(group))
removeChildGroup(group)
positionIndexChildren++
group.nodeIndexInParentForNaturalOrder = positionIndexChildren
this.childGroups.add(group)
}
override fun addChildEntry(entry: Entry) {
if (childEntries.contains(entry))
removeChildEntry(entry)
positionIndexChildren++
entry.nodeIndexInParentForNaturalOrder = positionIndexChildren
this.childEntries.add(entry)
}
@@ -76,6 +100,13 @@ abstract class GroupVersioned
this.childEntries.clear()
}
override fun nodeIndexInParentForNaturalOrder(): Int {
return if (nodeIndexInParentForNaturalOrder == -1)
childGroups.indexOf(this)
else
nodeIndexInParentForNaturalOrder
}
override fun toString(): String {
return titleGroup
}

View File

@@ -1,3 +1,22 @@
/*
* 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.group
import com.kunzisoft.keepass.database.action.node.NodeHandler
@@ -5,9 +24,9 @@ import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>, Entry> : NodeVersionedInterface<Group> {
fun getChildGroups(): MutableList<Group>
fun getChildGroups(): List<Group>
fun getChildEntries(): MutableList<Entry>
fun getChildEntries(): List<Entry>
fun addChildGroup(group: Group)
@@ -21,6 +40,7 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
fun allowAddEntryIfIsRoot(): Boolean
@Suppress("UNCHECKED_CAST")
fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>,
groupHandler: NodeHandler<Group>) {
doForEachChild(entryHandler, groupHandler)

View File

@@ -1,3 +1,22 @@
/*
* 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.node
import com.kunzisoft.keepass.database.element.Group
@@ -6,17 +25,6 @@ interface Node: NodeVersionedInterface<Group> {
val nodeId: NodeId<*>?
val nodePositionInParent: Int
get() {
parent?.getChildren(true)?.let { children ->
children.forEachIndexed { index, nodeVersioned ->
if (nodeVersioned.nodeId == this.nodeId)
return index
}
}
return -1
}
fun addParentFrom(node: Node) {
parent = node.parent
}

View File

@@ -21,8 +21,7 @@ package com.kunzisoft.keepass.database.element.node
import android.os.Parcel
import android.os.Parcelable
import java.util.UUID
import java.util.*
class NodeIdUUID : NodeId<UUID> {

View File

@@ -1,3 +1,22 @@
/*
* 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.node
import com.kunzisoft.keepass.database.element.DateInstant

View File

@@ -40,6 +40,8 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
val id: IdType
get() = nodeId.id
var nodeIndexInParentForNaturalOrder = -1
protected constructor()
protected constructor(parcel: Parcel) {

View File

@@ -1,3 +1,22 @@
/*
* 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.node
import android.os.Parcelable
@@ -28,5 +47,10 @@ interface NodeVersionedInterface<ParentGroup> : NodeTimeInterface, Parcelable {
fun isContainedIn(container: ParentGroup): Boolean
/**
* Groups are always before in natural order (DB order)
*/
fun nodeIndexInParentForNaturalOrder(): Int
fun touch(modified: Boolean, touchParents: Boolean)
}

View File

@@ -19,11 +19,11 @@
*/
package com.kunzisoft.keepass.database.element.security
import android.content.ContentResolver
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES
import com.kunzisoft.keepass.stream.ReadBytes
import com.kunzisoft.keepass.stream.readFromStream
import com.kunzisoft.keepass.stream.readBytes
import java.io.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
@@ -61,7 +61,9 @@ class BinaryAttachment : Parcelable {
val compressedByte = parcel.readByte().toInt()
isCompressed = if (compressedByte == 2) null else compressedByte != 0
isProtected = parcel.readByte().toInt() != 0
dataFile = File(parcel.readString())
parcel.readString()?.let {
dataFile = File(it)
}
}
@Throws(IOException::class)
@@ -73,23 +75,26 @@ class BinaryAttachment : Parcelable {
}
@Throws(IOException::class)
fun compress() {
if (dataFile != null) {
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file
if (isCompressed != true) {
val fileBinaryCompress = File(dataFile!!.parent, dataFile!!.name + "_temp")
val outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress))
readFromStream(getInputDataStream(), BUFFER_SIZE_BYTES,
object : ReadBytes {
override fun read(buffer: ByteArray) {
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: GZIPOutputStream? = null
var inputStream: InputStream? = null
try {
outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress))
inputStream = getInputDataStream()
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
})
outputStream.close()
} finally {
inputStream?.close()
outputStream?.close()
// Remove unGzip file
if (dataFile!!.delete()) {
if (fileBinaryCompress.renameTo(dataFile)) {
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
}
@@ -97,24 +102,28 @@ class BinaryAttachment : Parcelable {
}
}
}
}
@Throws(IOException::class)
fun decompress() {
if (dataFile != null) {
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile ->
if (isCompressed != false) {
val fileBinaryDecompress = File(dataFile!!.parent, dataFile!!.name + "_temp")
val outputStream = FileOutputStream(fileBinaryDecompress)
readFromStream(GZIPInputStream(getInputDataStream()), BUFFER_SIZE_BYTES,
object : ReadBytes {
override fun read(buffer: ByteArray) {
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: FileOutputStream? = null
var inputStream: GZIPInputStream? = null
try {
outputStream = FileOutputStream(fileBinaryDecompress)
inputStream = GZIPInputStream(getInputDataStream())
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
})
outputStream.close()
} finally {
inputStream?.close()
outputStream?.close()
// Remove gzip file
if (dataFile!!.delete()) {
if (fileBinaryDecompress.renameTo(dataFile)) {
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
}
@@ -122,6 +131,33 @@ class BinaryAttachment : Parcelable {
}
}
}
}
fun download(createdFileUri: Uri,
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) {
var dataDownloaded = 0
contentResolver.openOutputStream(createdFileUri).use { outputStream ->
outputStream?.let { fileOutputStream ->
if (isCompressed == true) {
GZIPInputStream(getInputDataStream())
} else {
getInputDataStream()
}.use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
fileOutputStream.write(buffer)
dataDownloaded += buffer.size
try {
val percentDownload = (100 * dataDownloaded / length()).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {}
}
}
}
}
}
@Throws(IOException::class)
fun clear() {

View File

@@ -1,3 +1,22 @@
/*
* 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.exception
import android.content.res.Resources

View File

@@ -1,3 +1,22 @@
/*
* 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.exception
import java.io.IOException

View File

@@ -21,9 +21,11 @@
package com.kunzisoft.keepass.database.file
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.stream.bytes4ToInt
import com.kunzisoft.keepass.stream.readBytesLength
import com.kunzisoft.keepass.stream.readBytes4ToInt
import java.io.IOException
import java.io.InputStream
class DatabaseHeaderKDB : DatabaseHeader() {
@@ -47,30 +49,25 @@ class DatabaseHeaderKDB : DatabaseHeader() {
*/
var contentsHash = ByteArray(32)
// As UInt
var numKeyEncRounds: Int = 0
/**
* Parse given buf, as read from file.
* @param buf
* @throws IOException
*/
@Throws(IOException::class)
fun loadFromFile(buf: ByteArray, offset: Int) {
signature1 = LEDataInputStream.readInt(buf, offset)
signature2 = LEDataInputStream.readInt(buf, offset + 4)
flags = LEDataInputStream.readInt(buf, offset + 8)
version = LEDataInputStream.readInt(buf, offset + 12)
System.arraycopy(buf, offset + 16, masterSeed, 0, 16)
System.arraycopy(buf, offset + 32, encryptionIV, 0, 16)
numGroups = LEDataInputStream.readInt(buf, offset + 48)
numEntries = LEDataInputStream.readInt(buf, offset + 52)
System.arraycopy(buf, offset + 56, contentsHash, 0, 32)
System.arraycopy(buf, offset + 88, transformSeed, 0, 32)
numKeyEncRounds = LEDataInputStream.readInt(buf, offset + 120)
fun loadFromFile(inputStream: InputStream) {
signature1 = inputStream.readBytes4ToInt() // 4 bytes
signature2 = inputStream.readBytes4ToInt() // 4 bytes
flags = inputStream.readBytes4ToInt() // 4 bytes
version = inputStream.readBytes4ToInt() // 4 bytes
masterSeed = inputStream.readBytesLength(16) // 16 bytes
encryptionIV = inputStream.readBytesLength(16) // 16 bytes
numGroups = inputStream.readBytes4ToInt() // 4 bytes
numEntries = inputStream.readBytes4ToInt() // 4 bytes
contentsHash = inputStream.readBytesLength(32) // 32 bytes
transformSeed = inputStream.readBytesLength(32) // 32 bytes
numKeyEncRounds = inputStream.readBytes4ToInt()
if (numKeyEncRounds < 0) {
// TODO: Really treat this like an unsigned integer
throw IOException("Does not support more than " + Integer.MAX_VALUE + " rounds.")

View File

@@ -30,10 +30,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
import com.kunzisoft.keepass.stream.CopyInputStream
import com.kunzisoft.keepass.stream.HmacBlockStream
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import com.kunzisoft.keepass.stream.*
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
@@ -148,7 +145,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
val headerBOS = ByteArrayOutputStream()
val copyInputStream = CopyInputStream(inputStream, headerBOS)
val digestInputStream = DigestInputStream(copyInputStream, messageDigest)
val littleEndianDataInputStream = LEDataInputStream(digestInputStream)
val littleEndianDataInputStream = LittleEndianDataInputStream(digestInputStream)
val sig1 = littleEndianDataInputStream.readInt()
val sig2 = littleEndianDataInputStream.readInt()
@@ -172,7 +169,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
}
@Throws(IOException::class)
private fun readHeaderField(dis: LEDataInputStream): Boolean {
private fun readHeaderField(dis: LittleEndianDataInputStream): Boolean {
val fieldID = dis.read().toByte()
val fieldSize: Int = if (version < FILE_VERSION_32_4) {
@@ -243,12 +240,12 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
throw IOException("Invalid cipher ID.")
}
databaseV4.dataCipher = DatabaseInputOutputUtils.bytesToUuid(pbId)
databaseV4.dataCipher = bytes16ToUuid(pbId)
}
private fun setTransformRound(roundsByte: ByteArray?) {
private fun setTransformRound(roundsByte: ByteArray) {
assignAesKdfEngineIfNotExists()
val rounds = LEDataInputStream.readLong(roundsByte!!, 0)
val rounds = bytes64ToLong(roundsByte)
databaseV4.kdfParameters?.setUInt64(AesKdf.PARAM_ROUNDS, rounds)
databaseV4.numberKeyEncryptionRounds = rounds
}
@@ -259,7 +256,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
throw IOException("Invalid compression flags.")
}
val flag = LEDataInputStream.readInt(pbFlags, 0)
val flag = bytes4ToInt(pbFlags)
if (flag < 0 || flag >= CompressionAlgorithm.values().size) {
throw IOException("Unrecognized compression flag.")
}
@@ -275,7 +272,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
throw IOException("Invalid stream id.")
}
val id = LEDataInputStream.readInt(streamID, 0)
val id = bytes4ToInt(streamID)
if (id < 0 || id >= CrsAlgorithm.values().size) {
throw IOException("Invalid stream id.")
}
@@ -295,6 +292,9 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
}
companion object {
var ULONG_MAX_VALUE: Long = -1
const val DBSIG_PRE2 = -0x4ab4049a
const val DBSIG_2 = -0x4ab40499
@@ -323,7 +323,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
@Throws(IOException::class)
fun computeHeaderHmac(header: ByteArray, key: ByteArray): ByteArray {
val blockKey = HmacBlockStream.GetHmacKey64(key, DatabaseInputOutputUtils.ULONG_MAX_VALUE)
val blockKey = HmacBlockStream.getHmacKey64(key, ULONG_MAX_VALUE)
val hmac: Mac
try {

View File

@@ -127,11 +127,7 @@ object DatabaseKDBXXML {
const val ElemCustomData = "CustomData"
const val ElemStringDictExItem = "Item"
val dateFormatter: ThreadLocal<SimpleDateFormat> = object : ThreadLocal<SimpleDateFormat>() {
override fun initialValue(): SimpleDateFormat {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
return dateFormat
}
val DateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}

View File

@@ -22,9 +22,11 @@ package com.kunzisoft.keepass.database.file.input
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.File
import java.io.InputStream
abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>> {
abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
(protected val cacheDirectory: File) {
/**
* Load a versioned database file, return contents in a new DatabaseVersioned.

View File

@@ -16,36 +16,10 @@
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*
Derived from
KeePass for J2ME
Copyright 2007 Naomaru Itoi <nao@phoneid.org>
This file was derived from
Java clone of KeePass - A KeePass file viewer for Java
Copyright 2006 Bill Zwicky <billzwicky@users.sourceforge.net>
This program 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; version 2
This program 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 this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.kunzisoft.keepass.database.file.input
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
@@ -53,28 +27,29 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.stream.NullOutputStream
import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import javax.crypto.*
import org.joda.time.Instant
import java.io.*
import java.security.*
import java.util.*
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.security.*
import java.util.Arrays
/**
* Load a KDB database file.
*/
class DatabaseInputKDB : DatabaseInput<DatabaseKDB>() {
class DatabaseInputKDB(cacheDirectory: File,
private val fixDuplicateUUID: Boolean = false)
: DatabaseInput<DatabaseKDB>(cacheDirectory) {
private lateinit var mDatabaseToOpen: DatabaseKDB
@@ -87,52 +62,63 @@ class DatabaseInputKDB : DatabaseInput<DatabaseKDB>() {
try {
// Load entire file, most of it's encrypted.
val fileSize = databaseInputStream.available()
val filebuf = ByteArray(fileSize + 16) // Pad with a blocksize (Twofish uses 128 bits), since Android 4.3 tries to write more to the buffer
databaseInputStream.read(filebuf, 0, fileSize) // TODO remove
databaseInputStream.close()
// Parse header (unencrypted)
if (fileSize < DatabaseHeaderKDB.BUF_SIZE)
throw IOException("File too short for header")
val hdr = DatabaseHeaderKDB()
hdr.loadFromFile(filebuf, 0)
val header = DatabaseHeaderKDB()
header.loadFromFile(databaseInputStream)
if (hdr.signature1 != DatabaseHeader.PWM_DBSIG_1 || hdr.signature2 != DatabaseHeaderKDB.DBSIG_2) {
val contentSize = databaseInputStream.available()
if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE))
throw IOException("Header corrupted")
if (header.signature1 != DatabaseHeader.PWM_DBSIG_1
|| header.signature2 != DatabaseHeaderKDB.DBSIG_2) {
throw SignatureDatabaseException()
}
if (!hdr.matchesVersion()) {
if (!header.matchesVersion()) {
throw VersionDatabaseException()
}
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
mDatabaseToOpen = DatabaseKDB()
mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
// Select algorithm
when {
hdr.flags and DatabaseHeaderKDB.FLAG_RIJNDAEL != 0 -> mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
hdr.flags and DatabaseHeaderKDB.FLAG_TWOFISH != 0 -> mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish
header.flags and DatabaseHeaderKDB.FLAG_RIJNDAEL != 0 -> {
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
}
header.flags and DatabaseHeaderKDB.FLAG_TWOFISH != 0 -> {
mDatabaseToOpen.encryptionAlgorithm = EncryptionAlgorithm.Twofish
}
else -> throw InvalidAlgorithmDatabaseException()
}
mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong()
mDatabaseToOpen.numberKeyEncryptionRounds = header.numKeyEncRounds.toLong()
// Generate transformedMasterKey from masterKey
mDatabaseToOpen.makeFinalKey(hdr.masterSeed, hdr.transformSeed, mDatabaseToOpen.numberKeyEncryptionRounds)
mDatabaseToOpen.makeFinalKey(
header.masterSeed,
header.transformSeed,
mDatabaseToOpen.numberKeyEncryptionRounds)
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
// Initialize Rijndael algorithm
val cipher: Cipher
try {
if (mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael) {
cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
} else if (mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.Twofish) {
cipher = CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
} else {
throw IOException("Encryption algorithm is not supported")
val cipher: Cipher = try {
when {
mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.AESRijndael -> {
CipherFactory.getInstance("AES/CBC/PKCS5Padding")
}
mDatabaseToOpen.encryptionAlgorithm === EncryptionAlgorithm.Twofish -> {
CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
}
else -> throw IOException("Encryption algorithm is not supported")
}
} catch (e1: NoSuchAlgorithmException) {
throw IOException("No such algorithm")
} catch (e1: NoSuchPaddingException) {
@@ -140,98 +126,231 @@ class DatabaseInputKDB : DatabaseInput<DatabaseKDB>() {
}
try {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(mDatabaseToOpen.finalKey, "AES"), IvParameterSpec(hdr.encryptionIV))
cipher.init(Cipher.DECRYPT_MODE,
SecretKeySpec(mDatabaseToOpen.finalKey, "AES"),
IvParameterSpec(header.encryptionIV))
} catch (e1: InvalidKeyException) {
throw IOException("Invalid key")
} catch (e1: InvalidAlgorithmParameterException) {
throw IOException("Invalid algorithm parameter.")
}
// Decrypt! The first bytes aren't encrypted (that's the header)
val encryptedPartSize: Int
val messageDigest: MessageDigest
try {
encryptedPartSize = cipher.doFinal(filebuf, DatabaseHeaderKDB.BUF_SIZE, fileSize - DatabaseHeaderKDB.BUF_SIZE, filebuf, DatabaseHeaderKDB.BUF_SIZE)
} catch (e1: ShortBufferException) {
throw IOException("Buffer too short")
} catch (e1: IllegalBlockSizeException) {
throw IOException("Invalid block size")
} catch (e1: BadPaddingException) {
throw InvalidCredentialsDatabaseException()
}
val md: MessageDigest
try {
md = MessageDigest.getInstance("SHA-256")
messageDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
throw IOException("No SHA-256 algorithm")
}
val nos = NullOutputStream()
val dos = DigestOutputStream(nos, md)
dos.write(filebuf, DatabaseHeaderKDB.BUF_SIZE, encryptedPartSize)
dos.close()
val hash = md.digest()
// Decrypt content
val cipherInputStream = BufferedInputStream(
DigestInputStream(
BetterCipherInputStream(databaseInputStream, cipher),
messageDigest
)
)
if (!Arrays.equals(hash, hdr.contentsHash)) {
/* TODO checksum
// Add a mark to the content start
if (!cipherInputStream.markSupported()) {
throw IOException("Input stream does not support mark.")
}
cipherInputStream.mark(cipherInputStream.available() +1)
// Consume all data to get the digest
var numberRead = 0
while (numberRead > -1) {
numberRead = cipherInputStream.read(ByteArray(1024))
}
Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)")
// Check sum
if (!Arrays.equals(messageDigest.digest(), header.contentsHash)) {
throw InvalidCredentialsDatabaseException()
}
// Back to the content start
cipherInputStream.reset()
*/
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
val newRoot = mDatabaseToOpen.createGroup()
newRoot.level = -1
mDatabaseToOpen.rootGroup = newRoot
// Import all groups
var pos = DatabaseHeaderKDB.BUF_SIZE
var newGrp = mDatabaseToOpen.createGroup()
run {
var i = 0
while (i < hdr.numGroups) {
val fieldType = LEDataInputStream.readUShort(filebuf, pos)
pos += 2
val fieldSize = LEDataInputStream.readInt(filebuf, pos)
pos += 4
// Import all nodes
var newGroup: GroupKDB? = null
var newEntry: EntryKDB? = null
var currentGroupNumber = 0
var currentEntryNumber = 0
while (currentGroupNumber < header.numGroups
|| currentEntryNumber < header.numEntries) {
if (fieldType == 0xFFFF) {
// End-Group record. Save group and count it.
mDatabaseToOpen.addGroupIndex(newGrp)
newGrp = mDatabaseToOpen.createGroup()
i++
} else {
readGroupField(mDatabaseToOpen, newGrp, fieldType, filebuf, pos)
}
pos += fieldSize
}
}
val fieldType = cipherInputStream.readBytes2ToUShort()
val fieldSize = cipherInputStream.readBytes4ToUInt().toInt()
// Import all entries
var newEnt = mDatabaseToOpen.createEntry()
var i = 0
while (i < hdr.numEntries) {
val fieldType = LEDataInputStream.readUShort(filebuf, pos)
val fieldSize = LEDataInputStream.readInt(filebuf, pos + 2)
if (fieldType == 0xFFFF) {
// End-Group record. Save group and count it.
mDatabaseToOpen.addEntryIndex(newEnt)
newEnt = mDatabaseToOpen.createEntry()
i++
} else {
readEntryField(mDatabaseToOpen, newEnt, filebuf, pos)
when (fieldType) {
0x0000 -> {
cipherInputStream.readBytesLength(fieldSize)
}
pos += 2 + 4 + fieldSize
0x0001 -> {
// Create new node depending on byte number
when (fieldSize) {
4 -> {
newGroup = mDatabaseToOpen.createGroup().apply {
setGroupId(cipherInputStream.readBytes4ToInt())
}
}
16 -> {
newEntry = mDatabaseToOpen.createEntry().apply {
nodeId = NodeIdUUID(cipherInputStream.readBytes16ToUuid())
}
}
else -> {
throw UnsupportedEncodingException("Field type $fieldType")
}
}
}
0x0002 -> {
newGroup?.let { group ->
group.title = cipherInputStream.readBytesToString(fieldSize)
} ?:
newEntry?.let { entry ->
val groupKDB = mDatabaseToOpen.createGroup()
groupKDB.nodeId = NodeIdInt(cipherInputStream.readBytes4ToInt())
entry.parent = groupKDB
}
}
0x0003 -> {
newGroup?.let { group ->
group.creationTime = cipherInputStream.readBytes5ToDate()
} ?:
newEntry?.let { entry ->
var iconId = cipherInputStream.readBytes4ToInt()
// Clean up after bug that set icon ids to -1
if (iconId == -1) {
iconId = 0
}
entry.icon = mDatabaseToOpen.iconFactory.getIcon(iconId)
}
}
0x0004 -> {
newGroup?.let { group ->
group.lastModificationTime = cipherInputStream.readBytes5ToDate()
} ?:
newEntry?.let { entry ->
entry.title = cipherInputStream.readBytesToString(fieldSize)
}
}
0x0005 -> {
newGroup?.let { group ->
group.lastAccessTime = cipherInputStream.readBytes5ToDate()
} ?:
newEntry?.let { entry ->
entry.url = cipherInputStream.readBytesToString(fieldSize)
}
}
0x0006 -> {
newGroup?.let { group ->
group.expiryTime = cipherInputStream.readBytes5ToDate()
} ?:
newEntry?.let { entry ->
entry.username = cipherInputStream.readBytesToString(fieldSize)
}
}
0x0007 -> {
newGroup?.let { group ->
group.icon = mDatabaseToOpen.iconFactory.getIcon(cipherInputStream.readBytes4ToInt())
} ?:
newEntry?.let { entry ->
entry.password = cipherInputStream.readBytesToString(fieldSize,false)
}
}
0x0008 -> {
newGroup?.let { group ->
group.level = cipherInputStream.readBytes2ToUShort()
} ?:
newEntry?.let { entry ->
entry.notes = cipherInputStream.readBytesToString(fieldSize)
}
}
0x0009 -> {
newGroup?.let { group ->
group.flags = cipherInputStream.readBytes4ToInt()
} ?:
newEntry?.let { entry ->
entry.creationTime = cipherInputStream.readBytes5ToDate()
}
}
0x000A -> {
newEntry?.let { entry ->
entry.lastModificationTime = cipherInputStream.readBytes5ToDate()
}
}
0x000B -> {
newEntry?.let { entry ->
entry.lastAccessTime = cipherInputStream.readBytes5ToDate()
}
}
0x000C -> {
newEntry?.let { entry ->
entry.expiryTime = cipherInputStream.readBytes5ToDate()
}
}
0x000D -> {
newEntry?.let { entry ->
entry.binaryDescription = cipherInputStream.readBytesToString(fieldSize)
}
}
0x000E -> {
newEntry?.let { entry ->
if (fieldSize > 0) {
// Generate an unique new file with timestamp
val binaryFile = File(cacheDirectory,
Instant.now().millis.toString())
entry.binaryData = BinaryAttachment(binaryFile)
BufferedOutputStream(FileOutputStream(binaryFile)).use { outputStream ->
cipherInputStream.readBytes(fieldSize,
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
outputStream.write(buffer)
}
}
}
}
}
0xFFFF -> {
// End record. Save node and count it.
newGroup?.let { group ->
mDatabaseToOpen.addGroupIndex(group)
currentGroupNumber++
newGroup = null
}
newEntry?.let { entry ->
mDatabaseToOpen.addEntryIndex(entry)
currentEntryNumber++
newEntry = null
}
cipherInputStream.readBytesLength(fieldSize)
}
else -> {
throw UnsupportedEncodingException("Field type $fieldType")
}
}
}
// Check sum
if (!Arrays.equals(messageDigest.digest(), header.contentsHash)) {
throw InvalidCredentialsDatabaseException()
}
constructTreeFromIndex()
} catch (e: LoadDatabaseException) {
mDatabaseToOpen.clearCache()
throw e
} catch (e: IOException) {
mDatabaseToOpen.clearCache()
throw IODatabaseException(e)
} catch (e: OutOfMemoryError) {
mDatabaseToOpen.clearCache()
throw NoMemoryDatabaseException(e)
} catch (e: Exception) {
mDatabaseToOpen.clearCache()
throw LoadDatabaseException(e)
}
@@ -279,71 +398,6 @@ class DatabaseInputKDB : DatabaseInput<DatabaseKDB>() {
}
}
/**
* Parse and save one record from binary file.
* @param buf
* @param offset
* @return If >0,
* @throws UnsupportedEncodingException
*/
@Throws(UnsupportedEncodingException::class)
private fun readGroupField(db: DatabaseKDB, grp: GroupKDB, fieldType: Int, buf: ByteArray, offset: Int) {
when (fieldType) {
0x0000 -> {
}
0x0001 -> grp.setGroupId(LEDataInputStream.readInt(buf, offset))
0x0002 -> grp.title = DatabaseInputOutputUtils.readCString(buf, offset)
0x0003 -> grp.creationTime = DatabaseInputOutputUtils.readCDate(buf, offset)
0x0004 -> grp.lastModificationTime = DatabaseInputOutputUtils.readCDate(buf, offset)
0x0005 -> grp.lastAccessTime = DatabaseInputOutputUtils.readCDate(buf, offset)
0x0006 -> grp.expiryTime = DatabaseInputOutputUtils.readCDate(buf, offset)
0x0007 -> grp.icon = db.iconFactory.getIcon(LEDataInputStream.readInt(buf, offset))
0x0008 -> grp.level = LEDataInputStream.readUShort(buf, offset)
0x0009 -> grp.flags = LEDataInputStream.readInt(buf, offset)
}// Ignore field
}
@Throws(UnsupportedEncodingException::class)
private fun readEntryField(db: DatabaseKDB, ent: EntryKDB, buf: ByteArray, offset: Int) {
var offsetMutable = offset
val fieldType = LEDataInputStream.readUShort(buf, offsetMutable)
offsetMutable += 2
val fieldSize = LEDataInputStream.readInt(buf, offsetMutable)
offsetMutable += 4
when (fieldType) {
0x0000 -> {
}
0x0001 -> ent.nodeId = NodeIdUUID(LEDataInputStream.readUuid(buf, offsetMutable))
0x0002 -> {
val groupKDB = mDatabaseToOpen.createGroup()
groupKDB.nodeId = NodeIdInt(LEDataInputStream.readInt(buf, offsetMutable))
ent.parent = groupKDB
}
0x0003 -> {
var iconId = LEDataInputStream.readInt(buf, offsetMutable)
// Clean up after bug that set icon ids to -1
if (iconId == -1) {
iconId = 0
}
ent.icon = db.iconFactory.getIcon(iconId)
}
0x0004 -> ent.title = DatabaseInputOutputUtils.readCString(buf, offsetMutable)
0x0005 -> ent.url = DatabaseInputOutputUtils.readCString(buf, offsetMutable)
0x0006 -> ent.username = DatabaseInputOutputUtils.readCString(buf, offsetMutable)
0x0007 -> ent.password = DatabaseInputOutputUtils.readPassword(buf, offsetMutable)
0x0008 -> ent.notes = DatabaseInputOutputUtils.readCString(buf, offsetMutable)
0x0009 -> ent.creationTime = DatabaseInputOutputUtils.readCDate(buf, offsetMutable)
0x000A -> ent.lastModificationTime = DatabaseInputOutputUtils.readCDate(buf, offsetMutable)
0x000B -> ent.lastAccessTime = DatabaseInputOutputUtils.readCDate(buf, offsetMutable)
0x000C -> ent.expiryTime = DatabaseInputOutputUtils.readCDate(buf, offsetMutable)
0x000D -> ent.binaryDesc = DatabaseInputOutputUtils.readCString(buf, offsetMutable)
0x000E -> ent.binaryData = DatabaseInputOutputUtils.readBytes(buf, offsetMutable, fieldSize)
}// Ignore field
}
companion object {
private val TAG = DatabaseInputKDB::class.java.name
}

View File

@@ -24,7 +24,8 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
@@ -37,12 +38,11 @@ import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DateKDBXUtil
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
import com.kunzisoft.keepass.database.file.DateKDBXUtil
import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import org.spongycastle.crypto.StreamCipher
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
@@ -54,10 +54,12 @@ import java.util.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import kotlin.math.min
class DatabaseInputKDBX(private val streamDir: File,
private val fixDuplicateUUID: Boolean = false) : DatabaseInput<DatabaseKDBX>() {
class DatabaseInputKDBX(cacheDirectory: File,
private val fixDuplicateUUID: Boolean = false)
: DatabaseInput<DatabaseKDBX>(cacheDirectory) {
private var randomStream: StreamCipher? = null
private lateinit var mDatabase: DatabaseKDBX
@@ -131,11 +133,11 @@ class DatabaseInputKDBX(private val streamDir: File,
if (mDatabase.kdbxVersion < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
val decrypted = attachCipherStream(databaseInputStream, cipher)
val dataDecrypted = LEDataInputStream(decrypted)
val dataDecrypted = LittleEndianDataInputStream(decrypted)
val storedStartBytes: ByteArray?
try {
storedStartBytes = dataDecrypted.readBytes(32)
if (storedStartBytes == null || storedStartBytes.size != 32) {
if (storedStartBytes.size != 32) {
throw InvalidCredentialsDatabaseException()
}
} catch (e: IOException) {
@@ -148,7 +150,7 @@ class DatabaseInputKDBX(private val streamDir: File,
isPlain = HashedBlockInputStream(dataDecrypted)
} else { // KDBX 4
val isData = LEDataInputStream(databaseInputStream)
val isData = LittleEndianDataInputStream(databaseInputStream)
val storedHash = isData.readBytes(32)
if (!Arrays.equals(storedHash, hashOfHeader)) {
throw InvalidCredentialsDatabaseException()
@@ -157,7 +159,7 @@ class DatabaseInputKDBX(private val streamDir: File,
val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
val headerHmac = DatabaseHeaderKDBX.computeHeaderHmac(pbHeader, hmacKey)
val storedHmac = isData.readBytes(32)
if (storedHmac == null || storedHmac.size != 32) {
if (storedHmac.size != 32) {
throw InvalidCredentialsDatabaseException()
}
// Mac doesn't match
@@ -207,12 +209,12 @@ class DatabaseInputKDBX(private val streamDir: File,
}
private fun attachCipherStream(inputStream: InputStream, cipher: Cipher): InputStream {
return BetterCipherInputStream(inputStream, cipher, 50 * 1024)
return CipherInputStream(inputStream, cipher)
}
@Throws(IOException::class)
private fun loadInnerHeader(inputStream: InputStream, header: DatabaseHeaderKDBX) {
val lis = LEDataInputStream(inputStream)
val lis = LittleEndianDataInputStream(inputStream)
while (true) {
if (!readInnerHeader(lis, header)) break
@@ -220,7 +222,8 @@ class DatabaseInputKDBX(private val streamDir: File,
}
@Throws(IOException::class)
private fun readInnerHeader(dataInputStream: LEDataInputStream, header: DatabaseHeaderKDBX): Boolean {
private fun readInnerHeader(dataInputStream: LittleEndianDataInputStream,
header: DatabaseHeaderKDBX): Boolean {
val fieldId = dataInputStream.read().toByte()
val size = dataInputStream.readInt()
@@ -243,13 +246,11 @@ class DatabaseInputKDBX(private val streamDir: File,
val protectedFlag = flag && DatabaseHeaderKDBX.KdbxBinaryFlags.Protected.toInt() != DatabaseHeaderKDBX.KdbxBinaryFlags.None.toInt()
val byteLength = size - 1
// Read in a file
val file = File(streamDir, unusedCacheFileName)
val file = File(cacheDirectory, unusedCacheFileName)
FileOutputStream(file).use { outputStream ->
dataInputStream.readBytes(byteLength, object : ReadBytes {
override fun read(buffer: ByteArray) {
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
outputStream.write(buffer)
}
})
}
val protectedBinary = BinaryAttachment(file, protectedFlag)
mDatabase.binaryPool.add(protectedBinary)
@@ -659,7 +660,7 @@ class DatabaseInputKDBX(private val streamDir: File,
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
ctxDeletedObject?.uuid = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
ctxDeletedObject?.deletionTime = readTime(xpp)
ctxDeletedObject?.setDeletionTime(readTime(xpp))
} else {
readUnknown(xpp)
}
@@ -822,13 +823,13 @@ class DatabaseInputKDBX(private val streamDir: File,
buf = buf8
}
val seconds = LEDataInputStream.readLong(buf, 0)
val seconds = bytes64ToLong(buf)
utcDate = DateKDBXUtil.convertKDBX4Time(seconds)
} else {
try {
utcDate = DatabaseKDBXXML.dateFormatter.get()?.parse(sDate)
utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate)
} catch (e: ParseException) {
// Catch with null test below
}
@@ -882,7 +883,7 @@ class DatabaseInputKDBX(private val streamDir: File,
}
val buf = Base64.decode(encoded, BASE_64_FLAG)
return DatabaseInputOutputUtils.bytesToUuid(buf)
return bytes16ToUuid(buf)
}
@Throws(IOException::class, XmlPullParserException::class)
@@ -960,7 +961,7 @@ class DatabaseInputKDBX(private val streamDir: File,
// New binary to retrieve
else {
var compressed: Boolean? = null
var compressed = false
var protected = false
if (xpp.attributeCount > 0) {
@@ -980,16 +981,16 @@ class DatabaseInputKDBX(private val streamDir: File,
return BinaryAttachment()
val data = Base64.decode(base64, BASE_64_FLAG)
val file = File(streamDir, unusedCacheFileName)
val file = File(cacheDirectory, unusedCacheFileName)
return FileOutputStream(file).use { outputStream ->
// Force compression in this specific case
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
&& compressed == false) {
&& !compressed) {
GZIPOutputStream(outputStream).write(data)
BinaryAttachment(file, protected, true)
} else {
outputStream.write(data)
BinaryAttachment(file, protected)
BinaryAttachment(file, protected, compressed)
}
}
}

View File

@@ -20,34 +20,34 @@
package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.stream.intTo4Bytes
import java.io.IOException
import java.io.OutputStream
class DatabaseHeaderOutputKDB(private val mHeader: DatabaseHeaderKDB, private val mOS: OutputStream) {
class DatabaseHeaderOutputKDB(private val mHeader: DatabaseHeaderKDB,
private val mOutputStream: OutputStream) {
@Throws(IOException::class)
fun outputStart() {
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.signature1))
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.signature2))
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.flags))
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.version))
mOS.write(mHeader.masterSeed)
mOS.write(mHeader.encryptionIV)
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.numGroups))
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.numEntries))
mOutputStream.write(intTo4Bytes(mHeader.signature1))
mOutputStream.write(intTo4Bytes(mHeader.signature2))
mOutputStream.write(intTo4Bytes(mHeader.flags))
mOutputStream.write(intTo4Bytes(mHeader.version))
mOutputStream.write(mHeader.masterSeed)
mOutputStream.write(mHeader.encryptionIV)
mOutputStream.write(intTo4Bytes(mHeader.numGroups))
mOutputStream.write(intTo4Bytes(mHeader.numEntries))
}
@Throws(IOException::class)
fun outputContentHash() {
mOS.write(mHeader.contentsHash)
mOutputStream.write(mHeader.contentsHash)
}
@Throws(IOException::class)
fun outputEnd() {
mOS.write(mHeader.transformSeed)
mOS.write(LEDataOutputStream.writeIntBuf(mHeader.numKeyEncRounds))
mOutputStream.write(mHeader.transformSeed)
mOutputStream.write(intTo4Bytes(mHeader.numKeyEncRounds))
}
@Throws(IOException::class)
@@ -59,6 +59,6 @@ class DatabaseHeaderOutputKDB(private val mHeader: DatabaseHeaderKDB, private va
@Throws(IOException::class)
fun close() {
mOS.close()
mOutputStream.close()
}
}

View File

@@ -19,17 +19,14 @@
*/
package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.utils.VariantDictionary
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.stream.HmacBlockStream
import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.stream.MacOutputStream
import com.kunzisoft.keepass.utils.DatabaseInputOutputUtils
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.ULONG_MAX_VALUE
import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.utils.VariantDictionary
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.OutputStream
@@ -37,13 +34,15 @@ import java.security.DigestOutputStream
import java.security.InvalidKeyException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class DatabaseHeaderOutputKDBX @Throws(DatabaseOutputException::class)
constructor(private val db: DatabaseKDBX, private val header: DatabaseHeaderKDBX, os: OutputStream) : DatabaseHeaderOutput() {
private val los: LEDataOutputStream
constructor(private val databaseKDBX: DatabaseKDBX,
private val header: DatabaseHeaderKDBX,
outputStream: OutputStream) : DatabaseHeaderOutput() {
private val los: LittleEndianDataOutputStream
private val mos: MacOutputStream
private val dos: DigestOutputStream
lateinit var headerHmac: ByteArray
@@ -58,15 +57,16 @@ constructor(private val db: DatabaseKDBX, private val header: DatabaseHeaderKDBX
}
try {
db.makeFinalKey(header.masterSeed)
databaseKDBX.makeFinalKey(header.masterSeed)
} catch (e: IOException) {
throw DatabaseOutputException(e)
}
val hmacKey = databaseKDBX.hmacKey ?: throw DatabaseOutputException("HmacKey is not defined")
val hmac: Mac
try {
hmac = Mac.getInstance("HmacSHA256")
val signingKey = SecretKeySpec(HmacBlockStream.GetHmacKey64(db.hmacKey, DatabaseInputOutputUtils.ULONG_MAX_VALUE), "HmacSHA256")
val signingKey = SecretKeySpec(HmacBlockStream.getHmacKey64(hmacKey, ULONG_MAX_VALUE), "HmacSHA256")
hmac.init(signingKey)
} catch (e: NoSuchAlgorithmException) {
throw DatabaseOutputException(e)
@@ -74,9 +74,9 @@ constructor(private val db: DatabaseKDBX, private val header: DatabaseHeaderKDBX
throw DatabaseOutputException(e)
}
dos = DigestOutputStream(os, md)
dos = DigestOutputStream(outputStream, md)
mos = MacOutputStream(dos, hmac)
los = LEDataOutputStream(mos)
los = LittleEndianDataOutputStream(mos)
}
@Throws(IOException::class)
@@ -86,15 +86,15 @@ constructor(private val db: DatabaseKDBX, private val header: DatabaseHeaderKDBX
los.writeUInt(DatabaseHeaderKDBX.DBSIG_2.toLong())
los.writeUInt(header.version)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, DatabaseInputOutputUtils.uuidToBytes(db.dataCipher))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, LEDataOutputStream.writeIntBuf(DatabaseHeaderKDBX.getFlagFromCompression(db.compressionAlgorithm)))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.dataCipher))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, intTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
if (header.version < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformSeed, header.transformSeed)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, LEDataOutputStream.writeLongBuf(db.numberKeyEncryptionRounds))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.TransformRounds, longTo8Bytes(databaseKDBX.numberKeyEncryptionRounds))
} else {
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.KdfParameters, KdfParameters.serialize(db.kdfParameters!!))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.KdfParameters, KdfParameters.serialize(databaseKDBX.kdfParameters!!))
}
if (header.encryptionIV.isNotEmpty()) {
@@ -104,13 +104,13 @@ constructor(private val db: DatabaseKDBX, private val header: DatabaseHeaderKDBX
if (header.version < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomstreamKey, header.innerRandomStreamKey)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.StreamStartBytes, header.streamStartBytes)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, LEDataOutputStream.writeIntBuf(header.innerRandomStream!!.id))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, intTo4Bytes(header.innerRandomStream!!.id))
}
if (db.containsPublicCustomData()) {
if (databaseKDBX.containsPublicCustomData()) {
val bos = ByteArrayOutputStream()
val los = LEDataOutputStream(bos)
VariantDictionary.serialize(db.publicCustomData, los)
val los = LittleEndianDataOutputStream(bos)
VariantDictionary.serialize(databaseKDBX.publicCustomData, los)
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.PublicCustomData, bos.toByteArray())
}

View File

@@ -22,9 +22,8 @@ package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.stream.ReadBytes
import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.stream.readFromStream
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
import com.kunzisoft.keepass.stream.readBytes
import java.io.IOException
import java.io.OutputStream
import kotlin.experimental.or
@@ -33,7 +32,7 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
private val header: DatabaseHeaderKDBX,
outputStream: OutputStream) {
private val dataOutputStream: LEDataOutputStream = LEDataOutputStream(outputStream)
private val dataOutputStream: LittleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream)
@Throws(IOException::class)
fun output() {
@@ -58,14 +57,10 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify
dataOutputStream.write(flag.toInt())
readFromStream(protectedBinary.getInputDataStream(), BUFFER_SIZE_BYTES,
object : ReadBytes {
override fun read(buffer: ByteArray) {
protectedBinary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
dataOutputStream.write(buffer)
}
}
)
}
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader.toInt())
dataOutputStream.writeInt(0)

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
import com.kunzisoft.keepass.stream.NullOutputStream
import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
@@ -128,7 +128,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
header.version = DatabaseHeaderKDB.DBVER_DW
header.numGroups = mDatabaseKDB.numberOfGroups()
header.numEntries = mDatabaseKDB.numberOfEntries()
header.numKeyEncRounds = mDatabaseKDB.numberKeyEncryptionRounds.toInt()
header.numKeyEncRounds = mDatabaseKDB.numberKeyEncryptionRounds.toInt() // TODO Signed Long - Unsigned Int
setIVs(header)
@@ -197,7 +197,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
@Suppress("CAST_NEVER_SUCCEEDS")
@Throws(DatabaseOutputException::class)
fun outputPlanGroupAndEntries(os: OutputStream) {
val los = LEDataOutputStream(os)
val los = LittleEndianDataOutputStream(os)
// useHeaderHash
if (headerHashBlock != null) {
@@ -261,7 +261,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
@Throws(IOException::class)
private fun writeExtData(headerDigest: ByteArray, os: OutputStream) {
val los = LEDataOutputStream(os)
val los = LittleEndianDataOutputStream(os)
writeExtDataField(los, 0x0001, headerDigest, headerDigest.size)
val headerRandom = ByteArray(32)
@@ -273,7 +273,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
@Throws(IOException::class)
private fun writeExtDataField(los: LEDataOutputStream, fieldType: Int, data: ByteArray?, fieldSize: Int) {
private fun writeExtDataField(los: LittleEndianDataOutputStream, fieldType: Int, data: ByteArray?, fieldSize: Int) {
los.writeUShort(fieldType)
los.writeInt(fieldSize)
if (data != null) {

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