mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
328 Commits
2.5.0.0bet
...
2.5beta30
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32d235e8c7 | ||
|
|
cb982b3513 | ||
|
|
d7ed6c26dd | ||
|
|
8ff19f7e68 | ||
|
|
729e062c3a | ||
|
|
7d0340ac07 | ||
|
|
01960e74c1 | ||
|
|
8e40250985 | ||
|
|
8ae2edb61a | ||
|
|
0baa7bcbf1 | ||
|
|
fffee48918 | ||
|
|
515abb6e14 | ||
|
|
6c1c3ff87f | ||
|
|
5b65575c7a | ||
|
|
0f258fc5f8 | ||
|
|
206bc661dc | ||
|
|
d0e35b109e | ||
|
|
61769c4f20 | ||
|
|
95778ee5f4 | ||
|
|
155030fdca | ||
|
|
98237ef76c | ||
|
|
f0a7b38199 | ||
|
|
9fc5e6751b | ||
|
|
4c1630312b | ||
|
|
d397c5c996 | ||
|
|
f6c41b5a60 | ||
|
|
06eb5c01c3 | ||
|
|
7df49f91e8 | ||
|
|
96dcbb0ce7 | ||
|
|
5f828fb986 | ||
|
|
533d663938 | ||
|
|
ae788503a9 | ||
|
|
cf0acd9c73 | ||
|
|
0857f2f1cf | ||
|
|
c05d412bdb | ||
|
|
c8e0ce717d | ||
|
|
cc3485b201 | ||
|
|
81ea7080c2 | ||
|
|
76ff6f5ae0 | ||
|
|
0c0d0b7a6f | ||
|
|
ec8cf1f6b7 | ||
|
|
da44310d1b | ||
|
|
4bd9c84bb0 | ||
|
|
3b1269a770 | ||
|
|
7c986ccee8 | ||
|
|
903bad8f36 | ||
|
|
4b9577437c | ||
|
|
c316011fbc | ||
|
|
3fb1f18c22 | ||
|
|
53935058f5 | ||
|
|
a3860c9581 | ||
|
|
dc20899d26 | ||
|
|
62ac3ddb75 | ||
|
|
b792a61bf9 | ||
|
|
aae9f9e1cb | ||
|
|
d098bf5e6a | ||
|
|
b0e8a3ecd9 | ||
|
|
4efa684022 | ||
|
|
f2c8082990 | ||
|
|
1abba80045 | ||
|
|
68564a2b75 | ||
|
|
385b701b38 | ||
|
|
11c9a1d707 | ||
|
|
c5aef6b561 | ||
|
|
dfcf73cfd0 | ||
|
|
7fc9389700 | ||
|
|
d1af7349bc | ||
|
|
0fd955197d | ||
|
|
cbf33507d1 | ||
|
|
bc60a5d97e | ||
|
|
57596b2991 | ||
|
|
43b3602a52 | ||
|
|
c09ec961b8 | ||
|
|
d140b453b2 | ||
|
|
9eb42636ec | ||
|
|
ee2d663fce | ||
|
|
8e83615a22 | ||
|
|
0ff129c5ca | ||
|
|
e49858439f | ||
|
|
3df07f7f47 | ||
|
|
ff9e179593 | ||
|
|
7539fee04b | ||
|
|
71a339a58f | ||
|
|
9ef2ea016b | ||
|
|
de4936a16d | ||
|
|
574d2b8904 | ||
|
|
1f3f7634e7 | ||
|
|
3c0725baff | ||
|
|
b0e1411012 | ||
|
|
39daf4714d | ||
|
|
4706afa823 | ||
|
|
25977d389d | ||
|
|
f760110569 | ||
|
|
133e78fe97 | ||
|
|
d92e0c8620 | ||
|
|
62fdb69d6b | ||
|
|
def57c9fb2 | ||
|
|
21c9c898c3 | ||
|
|
1f03c922c2 | ||
|
|
3f6ae6bdac | ||
|
|
60615ee1eb | ||
|
|
92b0d1bfa9 | ||
|
|
237988dc1f | ||
|
|
a846ec29ca | ||
|
|
4533e96bff | ||
|
|
0a401c3ac9 | ||
|
|
468abaf077 | ||
|
|
4ccf2f641c | ||
|
|
34eb2785cf | ||
|
|
09dbfe323e | ||
|
|
1f06c5b425 | ||
|
|
b98e089f7a | ||
|
|
a0ad06ed0a | ||
|
|
ec63365429 | ||
|
|
2cb85e4346 | ||
|
|
0d7c479c51 | ||
|
|
5a6c21e662 | ||
|
|
d6cadac98f | ||
|
|
dac2fc2c37 | ||
|
|
0fb45cef0d | ||
|
|
5ebdbd4003 | ||
|
|
b30f1023cb | ||
|
|
e5f65a4d1e | ||
|
|
ab42a65aa4 | ||
|
|
e351456bfe | ||
|
|
452e68b08f | ||
|
|
d65beed7a1 | ||
|
|
f5a5a0e8cb | ||
|
|
98380a0906 | ||
|
|
22fe7508f3 | ||
|
|
c8e241fc76 | ||
|
|
2c943e00d0 | ||
|
|
7ddb83b72d | ||
|
|
50bac01699 | ||
|
|
e0a92dfadd | ||
|
|
b50c951091 | ||
|
|
2f589a95a9 | ||
|
|
53eac86a95 | ||
|
|
9412f8955e | ||
|
|
71c98d82b1 | ||
|
|
42c1a925b4 | ||
|
|
1e84534ffd | ||
|
|
b087733e37 | ||
|
|
675efe29ac | ||
|
|
9833af8225 | ||
|
|
790c624571 | ||
|
|
67a70a8453 | ||
|
|
64bb05e2dd | ||
|
|
c111db6e73 | ||
|
|
24c5915bd3 | ||
|
|
04c3717618 | ||
|
|
78275a0984 | ||
|
|
bd908ed10d | ||
|
|
5473deec95 | ||
|
|
653258afd2 | ||
|
|
00a2210eea | ||
|
|
ec5688a013 | ||
|
|
fc0c7b5708 | ||
|
|
464f7ac486 | ||
|
|
fb2457146c | ||
|
|
d90870838e | ||
|
|
8c45f23291 | ||
|
|
315a3efa52 | ||
|
|
28abb5ae6f | ||
|
|
bb78d89682 | ||
|
|
fd36e19168 | ||
|
|
58db516e44 | ||
|
|
2b875e94dc | ||
|
|
0ba2447f55 | ||
|
|
8e684d0d3a | ||
|
|
ce3c1d4685 | ||
|
|
2cd77d47eb | ||
|
|
ac9366e351 | ||
|
|
af356586f8 | ||
|
|
7650db81a4 | ||
|
|
8925c86afd | ||
|
|
8f1836009e | ||
|
|
5fcf3f9b95 | ||
|
|
1c466d9e40 | ||
|
|
b97c2d9cbc | ||
|
|
3d970e4967 | ||
|
|
765c8f53dd | ||
|
|
4e81caeadf | ||
|
|
08b10e4d58 | ||
|
|
f8594f72e8 | ||
|
|
24ebee07cd | ||
|
|
a06ebb0991 | ||
|
|
cd5d4498e7 | ||
|
|
0bd62780c6 | ||
|
|
896f9327d6 | ||
|
|
cc83a99efe | ||
|
|
b6da20fef7 | ||
|
|
2912678559 | ||
|
|
409e870ef0 | ||
|
|
3d84074a0a | ||
|
|
a717fdfed4 | ||
|
|
be1f68015b | ||
|
|
2c29dcf1f6 | ||
|
|
2b1173177f | ||
|
|
3c0f7dc79c | ||
|
|
39d813bf3a | ||
|
|
5e2bc0d05b | ||
|
|
ec751159ae | ||
|
|
dda8b95f83 | ||
|
|
286012fe2a | ||
|
|
ab27299789 | ||
|
|
dfcdd5aa88 | ||
|
|
a05ea52689 | ||
|
|
de12b5de5b | ||
|
|
57b03eaca4 | ||
|
|
36bd00b760 | ||
|
|
4578a9974a | ||
|
|
d0371f58c6 | ||
|
|
9f80457351 | ||
|
|
56daf6f676 | ||
|
|
7f7b8d423b | ||
|
|
031afc80cb | ||
|
|
b77e28b04d | ||
|
|
2f8c3fdcfe | ||
|
|
f501a87099 | ||
|
|
d0ec5f26dd | ||
|
|
97776e9329 | ||
|
|
a19356c49e | ||
|
|
4f16918cf0 | ||
|
|
1af9761144 | ||
|
|
d74e814c79 | ||
|
|
16d09bca6c | ||
|
|
462c29b769 | ||
|
|
3939276d58 | ||
|
|
ff85f18c4c | ||
|
|
dd14fe4123 | ||
|
|
d804659ee2 | ||
|
|
e3152cf901 | ||
|
|
de236f321f | ||
|
|
66f4611866 | ||
|
|
6a6ef052af | ||
|
|
4e4606dabd | ||
|
|
0ce11103ab | ||
|
|
899d0e0557 | ||
|
|
4aefeff41f | ||
|
|
08d59e50e8 | ||
|
|
e6c06aba8c | ||
|
|
e5c2a04922 | ||
|
|
c134ccf8d9 | ||
|
|
4f959d1ff6 | ||
|
|
1c06d93951 | ||
|
|
b2dff29baa | ||
|
|
72633e9472 | ||
|
|
898a88f7d8 | ||
|
|
c2f09f6666 | ||
|
|
d45bcbc347 | ||
|
|
1f834567f8 | ||
|
|
0b62c04867 | ||
|
|
c71bb2a570 | ||
|
|
0a575b5bf8 | ||
|
|
6f2bf903c5 | ||
|
|
210ad4b8db | ||
|
|
b42cf0e204 | ||
|
|
2045d3aab8 | ||
|
|
e20cb9431d | ||
|
|
7161eaea8b | ||
|
|
b786da52f5 | ||
|
|
ccbfec838d | ||
|
|
f5c1872225 | ||
|
|
3001013bad | ||
|
|
1280803e5e | ||
|
|
f84fc07fe0 | ||
|
|
d83e53f589 | ||
|
|
52ba2c53f1 | ||
|
|
f266e1de4c | ||
|
|
5a0aafd3ce | ||
|
|
c7ce07a43c | ||
|
|
a73644c285 | ||
|
|
e28f1ffc99 | ||
|
|
afc691b2f4 | ||
|
|
00f6eb83c3 | ||
|
|
bbf51ebbec | ||
|
|
7816c8f16e | ||
|
|
fdbcba2412 | ||
|
|
50dc6cb0aa | ||
|
|
e413198d12 | ||
|
|
72592772f9 | ||
|
|
f918206bcd | ||
|
|
6b34d67da8 | ||
|
|
04fb093d4d | ||
|
|
7de0e0cc4a | ||
|
|
5cf7688e38 | ||
|
|
97cd06b52b | ||
|
|
6d522ead1d | ||
|
|
8e0fae62f3 | ||
|
|
af72098d60 | ||
|
|
62da582f4e | ||
|
|
f3efa6eddc | ||
|
|
c85fb7bd0a | ||
|
|
fc7b183461 | ||
|
|
a378810f88 | ||
|
|
2f5b322fad | ||
|
|
efb9b50f85 | ||
|
|
84fdef8eb6 | ||
|
|
76050a261b | ||
|
|
2667361450 | ||
|
|
a2b3313cc0 | ||
|
|
d343446235 | ||
|
|
cbce70f8a4 | ||
|
|
4573d6b6fb | ||
|
|
973305d13c | ||
|
|
4637016372 | ||
|
|
698ba4f7f1 | ||
|
|
502ebabf1f | ||
|
|
13f88626ca | ||
|
|
a39f58f7b5 | ||
|
|
aeb36468ce | ||
|
|
fdd196526d | ||
|
|
ecca892b16 | ||
|
|
03343d0301 | ||
|
|
a61c8b0cb6 | ||
|
|
a92129da00 | ||
|
|
0b783d6390 | ||
|
|
9aa1a2e30e | ||
|
|
15b3c69514 | ||
|
|
f2722e09ec | ||
|
|
b442859be0 | ||
|
|
1496a2efb1 | ||
|
|
a0edb111f0 | ||
|
|
6bcf54fe29 | ||
|
|
eb0e496cfd | ||
|
|
12c2a6e99c | ||
|
|
714433b62d |
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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]
|
||||
|
||||
97
CHANGELOG
97
CHANGELOG
@@ -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.
|
||||
|
||||
12
ReadMe.md
12
ReadMe.md
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -112,6 +112,14 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||
ActionDatabaseAsyncTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteAllKeyFiles() {
|
||||
ActionDatabaseAsyncTask(
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user