Compare commits

...

93 Commits

Author SHA1 Message Date
J-Jamet
ecc4550261 Add Deutsh description 2022-09-07 22:42:18 +02:00
J-Jamet
8b046512e3 fix: upgrade Gemfile.lock 2022-09-04 14:20:36 +02:00
J-Jamet
228a10c8e0 Merge branch 'translations' into develop 2022-09-04 12:24:32 +02:00
J-Jamet
9c53bea190 fix: replace <strong> tags 2022-09-04 12:23:49 +02:00
J-Jamet
11cf991498 Merge branch 'develop' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2022-09-04 12:15:55 +02:00
J-Jamet
a88c3721b2 Merge branch 'translations' into develop 2022-09-04 12:14:33 +02:00
J-Jamet
0b4b6d4d91 feat: upgrade to 3.5.0 2022-09-04 12:13:19 +02:00
J-Jamet
941f9bcd48 fix: Change key driver url 2022-09-03 23:03:11 +02:00
solokot
f1bf9fb25c Translated using Weblate (Russian)
Currently translated at 99.5% (628 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-09-03 19:24:05 +02:00
Matthaiks
1751fa49c0 Translated using Weblate (Polish)
Currently translated at 99.6% (629 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-09-03 19:24:05 +02:00
Kunzisoft
6b4fc9a4fa Translated using Weblate (French)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2022-09-03 19:24:04 +02:00
Retrial
7c8d85e428 Translated using Weblate (Greek)
Currently translated at 99.5% (628 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-09-03 19:24:04 +02:00
Allan Nordhøy
e335140f23 Translated using Weblate (English)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2022-09-03 19:24:03 +02:00
Kunzisoft
d85f398b5f Translated using Weblate (English)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2022-09-03 19:24:03 +02:00
Wilker Santana da Silva
a16082a59d Translated using Weblate (English)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2022-09-03 19:11:24 +02:00
Kunzisoft
456269a343 Translated using Weblate (English)
Currently translated at 100.0% (631 of 631 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2022-09-03 19:11:23 +02:00
J-Jamet
eb8e1e20eb fix: Remove cancel button for development dialog 2022-09-03 17:30:31 +02:00
Hosted Weblate
ed3c84fec0 Merge branch 'origin/develop' into Weblate. 2022-09-03 17:27:29 +02:00
J-Jamet
be40416a2d feat: Add privacy text in About section 2022-09-03 17:25:20 +02:00
PiQuark6046
5b5476a513 Translated using Weblate (Korean)
Currently translated at 35.0% (221 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2022-09-01 19:19:15 +02:00
random r
dc64dd6400 Translated using Weblate (Italian)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-09-01 19:19:14 +02:00
atilluF
eca02d3bde Translated using Weblate (Italian)
Currently translated at 97.4% (614 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-08-31 15:15:20 +02:00
SC
176b6c2936 Translated using Weblate (Portuguese)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt/
2022-08-26 20:21:24 +02:00
J-Jamet
5b22350bdf fix: Hide clipboard text when copy entry field #1386 2022-08-23 11:56:29 +02:00
J-Jamet
6e1e011234 fix: exec gradlew version 7.5.1 to update scripts 2022-08-23 11:32:10 +02:00
J-Jamet
ac65ef6a5c Merge branch 'develop' of github.com:Kunzisoft/KeePassDX into develop 2022-08-23 11:26:35 +02:00
Jérémy JAMET
fc198dde74 Merge pull request #1387 from lberrymage/update-gradle
Upgrade Gradle to 7.5.1
2022-08-23 11:25:59 +02:00
lberrymage
15ac51b2fc Upgrade Gradle to 7.5.1
Generated by `./gradlew wrapper --gradle-version 7.5.1`
2022-08-22 18:09:13 -08:00
J-Jamet
34214432e1 fix: upgrade libs 2022-08-17 23:01:45 +02:00
J-Jamet
361ca92493 fix: upgrade gradle plugin to 7.2.2 2022-08-17 22:56:50 +02:00
J-Jamet
e367051b80 fix: remove application/octet-stream file recognition #1211 2022-08-17 22:25:47 +02:00
Linerly
a2a4a50c5e Translated using Weblate (Indonesian)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2022-08-14 14:20:41 +02:00
Milo Ivir
afc74b2f2a Translated using Weblate (Croatian)
Currently translated at 99.2% (625 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2022-08-14 14:20:40 +02:00
devchung
fc756d1eaf Translated using Weblate (Chinese (Traditional))
Currently translated at 97.3% (613 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2022-08-14 14:20:39 +02:00
Eric
eb8a4b1e49 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2022-08-14 14:20:39 +02:00
Ihor Hordiichuk
8d258b3538 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2022-08-14 14:20:38 +02:00
solokot
a59cfa3477 Translated using Weblate (Russian)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2022-08-14 14:20:38 +02:00
Matthaiks
f1e513006e Translated using Weblate (Polish)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-08-14 14:20:37 +02:00
Retrial
9df5c8f439 Translated using Weblate (Greek)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2022-08-14 14:20:36 +02:00
Deleted User
3ae099accf Translated using Weblate (German)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-08-14 14:20:36 +02:00
VfBFan
bb3e9396f2 Translated using Weblate (German)
Currently translated at 100.0% (630 of 630 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-08-14 14:20:35 +02:00
Hosted Weblate
1628749bde Merge branch 'origin/develop' into Weblate. 2022-08-11 12:47:58 +02:00
J-Jamet
f2006b5e42 fix: show lock button hidden by screenshot mode banner #1377 2022-08-11 12:37:28 +02:00
GianpaMX
80d387d9e7 Add screenshot mode
* Add new screenshot mode entry under Settings -> App -> General
* Disable Screenshot mode  by default
* Add a screenshot mode indication at the bottom of the screen
* Set or clear window FLAG_SECURE accordingly
* Translate strings into Spanish
2022-08-10 15:19:09 +01:00
J-Jamet
4452b4d599 Merge branch 'feature/Hardware_Key' into develop 2022-08-08 14:00:21 +02:00
J-Jamet
dfeaeb9888 feature: todo open external app in f-droid 2022-08-08 13:57:51 +02:00
J-Jamet
7e45a20ee7 fix: Refactoring key driver app id 2022-08-07 23:27:59 +02:00
J-Jamet
f3fe92e4de Merge branch 'develop' into feature/Hardware_Key 2022-08-02 22:25:44 +02:00
J-Jamet
b606909c65 fix: Update libs and SDK to 32 2022-08-02 22:25:19 +02:00
J-Jamet
2882bb30d7 fix: Smaller advanced unlock UI 2022-08-02 21:47:33 +02:00
eamz8jpajok
5b62227e3f Translated using Weblate (German)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2022-07-28 23:21:09 +02:00
J-Jamet
8b6af6fd8a feat: Derive master key exception 2022-07-05 18:09:35 +02:00
J-Jamet
99e9a92953 fix: KDB opening 2022-07-05 17:55:12 +02:00
Noël Krähenbühl
9f626309c3 Translated using Weblate (English (United Kingdom))
Currently translated at 8.3% (51 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en_GB/
2022-06-25 17:16:13 +02:00
Anonimas
3fe7cf2bfd Translated using Weblate (Lithuanian)
Currently translated at 23.4% (143 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2022-06-19 16:16:36 +02:00
Matthaiks
9b5c274b49 Translated using Weblate (Polish)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2022-06-18 00:19:02 +02:00
WB
46b350e7ac Translated using Weblate (Galician)
Currently translated at 23.4% (143 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2022-06-16 00:19:34 +02:00
Óscar Fernández Díaz
22a4aeb108 Translated using Weblate (Spanish)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2022-06-14 00:19:14 +02:00
random r
332e116ba7 Translated using Weblate (Italian)
Currently translated at 100.0% (609 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2022-06-04 11:15:25 +02:00
Anonimas
8b594a1a1f Translated using Weblate (Lithuanian)
Currently translated at 22.9% (140 of 609 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lt/
2022-06-02 18:17:01 +02:00
J-Jamet
ab23ec6d4d Merge tag '3.4.5' into develop
3.4.5
2022-06-02 11:29:08 +02:00
J-Jamet
647e3f9383 Change intent challenge recognition 2022-05-30 18:19:01 +02:00
J-Jamet
597f52799d Cache to capture exception during database save 2022-05-30 16:59:33 +02:00
J-Jamet
a59e052ed8 Merge branch 'develop' into feature/Hardware_Key 2022-05-30 10:31:46 +02:00
J-Jamet
1ff2f501ca Fix capture database exception 2022-05-19 21:22:13 +02:00
J-Jamet
cfcb49e233 Better management of exceptions 2022-05-19 21:16:29 +02:00
J-Jamet
467df2020e Fix merge 2022-05-19 19:48:43 +02:00
J-Jamet
a961b41de0 Fix file save outside of the app 2022-05-19 19:20:21 +02:00
J-Jamet
40e8d5225e Fix notification and save state 2022-05-19 15:54:34 +02:00
J-Jamet
bc755ae1df Fix progress message 2022-05-19 15:00:12 +02:00
J-Jamet
b1cb0c3786 Fix infinite loop 2022-05-19 13:41:38 +02:00
J-Jamet
090d0fa2db Encapsulate channels 2022-05-19 12:53:12 +02:00
J-Jamet
27918a12b0 Fix small bugs 2022-05-19 11:47:53 +02:00
J-Jamet
ba1498b0b2 Fix error message and better implementation 2022-05-19 11:15:28 +02:00
J-Jamet
cbde96dd82 Add waiting task message and cancellable 2022-05-18 19:49:18 +02:00
J-Jamet
344118a755 Better error management 2022-05-18 18:35:24 +02:00
J-Jamet
259c8a4bd9 Setting to remember hardware key 2022-05-18 16:39:35 +02:00
J-Jamet
f4d5bd1bea Fix save and better write implementation 2022-05-11 15:26:49 +02:00
J-Jamet
20b352cabe Better code encapsulation 2022-05-11 14:19:32 +02:00
J-Jamet
20e35f1a69 Encapsulate database operations 2022-05-11 13:42:48 +02:00
J-Jamet
d963f56d0f Merge branch 'develop' into feature/Hardware_Key 2022-05-11 11:36:20 +02:00
J-Jamet
5734df89f0 Merge branch 'develop' into feature/Hardware_Key 2022-05-11 10:10:52 +02:00
J-Jamet
327c9de464 Change main credential validation 2022-05-10 19:59:56 +02:00
J-Jamet
8b2f994769 Save database with challenge response 2022-05-10 15:02:22 +02:00
J-Jamet
a5e53d872b Open database with challenge response in service 2022-05-09 15:56:53 +02:00
J-Jamet
19bc2444bc Merge branch 'develop' into feature/Hardware_Key 2022-05-05 16:15:03 +02:00
J-Jamet
b44c9cfc51 Opening refactoring 2022-04-28 20:39:26 +02:00
J-Jamet
5b4338abae Better implementation for challenge response intent 2022-04-27 14:39:08 +02:00
J-Jamet
e8f79ae467 Fix to open challenge-response dynamically / refactoring methods #8 2022-04-25 21:47:43 +02:00
J-Jamet
ecbee73eae Add view and first implementation of hardware key #8 2022-04-21 18:03:32 +02:00
J-Jamet
1874a0056d Merge branch 'develop' into feature/Hardware_Key 2022-04-20 14:24:10 +02:00
J-Jamet
279bd16b74 Best autofill recognition #1250 2022-02-26 13:13:07 +01:00
J-Jamet
2e0081b66c Prepare hardware key in main credential 2022-02-26 12:51:00 +01:00
136 changed files with 3580 additions and 1528 deletions

View File

@@ -1,3 +1,10 @@
KeePassDX(3.5.0)
* Support YubiKey challenge-response #8 #137
* Better exception management during database save #1346
* Better management of mime-types and extensions #1211
* Add "Screenshot mode" setting #459 #1377 #1354 (Thx @GianpaMX)
* Hide clipboard sensitive text when copy entry field #1386
KeePassDX(3.4.5)
* Fix custom data in group (fix KeeShare) #1335
* Fix device credential unlocking #1344

View File

@@ -3,25 +3,25 @@ GEM
specs:
CFPropertyList (3.0.5)
rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.577.0)
aws-sdk-core (3.130.1)
aws-partitions (1.626.0)
aws-sdk-core (3.140.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.55.0)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.58.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.113.0)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-sigv4 (1.5.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
@@ -34,10 +34,10 @@ GEM
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.92.2)
faraday (1.10.0)
excon (0.92.4)
faraday (1.10.2)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -56,8 +56,8 @@ GEM
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.3)
multipart-post (>= 1.2, < 3)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
@@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.205.1)
fastlane (2.209.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -107,9 +107,9 @@ GEM
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-versioning_android (0.1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.19.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.2)
google-apis-androidpublisher_v3 (0.25.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-core (0.7.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -118,27 +118,27 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.13.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-iamcredentials_v1 (0.13.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-playcustomapp_v1 (0.10.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-storage_v1 (0.17.0)
google-apis-core (>= 0.7, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.1)
google-cloud-storage (1.39.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.17.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.1.2)
googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -146,12 +146,12 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.4)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
json (2.6.1)
jwt (2.3.0)
json (2.6.2)
jwt (2.5.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
@@ -162,9 +162,9 @@ GEM
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.7)
public_suffix (5.0.0)
rake (13.0.6)
representable (3.1.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
@@ -174,9 +174,9 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.16.1)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.0)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
@@ -193,11 +193,11 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.1)
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.21.0)
xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)

View File

@@ -12,8 +12,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 32
versionCode = 114
versionName = "3.4.5"
versionCode = 115
versionName = "3.5.0 Beta01"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -93,7 +93,7 @@ android {
}
}
def room_version = "2.4.2"
def room_version = "2.4.3"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@@ -101,14 +101,14 @@ dependencies {
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.media:media:1.6.0'
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:$android_core_version"
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.fragment:fragment-ktx:1.5.2'
implementation "com.google.android.material:material:$android_material_version"
// Token auto complete
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed

View File

@@ -0,0 +1,90 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "f8fb4aed546de19ae7ca0797f49b26a4",
"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, `hardware_key` 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": "hardwareKey",
"columnName": "hardware_key",
"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, 'f8fb4aed546de19ae7ca0797f49b26a4')"
]
}
}

View File

@@ -89,7 +89,6 @@
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="application/octet-stream" />
<data android:mimeType="application/x-kdb" />
<data android:mimeType="application/x-kdbx" />
<data android:mimeType="application/x-keepass" />

View File

@@ -77,6 +77,12 @@ class AboutActivity : StylishActivity() {
HtmlCompat.FROM_HTML_MODE_LEGACY)
}
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_privacy),
HtmlCompat.FROM_HTML_MODE_LEGACY)
}
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),

View File

@@ -55,7 +55,8 @@ import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
@@ -156,7 +157,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity(
databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri
fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey
)
}
}
@@ -250,7 +252,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
?: MainCredential()
databaseFilesViewModel.addDatabaseFile(
databaseUri,
mainCredential.keyFileUri
mainCredential.keyFileUri,
mainCredential.hardwareKey
)
}
}
@@ -297,10 +300,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
}
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
MainCredentialActivity.launch(this,
databaseUri,
keyFile,
hardwareKey,
{ exception ->
fileNoFoundAction(exception)
},
@@ -321,7 +325,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null)
launchPasswordActivity(databaseUri, null, null)
// Delete flickering for kitkat <=
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
overridePendingTransition(0, 0)

View File

@@ -69,7 +69,7 @@ import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK

View File

@@ -56,9 +56,11 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
@@ -101,6 +103,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null
private var mRememberHardwareKey: Boolean = false
private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false
@@ -133,11 +137,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
PreferencesUtil.enableReadOnlyDatabase(this)
}
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
// Build elements to manage keyfile selection
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri)
mainCredentialView?.populateKeyFileView(uri)
}
}
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
@@ -171,6 +177,16 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
mainCredentialView?.onKeyFileChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
mainCredentialView?.onHardwareKeyChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
// Observe if default database
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
@@ -204,10 +220,19 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
databaseKeyFileUri
}
val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey
val hardwareKey =
if (mRememberHardwareKey
&& databaseHardwareKey == null) {
databaseFile?.hardwareKey
} else {
databaseHardwareKey
}
// Define title
filenameView?.text = databaseFile?.databaseAlias ?: ""
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
}
}
@@ -215,6 +240,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
super.onResume()
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
// Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
@@ -332,24 +358,36 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private fun getUriFromIntent(intent: Intent?) {
// If is a view intent
val action = intent?.action
if (action != null
&& action == VIEW_INTENT) {
mDatabaseFileUri = intent.data
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
if (action == VIEW_INTENT) {
fillCredentials(
intent.data,
UriUtil.getUriFromIntent(intent, KEY_KEYFILE),
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
)
} else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
mainCredentialView?.populateKeyFileTextView(it)
}
fillCredentials(
intent?.getParcelableExtra(KEY_FILENAME),
intent?.getParcelableExtra(KEY_KEYFILE),
HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
)
}
try {
intent?.removeExtra(KEY_KEYFILE)
intent?.removeExtra(KEY_HARDWARE_KEY)
} catch (e: Exception) {}
mDatabaseFileUri?.let {
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
}
}
private fun fillCredentials(databaseUri: Uri?,
keyFileUri: Uri?,
hardwareKey: HardwareKey?) {
mDatabaseFileUri = databaseUri
mainCredentialView?.populateKeyFileView(keyFileUri)
mainCredentialView?.populateHardwareKeyView(hardwareKey)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
getUriFromIntent(intent)
@@ -358,7 +396,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private fun launchGroupActivityIfLoaded(database: Database) {
// Check if database really loaded
if (database.loaded) {
clearCredentialsViews(true)
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
GroupActivity.launch(this,
database,
{ onValidateSpecialMode() },
@@ -408,7 +446,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
when (cipherDecryptDatabase.credentialStorage) {
CredentialStorage.PASSWORD -> {
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
}
CredentialStorage.KEY_FILE -> {
// TODO advanced unlock key file
@@ -423,14 +461,23 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
)
}
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
private fun onDatabaseFileLoaded(databaseFileUri: Uri?,
keyFileUri: Uri?,
hardwareKey: HardwareKey?) {
// Define Key File text
if (mRememberKeyFile) {
mainCredentialView?.populateKeyFileTextView(keyFileUri)
mainCredentialView?.populateKeyFileView(keyFileUri)
}
// Define hardware key
if (mRememberHardwareKey) {
mainCredentialView?.populateHardwareKeyView(hardwareKey)
}
// Define listener for validate button
confirmButtonView?.setOnClickListener { loadDatabase() }
confirmButtonView?.setOnClickListener {
mainCredentialView?.validateCredential()
}
// If Activity is launch with a password and want to open directly
val intent = intent
@@ -462,10 +509,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
}
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile,
clearHardwareKey: Boolean = !mRememberHardwareKey) {
mainCredentialView?.populatePasswordTextView(null)
if (clearKeyFile) {
mainCredentialView?.populateKeyFileTextView(null)
mainCredentialView?.populateKeyFileView(null)
}
if (clearHardwareKey) {
mainCredentialView?.populateHardwareKeyView(null)
}
}
@@ -656,18 +707,24 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile"
private const val KEY_HARDWARE_KEY = "hardwareKey"
private const val VIEW_INTENT = "android.intent.action.VIEW"
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
private fun buildAndLaunchIntent(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null)
intent.putExtra(KEY_KEYFILE, keyFile)
if (hardwareKey != null)
intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString())
intentBuildLauncher.invoke(intent)
}
@@ -680,8 +737,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
@Throws(FileNotFoundException::class)
fun launch(activity: Activity,
databaseFile: Uri,
keyFile: Uri?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
keyFile: Uri?,
hardwareKey: HardwareKey?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent)
}
}
@@ -696,8 +754,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForSearchResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult(
activity,
intent,
@@ -715,8 +774,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForSaveResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSaveModeResult(
activity,
intent,
@@ -734,8 +794,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForKeyboardResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
activity,
intent,
@@ -754,10 +815,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForAutofillResult(activity: AppCompatActivity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
@@ -775,8 +837,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForRegistration(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
registerInfo: RegisterInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
activity,
intent,
@@ -792,6 +855,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launch(activity: AppCompatActivity,
databaseUri: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
@@ -800,43 +864,67 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
try {
EntrySelectionHelper.doSpecialAction(activity.intent,
{
MainCredentialActivity.launch(activity,
databaseUri, keyFile)
launch(
activity,
databaseUri,
keyFile,
hardwareKey
)
},
{ searchInfo -> // Search Action
MainCredentialActivity.launchForSearchResult(activity,
databaseUri, keyFile,
searchInfo)
launchForSearchResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Save Action
MainCredentialActivity.launchForSaveResult(activity,
databaseUri, keyFile,
searchInfo)
launchForSaveResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Keyboard Selection Action
MainCredentialActivity.launchForKeyboardResult(activity,
databaseUri, keyFile,
searchInfo)
launchForKeyboardResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo, autofillComponent -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
MainCredentialActivity.launchForAutofillResult(activity,
databaseUri, keyFile,
launchForAutofillResult(
activity,
databaseUri,
keyFile,
hardwareKey,
autofillActivityResultLauncher,
autofillComponent,
searchInfo)
searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
{ registerInfo -> // Registration Action
MainCredentialActivity.launchForRegistration(activity,
databaseUri, keyFile,
registerInfo)
launchForRegistration(
activity,
databaseUri,
keyFile,
hardwareKey,
registerInfo
)
onLaunchActivitySpecialMode()
}
)

View File

@@ -27,7 +27,7 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.MainCredentialView
@@ -95,7 +95,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri)
mainCredentialView?.populateKeyFileView(uri)
}
}
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)

View File

@@ -26,7 +26,7 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
class PasswordEncodingDialogFragment : DialogFragment() {

View File

@@ -45,13 +45,16 @@ class ProFeatureDialogFragment : DialogFragment() {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.download) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
UriUtil.gotoUrl(activity,
activity.getString(R.string.play_store_url,
activity.getString(R.string.keepro_app_id))
)
}
} else {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.contribute) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
UriUtil.gotoUrl(activity, R.string.contribution_url)
}
}
builder.setMessage(stringBuilder)

View File

@@ -35,9 +35,12 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.HardwareKeySelectionView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.applyFontVisibility
@@ -45,18 +48,21 @@ import com.kunzisoft.keepass.view.applyFontVisibility
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null
private var mKeyFileUri: Uri? = null
private var mHardwareKey: HardwareKey? = null
private var rootView: View? = null
private lateinit var rootView: View
private var passwordCheckBox: CompoundButton? = null
private lateinit var passwordCheckBox: CompoundButton
private lateinit var passwordView: PassKeyView
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
private lateinit var passwordRepeatView: TextView
private var passKeyView: PassKeyView? = null
private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null
private lateinit var keyFileCheckBox: CompoundButton
private lateinit var keyFileSelectionView: KeyFileSelectionView
private var keyFileCheckBox: CompoundButton? = null
private var keyFileSelectionView: KeyFileSelectionView? = null
private lateinit var hardwareKeyCheckBox: CompoundButton
private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
private var mListener: AssignMainCredentialDialogListener? = null
@@ -67,13 +73,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mNoKeyConfirmationDialog: AlertDialog? = null
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
private var mAllowNoMasterKey: Boolean = false
private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
passwordCheckBox?.isChecked = true
passwordCheckBox.isChecked = true
}
}
@@ -113,10 +121,9 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
var allowNoMasterKey = false
arguments?.apply {
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
}
val builder = AlertDialog.Builder(activity)
@@ -128,57 +135,58 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
rootView.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
}
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passKeyView = rootView?.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation)
passwordRepeatView?.applyFontVisibility()
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
passwordView = rootView.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
passwordRepeatView.applyFontVisibility()
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
keyFileSelectionView.error = null
keyFileCheckBox.isChecked = true
keyFileSelectionView.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
hardwareKeySelectionView.selectionListener = { hardwareKey ->
hardwareKeyCheckBox.isChecked = true
hardwareKeySelectionView.error =
if (!HardwareKeyResponseHelper.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
// show hardware driver dialog if required
getString(R.string.error_driver_required, hardwareKey.toString())
} else {
null
}
}
val dialog = builder.create()
if (passwordCheckBox != null && keyFileCheckBox!= null) {
dialog.setOnShowListener { dialog1 ->
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
mMasterPassword = ""
mKeyFile = null
mKeyFileUri = null
mHardwareKey = null
var error = verifyPassword() || verifyKeyFile()
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
error = true
if (allowNoMasterKey)
showNoKeyConfirmationDialog()
else {
passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
}
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
approveMainCredential()
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
@@ -186,7 +194,6 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
dismiss()
}
}
}
return dialog
}
@@ -194,67 +201,113 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
return super.onCreateDialog(savedInstanceState)
}
private fun approveMainCredential() {
val errorPassword = verifyPassword()
val errorKeyFile = verifyKeyFile()
val errorHardwareKey = verifyHardwareKey()
// Check all to fill error
var error = errorPassword || errorKeyFile || errorHardwareKey
val hardwareKey = hardwareKeySelectionView.hardwareKey
if (!error
&& (!passwordCheckBox.isChecked)
&& (!keyFileCheckBox.isChecked)
&& (!hardwareKeyCheckBox.isChecked)
) {
error = true
if (mAllowNoMasterKey) {
// show no key dialog if required
showNoKeyConfirmationDialog()
} else {
passwordRepeatTextInputLayout.error =
getString(R.string.error_disallow_no_credentials)
}
} else if (!error
&& mMasterPassword.isNullOrEmpty()
&& !keyFileCheckBox.isChecked
&& !hardwareKeyCheckBox.isChecked
) {
// show empty password dialog if required
error = true
showEmptyPasswordConfirmationDialog()
} else if (!error
&& hardwareKey != null
&& !HardwareKeyResponseHelper.isHardwareKeyAvailable(
requireActivity(), hardwareKey, false)
) {
// show hardware driver dialog if required
error = true
hardwareKeySelectionView.error =
getString(R.string.error_driver_required, hardwareKey.toString())
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
}
private fun verifyPassword(): Boolean {
var error = false
passwordRepeatTextInputLayout.error = null
if (passwordCheckBox.isChecked) {
mMasterPassword = passwordView.passwordString
val confPassword = passwordRepeatView.text.toString()
// Verify that passwords match
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match)
}
}
return error
}
private fun verifyKeyFile(): Boolean {
var error = false
keyFileSelectionView.error = null
if (keyFileCheckBox.isChecked) {
keyFileSelectionView.uri?.let { uri ->
mKeyFileUri = uri
} ?: run {
error = true
keyFileSelectionView.error = getString(R.string.error_nokeyfile)
}
}
return error
}
private fun verifyHardwareKey(): Boolean {
var error = false
hardwareKeySelectionView.error = null
if (hardwareKeyCheckBox.isChecked) {
hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
mHardwareKey = hardwareKey
} ?: run {
error = true
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
}
}
return error
}
private fun retrieveMainCredential(): MainCredential {
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
return MainCredential(masterPassword, keyFile)
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
return MainCredential(masterPassword, keyFileUri, hardwareKey)
}
override fun onResume() {
super.onResume()
// To check checkboxes if a text is present
passKeyView?.addTextChangedListener(passwordTextWatcher)
passwordView.addTextChangedListener(passwordTextWatcher)
}
override fun onPause() {
super.onPause()
passKeyView?.removeTextChangedListener(passwordTextWatcher)
}
private fun verifyPassword(): Boolean {
var error = false
if (passwordCheckBox != null
&& passwordCheckBox!!.isChecked
&& passKeyView != null
&& passwordRepeatView != null) {
mMasterPassword = passKeyView!!.passwordString
val confPassword = passwordRepeatView!!.text.toString()
// Verify that passwords match
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
}
if ((mMasterPassword == null
|| mMasterPassword!!.isEmpty())
&& (keyFileCheckBox == null
|| !keyFileCheckBox!!.isChecked
|| keyFileSelectionView?.uri == null)) {
error = true
showEmptyPasswordConfirmationDialog()
}
}
return error
}
private fun verifyKeyFile(): Boolean {
var error = false
if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked) {
keyFileSelectionView?.uri?.let { uri ->
mKeyFile = uri
} ?: run {
error = true
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
}
}
return error
passwordView.removeTextChangedListener(passwordTextWatcher)
}
private fun showEmptyPasswordConfirmationDialog() {
@@ -262,11 +315,9 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@SetMainCredentialDialogFragment.dismiss()
}
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
mEmptyPasswordConfirmationDialog = builder.create()
mEmptyPasswordConfirmationDialog?.show()
@@ -299,8 +350,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
})
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ ->
keyFileCheckBox?.isChecked = false
keyFileSelectionView?.uri = null
keyFileCheckBox.isChecked = false
keyFileSelectionView.uri = null
}
mEmptyKeyFileConfirmationDialog = builder.create()
mEmptyKeyFileConfirmationDialog?.show()

View File

@@ -39,6 +39,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder()
/*
if (UriUtil.contributingUser(activity)) {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
@@ -46,14 +47,14 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
} else {
*/
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.contribute) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}
//}
builder.setMessage(stringBuilder)
// Create the AlertDialog object and return it
return builder.create()

View File

@@ -56,7 +56,7 @@ class ExternalFileHelper {
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
val resultCallback = ActivityResultCallback<Uri> { result ->
val resultCallback = ActivityResultCallback<Uri?> { result ->
result?.let { uri ->
UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri)
@@ -91,7 +91,7 @@ class ExternalFileHelper {
fun buildCreateDocument(typeString: String = "application/octet-stream",
onFileCreated: (fileCreated: Uri?)->Unit) {
val resultCallback = ActivityResultCallback<Uri> { result ->
val resultCallback = ActivityResultCallback<Uri?> { result ->
onFileCreated.invoke(result)
}
@@ -150,7 +150,7 @@ class ExternalFileHelper {
class OpenDocument : ActivityResultContracts.OpenDocument() {
@SuppressLint("InlinedApi")
override fun createIntent(context: Context, input: Array<out String>): Intent {
override fun createIntent(context: Context, input: Array<String>): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
@@ -178,11 +178,10 @@ class ExternalFileHelper {
}
}
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
class CreateDocument(typeString: String) : ActivityResultContracts.CreateDocument(typeString) {
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
}
}
}

View File

@@ -6,9 +6,10 @@ import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.ChallengeResponseViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
@@ -17,10 +18,12 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
protected var mDatabase: Database? = null
private val mChallengeResponseViewModel: ChallengeResponseViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider = DatabaseTaskProvider(this, mChallengeResponseViewModel)
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
val databaseWasReloaded = database?.wasReloaded == true
@@ -36,6 +39,13 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
}
}
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
mDatabase = null
super.onDestroy()
}
override fun onDatabaseRetrieved(database: Database?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)

View File

@@ -32,7 +32,6 @@ import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
@@ -44,7 +43,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable

View File

@@ -21,14 +21,21 @@ package com.kunzisoft.keepass.activities.stylish
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.WindowManager
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
import com.kunzisoft.keepass.settings.PreferencesUtil
/**
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
@@ -81,8 +88,24 @@ abstract class StylishActivity : AppCompatActivity() {
setTheme(themeId)
}
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(onScreenshotModePrefListener)
}
private val onScreenshotModePrefListener = OnSharedPreferenceChangeListener { _, key ->
if (key != getString(R.string.enable_screenshot_mode_key)) return@OnSharedPreferenceChangeListener
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
}
private fun setScreenshotMode(isEnabled: Boolean) {
findViewById<View>(R.id.screenshot_mode_banner)?.visibility = if (isEnabled) VISIBLE else GONE
// Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
if (isEnabled) {
window.clearFlags(FLAG_SECURE)
} else {
window.setFlags(FLAG_SECURE, FLAG_SECURE)
}
}
override fun onResume() {
@@ -94,6 +117,7 @@ abstract class StylishActivity : AppCompatActivity() {
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
recreateActivity()
}
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
}
private fun recreateActivity() {

View File

@@ -23,8 +23,15 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context
import androidx.room.AutoMigration
@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class])
@Database(
version = 2,
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
autoMigrations = [
AutoMigration (from = 1, to = 2)
]
)
abstract class AppDatabase : RoomDatabase() {
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app.database
import android.content.Context
import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -44,6 +45,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
DatabaseFile(
databaseUri,
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
fileDatabaseInfo.exists,
@@ -87,6 +89,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
DatabaseFile(
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.exists,
@@ -107,11 +110,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
).execute()
}
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
fun addOrUpdateDatabaseUri(databaseUri: Uri,
keyFileUri: Uri? = null,
hardwareKey: HardwareKey? = null,
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
addOrUpdateDatabaseFile(DatabaseFile(
databaseUri,
keyFileUri
keyFileUri,
hardwareKey
), databaseFileAddedOrUpdatedResult)
}
@@ -130,6 +136,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
?: fileDatabaseHistoryRetrieve?.databaseAlias
?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(),
databaseFileToAddOrUpdate.hardwareKey?.value,
System.currentTimeMillis()
)
@@ -149,6 +156,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
UriUtil.decode(fileDatabaseHistory.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.exists,
@@ -174,6 +182,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
UriUtil.decode(fileDatabaseHistory.databaseUri),
databaseFileToDelete.databaseAlias
)

View File

@@ -35,6 +35,9 @@ data class FileDatabaseHistoryEntity(
@ColumnInfo(name = "keyfile_uri")
var keyFileUri: String?,
@ColumnInfo(name = "hardware_key")
var hardwareKey: String?,
@ColumnInfo(name = "updated")
val updated: Long
) {

View File

@@ -34,12 +34,10 @@ import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage
@@ -398,7 +396,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
}
} ?: deleteEncryptedDatabaseKey()
}
} ?: throw IODatabaseException()
} ?: throw UnknownDatabaseLocationException()
} ?: throw Exception("AdvancedUnlockManager not initialized")
}

View File

@@ -24,15 +24,16 @@ 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.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.database.element.MainCredential
open class AssignMainCredentialInDatabaseRunnable (
context: Context,
database: Database,
protected val mDatabaseUri: Uri,
protected val mMainCredential: MainCredential)
: SaveDatabaseRunnable(context, database, true) {
mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, true, mainCredential, challengeResponseRetriever) {
private var mBackupKey: ByteArray? = null
@@ -40,10 +41,7 @@ open class AssignMainCredentialInDatabaseRunnable (
// Set key
try {
mBackupKey = ByteArray(database.masterKey.size)
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
database.masterKey.copyInto(mBackupKey!!)
} catch (e: Exception) {
erase(mBackupKey)
setError(e)

View File

@@ -24,7 +24,8 @@ 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.model.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil
class CreateDatabaseRunnable(context: Context,
@@ -33,9 +34,10 @@ class CreateDatabaseRunnable(context: Context,
private val databaseName: String,
private val rootName: String,
private val templateGroupName: String?,
mainCredential: MainCredential,
val mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private val createDatabaseResult: ((Result) -> Unit)?)
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential, challengeResponseRetriever) {
override fun onStartRun() {
try {
@@ -58,8 +60,11 @@ class CreateDatabaseRunnable(context: Context,
// Add database to recent files
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
.addOrUpdateDatabaseUri(
mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mainCredential.keyFileUri else null,
if (PreferencesUtil.rememberHardwareKey(context)) mainCredential.hardwareKey else null,
)
}
// Register the current time to init the lock timer

View File

@@ -38,12 +38,16 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
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.MainCredential
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
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.exception.InvalidCredentialsDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.ProgressMessage
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
@@ -82,6 +86,7 @@ import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.viewmodels.ChallengeResponseViewModel
import kotlinx.coroutines.launch
import java.util.*
@@ -92,7 +97,6 @@ import java.util.*
class DatabaseTaskProvider {
private var activity: FragmentActivity? = null
private var service: Service? = null
private var context: Context
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
@@ -111,30 +115,80 @@ class DatabaseTaskProvider {
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
constructor(activity: FragmentActivity) {
private var mChallengeResponseViewModel: ChallengeResponseViewModel? = null
constructor(activity: FragmentActivity,
challengeResponseViewModel: ChallengeResponseViewModel) {
this.activity = activity
this.context = activity
this.intentDatabaseTask = Intent(activity.applicationContext,
DatabaseTaskNotificationService::class.java)
// ViewModel used to keep response if activity recreated
this.mChallengeResponseViewModel = challengeResponseViewModel
// To manage hardware key challenge response
val hardwareKeyResponseHelper = HardwareKeyResponseHelper(activity)
hardwareKeyResponseHelper.buildHardwareKeyResponse { responseData, _ ->
// TODO Verify database
// Send to view model in case activity is restarted and not yet connected to service
challengeResponseViewModel.respond(responseData ?: ByteArray(0))
}
challengeResponseViewModel.dataResponded.observe(activity) { response ->
// Consume the response
if (response != null) {
val binder = mBinder
if (binder != null) {
binder.getService().respondToChallenge(response)
challengeResponseViewModel.consumeResponse()
}
}
}
this.requestChallengeListener = object: DatabaseTaskNotificationService.RequestChallengeListener {
override fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?) {
if (HardwareKeyResponseHelper.isHardwareKeyAvailable(activity, hardwareKey)) {
hardwareKeyResponseHelper.launchChallengeForResponse(hardwareKey, seed)
} else {
throw InvalidCredentialsDatabaseException(
context.getString(R.string.error_driver_required, hardwareKey.toString())
)
}
}
}
}
constructor(service: Service) {
this.service = service
this.context = service
this.intentDatabaseTask = Intent(service.applicationContext,
DatabaseTaskNotificationService::class.java)
}
fun destroy() {
this.activity = null
this.onDatabaseRetrieved = null
this.onActionFinish = null
this.databaseTaskBroadcastReceiver = null
this.mBinder = null
this.serviceConnection = null
this.progressTaskDialogFragment = null
this.databaseChangedDialogFragment = null
this.mChallengeResponseViewModel = null
}
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
startDialog(titleId, messageId, warningId)
override fun onStartAction(database: Database,
progressMessage: ProgressMessage) {
startDialog(progressMessage)
}
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
updateDialog(titleId, messageId, warningId)
override fun onUpdateAction(database: Database,
progressMessage: ProgressMessage) {
updateDialog(progressMessage)
}
override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) {
override fun onStopAction(database: Database,
actionTask: String,
result: ActionRunnable.Result) {
onActionFinish?.invoke(database, actionTask, result)
// Remove the progress task
stopDialog()
@@ -181,9 +235,9 @@ class DatabaseTaskProvider {
}
}
private fun startDialog(titleId: Int? = null,
messageId: Int? = null,
warningId: Int? = null) {
private var requestChallengeListener: DatabaseTaskNotificationService.RequestChallengeListener? = null
private fun startDialog(progressMessage: ProgressMessage) {
activity?.let { activity ->
activity.lifecycleScope.launch {
if (progressTaskDialogFragment == null) {
@@ -197,22 +251,17 @@ class DatabaseTaskProvider {
PROGRESS_TASK_DIALOG_TAG
)
}
updateDialog(titleId, messageId, warningId)
updateDialog(progressMessage)
}
}
}
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
private fun updateDialog(progressMessage: ProgressMessage) {
progressTaskDialogFragment?.apply {
titleId?.let {
updateTitle(it)
}
messageId?.let {
updateMessage(it)
}
warningId?.let {
updateWarning(it)
}
updateTitle(progressMessage.titleId)
updateMessage(progressMessage.messageId)
updateWarning(progressMessage.warningId)
setCancellable(progressMessage.cancelable)
}
}
@@ -226,25 +275,38 @@ class DatabaseTaskProvider {
serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addDatabaseListener(databaseListener)
addDatabaseFileInfoListener(databaseInfoListener)
addActionTaskListener(actionTaskListener)
addServiceListeners(this)
getService().checkDatabase()
getService().checkDatabaseInfo()
getService().checkAction()
}
mChallengeResponseViewModel?.resendResponse()
}
override fun onServiceDisconnected(name: ComponentName?) {
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
removeServiceListeners(mBinder)
mBinder = null
}
}
}
}
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.addDatabaseListener(databaseListener)
service?.addDatabaseFileInfoListener(databaseInfoListener)
service?.addActionTaskListener(actionTaskListener)
requestChallengeListener?.let {
service?.addRequestChallengeListener(it)
}
}
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.removeActionTaskListener(actionTaskListener)
service?.removeDatabaseFileInfoListener(databaseInfoListener)
service?.removeDatabaseListener(databaseListener)
service?.removeRequestChallengeListener()
}
private fun bindService() {
initServiceConnection()
serviceConnection?.let {
@@ -262,10 +324,6 @@ class DatabaseTaskProvider {
serviceConnection = null
}
fun isBinded(): Boolean {
return mBinder != null
}
fun registerProgressTask() {
stopDialog()
@@ -299,9 +357,7 @@ class DatabaseTaskProvider {
fun unregisterProgressTask() {
stopDialog()
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
removeServiceListeners(mBinder)
mBinder = null
unBindService()
@@ -321,7 +377,7 @@ class DatabaseTaskProvider {
context.startService(intentDatabaseTask)
} catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e)
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
}
}
@@ -332,7 +388,8 @@ class DatabaseTaskProvider {
*/
fun startDatabaseCreate(databaseUri: Uri,
mainCredential: MainCredential) {
mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
@@ -385,7 +442,8 @@ class DatabaseTaskProvider {
}
fun startDatabaseAssignPassword(databaseUri: Uri,
mainCredential: MainCredential) {
mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)

View File

@@ -25,9 +25,10 @@ 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.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -35,8 +36,9 @@ import com.kunzisoft.keepass.utils.UriUtil
class LoadDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
private val mUri: Uri,
private val mDatabaseUri: Uri,
private val mMainCredential: MainCredential,
private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray,
private val mReadonly: Boolean,
private val mCipherEncryptDatabase: CipherEncryptDatabase?,
private val mFixDuplicateUUID: Boolean,
@@ -51,18 +53,21 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onActionRun() {
try {
mDatabase.loadData(mUri,
mMainCredential,
mReadonly,
mDatabase.loadData(
context.contentResolver,
mDatabaseUri,
mMainCredential,
mChallengeResponseRetriever,
mReadonly,
UriUtil.getBinaryDir(context),
{ memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
mFixDuplicateUUID,
progressTaskUpdater)
progressTaskUpdater
)
}
catch (e: LoadDatabaseException) {
catch (e: DatabaseInputException) {
setError(e)
}
@@ -70,8 +75,11 @@ class LoadDatabaseRunnable(private val context: Context,
// Save keyFile in app database
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
.addOrUpdateDatabaseUri(
mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null,
if (PreferencesUtil.rememberHardwareKey(context)) mMainCredential.hardwareKey else null,
)
}
// Register the biometric

View File

@@ -22,9 +22,10 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -33,6 +34,7 @@ class MergeDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
private val mDatabaseToMergeUri: Uri?,
private val mDatabaseToMergeMainCredential: MainCredential?,
private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private val progressTaskUpdater: ProgressTaskUpdater?,
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() {
@@ -43,15 +45,17 @@ class MergeDatabaseRunnable(private val context: Context,
override fun onActionRun() {
try {
mDatabase.mergeData(mDatabaseToMergeUri,
mDatabaseToMergeMainCredential,
mDatabase.mergeData(
context.contentResolver,
mDatabaseToMergeUri,
mDatabaseToMergeMainCredential,
mDatabaseToMergeChallengeResponseRetriever,
{ memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
progressTaskUpdater
)
} catch (e: LoadDatabaseException) {
} catch (e: DatabaseException) {
setError(e)
}

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -47,7 +47,7 @@ class ReloadDatabaseRunnable(private val context: Context,
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
progressTaskUpdater)
} catch (e: LoadDatabaseException) {
} catch (e: DatabaseException) {
setError(e)
}

View File

@@ -21,12 +21,14 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.hardware.HardwareKey
class RemoveUnlinkedDataDatabaseRunnable (
context: Context,
database: Database,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
saveDatabase: Boolean,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
override fun onActionRun() {
try {

View File

@@ -23,11 +23,15 @@ import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
open class SaveDatabaseRunnable(protected var context: Context,
protected var database: Database,
private var saveDatabase: Boolean,
private var mainCredential: MainCredential?, // If null, uses composite Key
private var challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private var databaseCopyUri: Uri? = null)
: ActionRunnable() {
@@ -39,7 +43,12 @@ open class SaveDatabaseRunnable(protected var context: Context,
database.checkVersion()
if (saveDatabase && result.isSuccess) {
try {
database.saveData(databaseCopyUri, context.contentResolver)
database.saveData(
context.contentResolver,
context.cacheDir,
databaseCopyUri,
mainCredential,
challengeResponseRetriever)
} catch (e: DatabaseException) {
setError(e)
}

View File

@@ -22,14 +22,16 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateCompressionBinariesDatabaseRunnable (
context: Context,
database: Database,
private val oldCompressionAlgorithm: CompressionAlgorithm,
private val newCompressionAlgorithm: CompressionAlgorithm,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
saveDatabase: Boolean,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
override fun onStartRun() {
// Set new compression

View File

@@ -23,14 +23,16 @@ 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
import com.kunzisoft.keepass.hardware.HardwareKey
class DeleteEntryHistoryDatabaseRunnable (
context: Context,
database: Database,
private val mainEntry: Entry,
private val entryHistoryPosition: Int,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
saveDatabase: Boolean,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
override fun onStartRun() {
try {

View File

@@ -23,6 +23,7 @@ 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.hardware.HardwareKey
import com.kunzisoft.keepass.tasks.ActionRunnable
class RestoreEntryHistoryDatabaseRunnable (
@@ -30,7 +31,8 @@ class RestoreEntryHistoryDatabaseRunnable (
private val database: Database,
private val mainEntry: Entry,
private val entryHistoryPosition: Int,
private val saveDatabase: Boolean)
private val saveDatabase: Boolean,
private val challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionRunnable() {
private var updateEntryRunnable: UpdateEntryRunnable? = null
@@ -43,12 +45,15 @@ class RestoreEntryHistoryDatabaseRunnable (
historyToRestore.addEntryToHistory(it)
}
// Update the entry with the fresh formatted entry to restore
updateEntryRunnable = UpdateEntryRunnable(context,
updateEntryRunnable = UpdateEntryRunnable(
context,
database,
mainEntry,
historyToRestore,
saveDatabase,
null)
null,
challengeResponseRetriever
)
updateEntryRunnable?.onStartRun()

View File

@@ -22,13 +22,15 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.hardware.HardwareKey
abstract class ActionNodeDatabaseRunnable(
context: Context,
database: Database,
private val afterActionNodesFinish: AfterActionNodesFinish?,
save: Boolean)
: SaveDatabaseRunnable(context, database, save) {
save: Boolean,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, save, null, challengeResponseRetriever) {
/**
* Function do to a node action

View File

@@ -24,6 +24,7 @@ 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.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class AddEntryRunnable constructor(
context: Context,
@@ -31,8 +32,9 @@ class AddEntryRunnable constructor(
private val mNewEntry: Entry,
private val mParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() {
mNewEntry.touch(modified = true, touchParents = true)

View File

@@ -23,6 +23,7 @@ import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class AddGroupRunnable constructor(
context: Context,
@@ -30,8 +31,9 @@ class AddGroupRunnable constructor(
private val mNewGroup: Group,
private val mParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() {
mNewGroup.touch(modified = true, touchParents = true)

View File

@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context
import android.util.Log
import com.kunzisoft.keepass.database.element.*
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.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
class CopyNodesRunnable constructor(
context: Context,
@@ -33,8 +36,9 @@ class CopyNodesRunnable constructor(
private val mNodesToCopy: List<Node>,
private val mNewParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mEntriesCopied = ArrayList<Entry>()

View File

@@ -20,16 +20,20 @@
package com.kunzisoft.keepass.database.action.node
import android.content.Context
import com.kunzisoft.keepass.database.element.*
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.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey
class DeleteNodesRunnable(context: Context,
database: Database,
private val mNodesToDelete: List<Node>,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mOldParent: Group? = null
private var mCanRecycle: Boolean = false

View File

@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context
import android.util.Log
import com.kunzisoft.keepass.database.element.*
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.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
class MoveNodesRunnable constructor(
context: Context,
@@ -33,8 +36,9 @@ class MoveNodesRunnable constructor(
private val mNodesToMove: List<Node>,
private val mNewParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mOldParent: Group? = null

View File

@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateEntryRunnable constructor(
context: Context,
@@ -31,8 +32,9 @@ class UpdateEntryRunnable constructor(
private val mOldEntry: Entry,
private val mNewEntry: Entry,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() {
if (mOldEntry.nodeId == mNewEntry.nodeId) {

View File

@@ -23,6 +23,7 @@ import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateGroupRunnable constructor(
context: Context,
@@ -30,8 +31,9 @@ class UpdateGroupRunnable constructor(
private val mOldGroup: Group,
private val mNewGroup: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() {
if (mOldGroup.nodeId == mNewGroup.nodeId) {

View File

@@ -0,0 +1,34 @@
package com.kunzisoft.keepass.database.element
import com.kunzisoft.keepass.hardware.HardwareKey
data class CompositeKey(var passwordData: ByteArray? = null,
var keyFileData: ByteArray? = null,
var hardwareKey: HardwareKey? = null) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CompositeKey
if (passwordData != null) {
if (other.passwordData == null) return false
if (!passwordData.contentEquals(other.passwordData)) return false
} else if (other.passwordData != null) return false
if (keyFileData != null) {
if (other.keyFileData == null) return false
if (!keyFileData.contentEquals(other.keyFileData)) return false
} else if (other.keyFileData != null) return false
if (hardwareKey != other.hardwareKey) return false
return true
}
override fun hashCode(): Int {
var result = passwordData?.contentHashCode() ?: 0
result = 31 * result + (keyFileData?.contentHashCode() ?: 0)
result = 31 * result + (hardwareKey?.hashCode() ?: 0)
return result
}
}

View File

@@ -54,12 +54,10 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import com.kunzisoft.keepass.utils.*
import java.io.*
import java.util.*
@@ -73,7 +71,7 @@ class Database {
var fileUri: Uri? = null
private set
private var mSearchHelper: SearchHelper? = null
private var mSearchHelper: SearchHelper = SearchHelper()
var isReadOnly = false
@@ -384,10 +382,14 @@ class Database {
set(masterKey) {
mDatabaseKDB?.masterKey = masterKey
mDatabaseKDBX?.masterKey = masterKey
mDatabaseKDBX?.keyLastChanged = DateInstant()
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
val transformSeed: ByteArray?
get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed
var rootGroup: Group?
get() {
mDatabaseKDB?.rootGroup?.let {
@@ -557,79 +559,28 @@ class Database {
this.dataModifiedSinceLastLoading = false
}
@Throws(LoadDatabaseException::class)
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
openDatabaseKDB: (InputStream) -> DatabaseKDB,
openDatabaseKDBX: (InputStream) -> DatabaseKDBX) {
var databaseInputStream: InputStream? = null
try {
// Load Data, pass Uris as InputStreams
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri)
?: throw IOException("Database input stream cannot be retrieve")
databaseInputStream = BufferedInputStream(databaseStream)
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.
databaseInputStream.mark(10)
// Get the file directory to save the attachments
val sig1 = databaseInputStream.readBytes4ToUInt()
val sig2 = databaseInputStream.readBytes4ToUInt()
// Return to the start
databaseInputStream.reset()
when {
// Header of database KDB
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(openDatabaseKDB(databaseInputStream))
// Header of database KDBX
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(openDatabaseKDBX(databaseInputStream))
// Header not recognized
else -> throw SignatureDatabaseException()
}
this.mSearchHelper = SearchHelper()
loaded = true
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
} finally {
databaseInputStream?.close()
}
}
@Throws(LoadDatabaseException::class)
fun loadData(uri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
@Throws(DatabaseInputException::class)
fun loadData(
contentResolver: ContentResolver,
databaseUri: Uri,
mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
readOnly: Boolean,
cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
progressTaskUpdater: ProgressTaskUpdater?
) {
// Save database URI
this.fileUri = uri
this.fileUri = databaseUri
// Check if the file is writable
this.isReadOnly = readOnly
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
try {
// Get keyFile inputStream
mainCredential.keyFileUri?.let { keyFile ->
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
}
// Read database stream for the first time
readDatabaseStream(contentResolver, uri,
readDatabaseStream(contentResolver, databaseUri,
{ databaseInputStream ->
val databaseKDB = DatabaseKDB().apply {
binaryCache.cacheDirectory = cacheDirectory
@@ -639,12 +590,12 @@ class Database {
.openDatabase(databaseInputStream,
progressTaskUpdater
) {
databaseKDB.retrieveMasterKey(
mainCredential.masterPassword,
keyFileInputStream
databaseKDB.deriveMasterKey(
contentResolver,
mainCredential
)
}
databaseKDB
setDatabaseKDB(databaseKDB)
},
{ databaseInputStream ->
val databaseKDBX = DatabaseKDBX().apply {
@@ -655,23 +606,23 @@ class Database {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream,
progressTaskUpdater) {
databaseKDBX.retrieveMasterKey(
mainCredential.masterPassword,
keyFileInputStream,
databaseKDBX.deriveMasterKey(
contentResolver,
mainCredential,
challengeResponseRetriever
)
}
}
databaseKDBX
setDatabaseKDBX(databaseKDBX)
}
)
} catch (e: FileNotFoundException) {
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
loaded = true
} catch (e: Exception) {
throw LoadDatabaseException(e)
Log.e(TAG, "Unable to load the database")
if (e is DatabaseInputException)
throw e
throw DatabaseInputException(e)
} finally {
keyFileInputStream?.close()
dataModifiedSinceLastLoading = false
}
}
@@ -680,48 +631,44 @@ class Database {
return mDatabaseKDBX != null
}
@Throws(LoadDatabaseException::class)
fun mergeData(databaseToMergeUri: Uri?,
databaseToMergeMainCredential: MainCredential?,
@Throws(DatabaseInputException::class)
fun mergeData(
contentResolver: ContentResolver,
databaseToMergeUri: Uri?,
databaseToMergeMainCredential: MainCredential?,
databaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
progressTaskUpdater: ProgressTaskUpdater?
) {
mDatabaseKDB?.let {
throw IODatabaseException("Unable to merge from a database V1")
throw MergeDatabaseKDBException()
}
// New database instance to get new changes
val databaseToMerge = Database()
databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
try {
val databaseUri = databaseToMerge.fileUri
if (databaseUri != null) {
if (databaseToMergeMainCredential != null) {
// Get keyFile inputStream
databaseToMergeMainCredential.keyFileUri?.let { keyFile ->
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
}
}
databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
readDatabaseStream(contentResolver, databaseUri,
{ databaseInputStream ->
val databaseToMergeKDB = DatabaseKDB()
DatabaseInputKDB(databaseToMergeKDB)
.openDatabase(databaseInputStream, progressTaskUpdater) {
if (databaseToMergeMainCredential != null) {
databaseToMergeKDB.retrieveMasterKey(
databaseToMergeMainCredential.masterPassword,
keyFileInputStream,
databaseToMergeKDB.deriveMasterKey(
contentResolver,
databaseToMergeMainCredential
)
} else {
databaseToMergeKDB.masterKey = masterKey
this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
databaseToMergeKDB.copyMasterKeyFrom(thisDatabaseKDB)
}
}
databaseToMergeKDB
}
databaseToMerge.setDatabaseKDB(databaseToMergeKDB)
},
{ databaseInputStream ->
val databaseToMergeKDBX = DatabaseKDBX()
@@ -729,18 +676,22 @@ class Database {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, progressTaskUpdater) {
if (databaseToMergeMainCredential != null) {
databaseToMergeKDBX.retrieveMasterKey(
databaseToMergeMainCredential.masterPassword,
keyFileInputStream,
databaseToMergeKDBX.deriveMasterKey(
contentResolver,
databaseToMergeMainCredential,
databaseToMergeChallengeResponseRetriever
)
} else {
databaseToMergeKDBX.masterKey = masterKey
this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
databaseToMergeKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
}
}
}
databaseToMergeKDBX
}
databaseToMerge.setDatabaseKDBX(databaseToMergeKDBX)
}
)
loaded = true
mDatabaseKDBX?.let { currentDatabaseKDBX ->
val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply {
@@ -760,24 +711,24 @@ class Database {
}
}
} else {
throw IODatabaseException("Database URI is null, database cannot be merged")
throw UnknownDatabaseLocationException()
}
} catch (e: FileNotFoundException) {
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
Log.e(TAG, "Unable to merge the database")
if (e is DatabaseException)
throw e
throw DatabaseInputException(e)
} finally {
keyFileInputStream?.close()
databaseToMerge.clearAndClose()
}
}
@Throws(LoadDatabaseException::class)
fun reloadData(contentResolver: ContentResolver,
@Throws(DatabaseInputException::class)
fun reloadData(
contentResolver: ContentResolver,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
progressTaskUpdater: ProgressTaskUpdater?
) {
// Retrieve the stream from the old database URI
try {
@@ -791,9 +742,11 @@ class Database {
}
DatabaseInputKDB(databaseKDB)
.openDatabase(databaseInputStream, progressTaskUpdater) {
databaseKDB.masterKey = masterKey
this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
databaseKDB.copyMasterKeyFrom(thisDatabaseKDB)
}
databaseKDB
}
setDatabaseKDB(databaseKDB)
},
{ databaseInputStream ->
val databaseKDBX = DatabaseKDBX()
@@ -803,26 +756,144 @@ class Database {
DatabaseInputKDBX(databaseKDBX).apply {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, progressTaskUpdater) {
databaseKDBX.masterKey = masterKey
this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
databaseKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
}
}
databaseKDBX
}
setDatabaseKDBX(databaseKDBX)
}
)
loaded = true
} else {
throw IODatabaseException("Database URI is null, database cannot be reloaded")
throw UnknownDatabaseLocationException()
}
} catch (e: FileNotFoundException) {
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
Log.e(TAG, "Unable to reload the database")
if (e is DatabaseException)
throw e
throw DatabaseInputException(e)
} finally {
dataModifiedSinceLastLoading = false
}
}
@Throws(Exception::class)
private fun readDatabaseStream(contentResolver: ContentResolver,
databaseUri: Uri,
openDatabaseKDB: (InputStream) -> Unit,
openDatabaseKDBX: (InputStream) -> Unit) {
try {
// Load Data, pass Uris as InputStreams
val databaseStream = UriUtil.getUriInputStream(contentResolver, databaseUri)
?: throw UnknownDatabaseLocationException()
BufferedInputStream(databaseStream).use { databaseInputStream ->
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
databaseInputStream.mark(10)
// Get the file directory to save the attachments
val sig1 = databaseInputStream.readBytes4ToUInt()
val sig2 = databaseInputStream.readBytes4ToUInt()
// Return to the start
databaseInputStream.reset()
when {
// Header of database KDB
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> openDatabaseKDB(
databaseInputStream
)
// Header of database KDBX
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> openDatabaseKDBX(
databaseInputStream
)
// Header not recognized
else -> throw SignatureDatabaseException()
}
}
} catch (fileNotFoundException : FileNotFoundException) {
throw FileNotFoundDatabaseException()
}
}
@Throws(DatabaseOutputException::class)
fun saveData(contentResolver: ContentResolver,
cacheDir: File,
databaseCopyUri: Uri?,
mainCredential: MainCredential?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) {
val saveUri = databaseCopyUri ?: this.fileUri
// Build temp database file to avoid file corruption if error
val cacheFile = File(cacheDir, saveUri.hashCode().toString())
try {
if (saveUri != null) {
// Save in a temp memory to avoid exception
cacheFile.outputStream().use { outputStream ->
mDatabaseKDB?.let { databaseKDB ->
DatabaseOutputKDB(databaseKDB).apply {
writeDatabase(outputStream) {
if (mainCredential != null) {
databaseKDB.deriveMasterKey(
contentResolver,
mainCredential
)
} else {
// No master key change
}
}
}
}
?: mDatabaseKDBX?.let { databaseKDBX ->
DatabaseOutputKDBX(databaseKDBX).apply {
writeDatabase(outputStream) {
if (mainCredential != null) {
// Build new master key from MainCredential
databaseKDBX.deriveMasterKey(
contentResolver,
mainCredential,
challengeResponseRetriever
)
} else {
// Reuse composite key parts
databaseKDBX.deriveCompositeKey(
challengeResponseRetriever
)
}
}
}
}
}
// Copy from the cache to the final stream
UriUtil.getUriOutputStream(contentResolver, saveUri)?.use { outputStream ->
cacheFile.inputStream().use { inputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
} else {
throw UnknownDatabaseLocationException()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to save database", e)
if (e is DatabaseException)
throw e
throw DatabaseOutputException(e)
} finally {
try {
Log.d(TAG, "Delete database cache file $cacheFile")
cacheFile.delete()
} catch (e: Exception) {
Log.e(TAG, "Cache file $cacheFile cannot be deleted", e)
}
if (databaseCopyUri == null) {
this.dataModifiedSinceLastLoading = false
}
}
}
fun groupIsInRecycleBin(group: Group): Boolean {
val groupKDB = group.groupKDB
val groupKDBX = group.groupKDBX
@@ -845,13 +916,13 @@ class Database {
fun createVirtualGroupFromSearch(searchParameters: SearchParameters,
fromGroup: NodeId<*>? = null,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
return mSearchHelper.createVirtualGroupWithSearchResult(this,
searchParameters, fromGroup, max)
}
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
return mSearchHelper.createVirtualGroupWithSearchResult(this,
SearchParameters().apply {
searchQuery = searchInfoString
searchInTitles = true
@@ -908,40 +979,6 @@ class Database {
dataModifiedSinceLastLoading = true
}
@Throws(DatabaseOutputException::class)
fun saveData(databaseCopyUri: Uri?, contentResolver: ContentResolver) {
try {
val saveUri = databaseCopyUri ?: this.fileUri
if (saveUri != null) {
var outputStream: OutputStream? = null
try {
outputStream = UriUtil.getUriOutputStream(contentResolver, saveUri)
outputStream?.let { definedOutputStream ->
val databaseOutput =
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let {
DatabaseOutputKDBX(
it,
definedOutputStream
)
}
databaseOutput?.output()
}
} catch (e: Exception) {
throw IOException(e)
} finally {
outputStream?.close()
}
if (databaseCopyUri == null) {
this.dataModifiedSinceLastLoading = false
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to save database", e)
throw DatabaseOutputException(e)
}
}
fun clearIndexesAndBinaries(filesDirectory: File? = null) {
this.mDatabaseKDB?.clearIndexes()
this.mDatabaseKDBX?.clearIndexes()
@@ -987,20 +1024,13 @@ class Database {
}
fun validatePasswordEncoding(mainCredential: MainCredential): Boolean {
val password = mainCredential.masterPassword
val password = mainCredential.password
val containsKeyFile = mainCredential.keyFileUri != null
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
?: false
}
@Throws(IOException::class)
fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.keyLastChanged = DateInstant()
}
fun rootCanContainsEntry(): Boolean {
return mDatabaseKDB?.rootCanContainsEntry() ?: mDatabaseKDBX?.rootCanContainsEntry() ?: false
}

View File

@@ -0,0 +1,276 @@
/*
* Copyright 2022 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.ContentResolver
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import android.util.Base64
import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
data class MainCredential(var password: String? = null,
var keyFileUri: Uri? = null,
var hardwareKey: HardwareKey? = null): Parcelable {
constructor(parcel: Parcel) : this() {
password = parcel.readString()
keyFileUri = parcel.readParcelable(Uri::class.java.classLoader)
hardwareKey = parcel.readEnum<HardwareKey>()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(password)
parcel.writeParcelable(keyFileUri, flags)
parcel.writeEnum(hardwareKey)
}
override fun describeContents(): Int {
return 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MainCredential
if (password != other.password) return false
if (keyFileUri != other.keyFileUri) return false
if (hardwareKey != other.hardwareKey) return false
return true
}
override fun hashCode(): Int {
var result = password?.hashCode() ?: 0
result = 31 * result + (keyFileUri?.hashCode() ?: 0)
result = 31 * result + (hardwareKey?.hashCode() ?: 0)
return result
}
companion object CREATOR : Parcelable.Creator<MainCredential> {
override fun createFromParcel(parcel: Parcel): MainCredential {
return MainCredential(parcel)
}
override fun newArray(size: Int): Array<MainCredential?> {
return arrayOfNulls(size)
}
private val TAG = MainCredential::class.java.simpleName
@Throws(IOException::class)
fun retrievePasswordKey(key: String,
encoding: Charset
): ByteArray {
val bKey: ByteArray = try {
key.toByteArray(encoding)
} catch (e: UnsupportedEncodingException) {
key.toByteArray()
}
return HashManager.hashSha256(bKey)
}
@Throws(IOException::class)
fun retrieveFileKey(contentResolver: ContentResolver,
keyFileUri: Uri?,
allowXML: Boolean): ByteArray {
if (keyFileUri == null)
throw IOException("Keyfile URI is null")
val keyData = getKeyFileData(contentResolver, keyFileUri)
?: throw IOException("No data retrieved")
try {
// Check XML key file
val xmlKeyByteArray = if (allowXML)
loadXmlKeyFile(ByteArrayInputStream(keyData))
else
null
if (xmlKeyByteArray != null) {
return xmlKeyByteArray
}
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
}
}
// Hash file as binary data
return HashManager.hashSha256(keyData)
} catch (e: Exception) {
throw IOException("Unable to load the keyfile.", e)
}
}
@Throws(IOException::class)
fun retrieveHardwareKey(keyData: ByteArray): ByteArray {
return HashManager.hashSha256(keyData)
}
@Throws(Exception::class)
private fun getKeyFileData(contentResolver: ContentResolver,
keyFileUri: Uri): ByteArray? {
UriUtil.getUriInputStream(contentResolver, keyFileUri)?.use { keyFileInputStream ->
return keyFileInputStream.readBytes()
}
return null
}
private fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
try {
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
try {
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e : ParserConfigurationException) {
Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
}
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
val doc = documentBuilder.parse(keyInputStream)
var xmlKeyFileVersion = 1F
val docElement = doc.documentElement
val keyFileChildNodes = docElement.childNodes
// <KeyFile> Root node
if (docElement == null
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
return null
}
if (keyFileChildNodes.length < 2)
return null
for (keyFileChildPosition in 0 until keyFileChildNodes.length) {
val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition)
// <Meta>
if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) {
val metaChildNodes = keyFileChildNode.childNodes
for (metaChildPosition in 0 until metaChildNodes.length) {
val metaChildNode = metaChildNodes.item(metaChildPosition)
// <Version>
if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) {
val versionChildNodes = metaChildNode.childNodes
for (versionChildPosition in 0 until versionChildNodes.length) {
val versionChildNode = versionChildNodes.item(versionChildPosition)
if (versionChildNode.nodeType == Node.TEXT_NODE) {
val versionText = versionChildNode.textContent.removeSpaceChars()
try {
xmlKeyFileVersion = versionText.toFloat()
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
} catch (e: Exception) {
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
}
}
}
}
}
}
// <Key>
if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) {
val keyChildNodes = keyFileChildNode.childNodes
for (keyChildPosition in 0 until keyChildNodes.length) {
val keyChildNode = keyChildNodes.item(keyChildPosition)
// <Data>
if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) {
var hashString : String? = null
if (keyChildNode.hasAttributes()) {
val dataNodeAttributes = keyChildNode.attributes
hashString = dataNodeAttributes
.getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue
}
val dataChildNodes = keyChildNode.childNodes
for (dataChildPosition in 0 until dataChildNodes.length) {
val dataChildNode = dataChildNodes.item(dataChildPosition)
if (dataChildNode.nodeType == Node.TEXT_NODE) {
val dataString = dataChildNode.textContent.removeSpaceChars()
when (xmlKeyFileVersion) {
1F -> {
// No hash in KeyFile XML version 1
return Base64.decode(dataString,
DatabaseKDBX.BASE_64_FLAG
)
}
2F -> {
return if (hashString != null
&& checkKeyFileHash(dataString, hashString)
) {
Log.i(TAG, "Successful key file hash check.")
Hex.decodeHex(dataString.toCharArray())
} else {
Log.e(TAG, "Unable to check the hash of the key file.")
null
}
}
}
}
}
}
}
}
}
} catch (e: Exception) {
return null
}
return null
}
private fun checkKeyFileHash(data: String, hash: String): Boolean {
var success = false
try {
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray()))
.copyOfRange(0, 4).toHexString()
success = dataDigest == hash
} catch (e: Exception) {
e.printStackTrace()
}
return success
}
private const val XML_NODE_ROOT_NAME = "KeyFile"
private const val XML_NODE_META_NAME = "Meta"
private const val XML_NODE_VERSION_NAME = "Version"
private const val XML_NODE_KEY_NAME = "Key"
private const val XML_NODE_DATA_NAME = "Data"
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
}
}

View File

@@ -19,11 +19,13 @@
package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.encrypt.aes.AESTransformer
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
@@ -31,8 +33,10 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard
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.exception.EmptyKeyDatabaseException
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
import java.io.IOException
import java.io.InputStream
import java.nio.charset.Charset
import java.util.*
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
@@ -56,8 +60,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
KdfFactory.aesKdf
)
override val passwordEncoding: String
get() = "ISO-8859-1"
override val passwordEncoding: Charset
get() = Charsets.ISO_8859_1
override var numberKeyEncryptionRounds = 300L
@@ -116,20 +120,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return newId
}
@Throws(IOException::class)
override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
return if (key != null && keyInputStream != null) {
getCompositeKey(key, keyInputStream)
} else if (key != null) { // key.length() >= 0
getPasswordKey(key)
} else if (keyInputStream != null) { // key == null
getFileKey(keyInputStream)
} else {
throw IllegalArgumentException("Key cannot be empty.")
}
}
@Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) {
// Encrypt the master key a few times to make brute-force key-search harder
@@ -138,6 +128,41 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
finalKey = HashManager.hashSha256(masterSeed, transformedKey)
}
fun deriveMasterKey(
contentResolver: ContentResolver,
mainCredential: MainCredential
) {
// Exception when no password
if (mainCredential.hardwareKey != null)
throw HardwareKeyDatabaseException()
if (mainCredential.password == null && mainCredential.keyFileUri == null)
throw EmptyKeyDatabaseException()
// Retrieve plain data
val password = mainCredential.password
val keyFileUri = mainCredential.keyFileUri
val passwordBytes = if (password != null) MainCredential.retrievePasswordKey(
password,
passwordEncoding
) else null
val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey(
contentResolver,
keyFileUri,
false
) else null
// Build master key
if (passwordBytes != null
&& keyFileBytes != null) {
this.masterKey = HashManager.hashSha256(
passwordBytes,
keyFileBytes
)
} else {
this.masterKey = passwordBytes ?: keyFileBytes ?: byteArrayOf(0)
}
}
override fun createGroup(): GroupKDB {
return GroupKDB()
}

View File

@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import android.content.res.Resources
import android.util.Base64
import android.util.Log
@@ -31,10 +32,7 @@ import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -49,30 +47,27 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.longTo8Bytes
import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node
import java.io.IOException
import java.io.InputStream
import java.nio.charset.Charset
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.Mac
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import kotlin.collections.HashSet
import kotlin.math.min
class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// To resave the database with same credential when already loaded
private var mCompositeKey = CompositeKey()
var hmacKey: ByteArray? = null
private set
@@ -233,6 +228,79 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
}
fun deriveMasterKey(
contentResolver: ContentResolver,
mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
) {
// Retrieve each plain credential
val password = mainCredential.password
val keyFileUri = mainCredential.keyFileUri
val hardwareKey = mainCredential.hardwareKey
val passwordBytes = if (password != null) MainCredential.retrievePasswordKey(
password,
passwordEncoding
) else null
val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey(
contentResolver,
keyFileUri,
true
) else null
val hardwareKeyBytes = if (hardwareKey != null) MainCredential.retrieveHardwareKey(
challengeResponseRetriever.invoke(hardwareKey, transformSeed)
) else null
// Save to rebuild master password with new seed later
mCompositeKey = CompositeKey(passwordBytes, keyFileBytes, hardwareKey)
// Build the master key
this.masterKey = composedKeyToMasterKey(
passwordBytes,
keyFileBytes,
hardwareKeyBytes
)
}
@Throws(DatabaseOutputException::class)
fun deriveCompositeKey(
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
) {
val passwordBytes = mCompositeKey.passwordData
val keyFileBytes = mCompositeKey.keyFileData
val hardwareKey = mCompositeKey.hardwareKey
if (hardwareKey == null) {
// If no hardware key, simply rebuild from composed keys
this.masterKey = composedKeyToMasterKey(
passwordBytes,
keyFileBytes
)
} else {
val hardwareKeyBytes = MainCredential.retrieveHardwareKey(
challengeResponseRetriever.invoke(hardwareKey, transformSeed)
)
this.masterKey = composedKeyToMasterKey(
passwordBytes,
keyFileBytes,
hardwareKeyBytes
)
}
}
private fun composedKeyToMasterKey(passwordData: ByteArray?,
keyFileData: ByteArray?,
hardwareKeyData: ByteArray? = null): ByteArray {
return HashManager.hashSha256(
passwordData,
keyFileData,
hardwareKeyData
)
}
fun copyMasterKeyFrom(databaseVersioned: DatabaseKDBX) {
super.copyMasterKeyFrom(databaseVersioned)
this.mCompositeKey = databaseVersioned.mCompositeKey
}
fun getMinKdbxVersion(): UnsignedInt {
val entryHandler = EntryOperationHandler()
val groupHandler = GroupOperationHandler()
@@ -364,8 +432,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
kdfEngine.setParallelism(kdfParameters!!, parallelism)
}
override val passwordEncoding: String
get() = "UTF-8"
override val passwordEncoding: Charset
get() = Charsets.UTF_8
private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? {
if (groupUUID == UUID_ZERO)
@@ -528,22 +596,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return mFieldReferenceEngine.compile(textReference, recursionLevel)
}
@Throws(IOException::class)
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
var masterKey = byteArrayOf()
if (key != null && keyInputStream != null) {
return getCompositeKey(key, keyInputStream)
} else if (key != null) { // key.length() >= 0
masterKey = getPasswordKey(key)
} else if (keyInputStream != null) { // key == null
masterKey = getFileKey(keyInputStream)
}
return HashManager.hashSha256(masterKey)
}
@Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray) {
@@ -615,115 +667,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return ret
}
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
try {
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
try {
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e : ParserConfigurationException) {
Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
}
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
val doc = documentBuilder.parse(keyInputStream)
var xmlKeyFileVersion = 1F
val docElement = doc.documentElement
val keyFileChildNodes = docElement.childNodes
// <KeyFile> Root node
if (docElement == null
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
return null
}
if (keyFileChildNodes.length < 2)
return null
for (keyFileChildPosition in 0 until keyFileChildNodes.length) {
val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition)
// <Meta>
if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) {
val metaChildNodes = keyFileChildNode.childNodes
for (metaChildPosition in 0 until metaChildNodes.length) {
val metaChildNode = metaChildNodes.item(metaChildPosition)
// <Version>
if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) {
val versionChildNodes = metaChildNode.childNodes
for (versionChildPosition in 0 until versionChildNodes.length) {
val versionChildNode = versionChildNodes.item(versionChildPosition)
if (versionChildNode.nodeType == Node.TEXT_NODE) {
val versionText = versionChildNode.textContent.removeSpaceChars()
try {
xmlKeyFileVersion = versionText.toFloat()
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
} catch (e: Exception) {
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
}
}
}
}
}
}
// <Key>
if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) {
val keyChildNodes = keyFileChildNode.childNodes
for (keyChildPosition in 0 until keyChildNodes.length) {
val keyChildNode = keyChildNodes.item(keyChildPosition)
// <Data>
if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) {
var hashString : String? = null
if (keyChildNode.hasAttributes()) {
val dataNodeAttributes = keyChildNode.attributes
hashString = dataNodeAttributes
.getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue
}
val dataChildNodes = keyChildNode.childNodes
for (dataChildPosition in 0 until dataChildNodes.length) {
val dataChildNode = dataChildNodes.item(dataChildPosition)
if (dataChildNode.nodeType == Node.TEXT_NODE) {
val dataString = dataChildNode.textContent.removeSpaceChars()
when (xmlKeyFileVersion) {
1F -> {
// No hash in KeyFile XML version 1
return Base64.decode(dataString, BASE_64_FLAG)
}
2F -> {
return if (hashString != null
&& checkKeyFileHash(dataString, hashString)) {
Log.i(TAG, "Successful key file hash check.")
Hex.decodeHex(dataString.toCharArray())
} else {
Log.e(TAG, "Unable to check the hash of the key file.")
null
}
}
}
}
}
}
}
}
}
} catch (e: Exception) {
return null
}
return null
}
private fun checkKeyFileHash(data: String, hash: String): Boolean {
var success = false
try {
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray()))
.copyOfRange(0, 4).toHexString()
success = dataDigest == hash
} catch (e: Exception) {
e.printStackTrace()
}
return success
}
override fun newGroupId(): NodeIdUUID {
var newId: NodeIdUUID
do {
@@ -928,13 +871,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited
private const val XML_NODE_ROOT_NAME = "KeyFile"
private const val XML_NODE_META_NAME = "Meta"
private const val XML_NODE_VERSION_NAME = "Version"
private const val XML_NODE_KEY_NAME = "Key"
private const val XML_NODE_DATA_NAME = "Data"
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
const val BASE_64_FLAG = Base64.NO_WRAP
}
}

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.database.element.database
import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
@@ -32,11 +31,9 @@ import com.kunzisoft.keepass.database.element.icon.IconsManager
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import org.apache.commons.codec.binary.Hex
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.util.*
abstract class DatabaseVersioned<
@@ -46,7 +43,6 @@ abstract class DatabaseVersioned<
Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
> {
// Algorithm used to encrypt the database
abstract var encryptionAlgorithm: EncryptionAlgorithm
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
@@ -55,11 +51,12 @@ abstract class DatabaseVersioned<
abstract val kdfAvailableList: List<KdfEngine>
abstract var numberKeyEncryptionRounds: Long
protected abstract val passwordEncoding: String
abstract val passwordEncoding: Charset
var masterKey = ByteArray(32)
var finalKey: ByteArray? = null
protected set
var transformSeed: ByteArray? = null
abstract val version: String
abstract val defaultFileExtension: String
@@ -91,58 +88,6 @@ abstract class DatabaseVersioned<
return getGroupIndexes().filter { it != rootGroup }
}
@Throws(IOException::class)
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@Throws(IOException::class)
fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) {
masterKey = getMasterKey(key, keyfileInputStream)
}
@Throws(IOException::class)
protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
val fileKey = getFileKey(keyfileInputStream)
val passwordKey = getPasswordKey(key)
return HashManager.hashSha256(passwordKey, fileKey)
}
@Throws(IOException::class)
protected fun getPasswordKey(key: String): ByteArray {
val bKey: ByteArray = try {
key.toByteArray(charset(passwordEncoding))
} catch (e: UnsupportedEncodingException) {
key.toByteArray()
}
return HashManager.hashSha256(bKey)
}
@Throws(IOException::class)
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
try {
val keyData = keyInputStream.readBytes()
// Check XML key file
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
if (xmlKeyByteArray != null) {
return xmlKeyByteArray
}
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
}
}
// Hash file as binary data
return HashManager.hashSha256(keyData)
} catch (outOfMemoryError: OutOfMemoryError) {
throw IOException("Keyfile data is too large", outOfMemoryError)
}
}
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
return null
}
@@ -158,20 +103,25 @@ abstract class DatabaseVersioned<
val bKey: ByteArray
try {
bKey = password.toByteArray(charset(encoding))
bKey = password.toByteArray(encoding)
} catch (e: UnsupportedEncodingException) {
return false
}
val reEncoded: String
try {
reEncoded = String(bKey, charset(encoding))
reEncoded = String(bKey, encoding)
} catch (e: UnsupportedEncodingException) {
return false
}
return password == reEncoded
}
fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned<GroupId, EntryId, Group, Entry>) {
this.masterKey = databaseVersioned.masterKey
this.transformSeed = databaseVersioned.transformSeed
}
/*
* -------------------------------------
* Node Creation

View File

@@ -24,128 +24,172 @@ import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import java.io.PrintStream
import java.io.PrintWriter
abstract class DatabaseException : Exception {
var innerMessage: String? = null
abstract var errorId: Int
var parameters: (Array<out String>)? = null
var mThrowable: Throwable? = null
constructor() : super()
constructor(message: String) : super(message)
constructor(message: String, throwable: Throwable) : super(message, throwable)
constructor(throwable: Throwable) : super(throwable)
constructor(message: String, throwable: Throwable) {
mThrowable = throwable
innerMessage = StringBuilder().apply {
append(message)
if (throwable.localizedMessage != null) {
append(" ")
append(throwable.localizedMessage)
}
}.toString()
}
constructor(throwable: Throwable) {
mThrowable = throwable
innerMessage = throwable.localizedMessage
}
fun getLocalizedMessage(resources: Resources): String {
parameters?.let {
return resources.getString(errorId, *it)
} ?: return resources.getString(errorId)
val throwable = mThrowable
if (throwable is DatabaseException)
errorId = throwable.errorId
val localMessage = parameters?.let {
resources.getString(errorId, *it)
} ?: resources.getString(errorId)
return StringBuilder().apply {
append(localMessage)
if (innerMessage != null) {
append(" ")
append(innerMessage)
}
}.toString()
}
override fun printStackTrace() {
mThrowable?.printStackTrace()
super.printStackTrace()
}
override fun printStackTrace(s: PrintStream) {
mThrowable?.printStackTrace(s)
super.printStackTrace(s)
}
override fun printStackTrace(s: PrintWriter) {
mThrowable?.printStackTrace(s)
super.printStackTrace(s)
}
}
open class LoadDatabaseException : DatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database
constructor() : super()
constructor(string: String) : super(string)
constructor(throwable: Throwable) : super(throwable)
}
class FileNotFoundDatabaseException : LoadDatabaseException {
class FileNotFoundDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.file_not_found_content
constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception)
}
class InvalidAlgorithmDatabaseException : LoadDatabaseException {
class CorruptedDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.corrupted_file
}
class InvalidAlgorithmDatabaseException : DatabaseInputException {
@StringRes
override var errorId: Int = R.string.invalid_algorithm
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class DuplicateUuidDatabaseException: LoadDatabaseException {
class UnknownDatabaseLocationException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.invalid_db_same_uuid
constructor(type: Type, uuid: NodeId<*>) : super() {
parameters = arrayOf(type.name, uuid.toString())
}
constructor(exception: Throwable) : super(exception)
override var errorId: Int = R.string.error_location_unknown
}
class IODatabaseException : LoadDatabaseException {
class HardwareKeyDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_hardware_key_unsupported
}
class EmptyKeyDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_empty_key
}
class SignatureDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.invalid_db_sig
}
class VersionDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.unsupported_db_version
}
class InvalidCredentialsDatabaseException : DatabaseInputException {
@StringRes
override var errorId: Int = R.string.invalid_credentials
constructor() : super()
constructor(string: String) : super(string)
}
class KDFMemoryDatabaseException(exception: Throwable) : DatabaseInputException(exception) {
@StringRes
override var errorId: Int = R.string.error_load_database_KDF_memory
}
class NoMemoryDatabaseException(exception: Throwable) : DatabaseInputException(exception) {
@StringRes
override var errorId: Int = R.string.error_out_of_memory
}
class DuplicateUuidDatabaseException(type: Type, uuid: NodeId<*>) : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.invalid_db_same_uuid
init {
parameters = arrayOf(type.name, uuid.toString())
}
}
class XMLMalformedDatabaseException : DatabaseInputException {
@StringRes
override var errorId: Int = R.string.error_XML_malformed
constructor() : super()
constructor(string: String) : super(string)
}
class MergeDatabaseKDBException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.error_unable_merge_database_kdb
}
class MoveEntryDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_move_entry_here
}
class MoveGroupDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_move_group_here
}
class CopyEntryDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_copy_entry_here
}
class CopyGroupDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_copy_group_here
}
open class DatabaseInputException : DatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database
constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception)
constructor(throwable: Throwable) : super(throwable)
}
class KDFMemoryDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database_KDF_memory
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class SignatureDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.invalid_db_sig
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class VersionDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.unsupported_db_version
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class InvalidCredentialsDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.invalid_credentials
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class NoMemoryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_out_of_memory
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class MoveEntryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_move_entry_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class MoveGroupDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_move_group_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class CopyEntryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_copy_entry_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class CopyGroupDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_copy_group_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
// TODO Output Exception
open class DatabaseOutputException : DatabaseException {
@StringRes
override var errorId: Int = R.string.error_save_database

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.file.input
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.InputStream
@@ -33,15 +33,9 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var m
/**
* Load a versioned database file, return contents in a new DatabaseVersioned.
*
* @param databaseInputStream Existing file to load.
* @param password Pass phrase for infile.
* @return new DatabaseVersioned container.
*
* @throws LoadDatabaseException on database error (contains IO exceptions)
*/
@Throws(LoadDatabaseException::class)
@Throws(DatabaseInputException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)): D

View File

@@ -47,7 +47,7 @@ import javax.crypto.CipherInputStream
class DatabaseInputKDB(database: DatabaseKDB)
: DatabaseInput<DatabaseKDB>(database) {
@Throws(LoadDatabaseException::class)
@Throws(DatabaseInputException::class)
override fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)): DatabaseKDB {
@@ -76,6 +76,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
throw VersionDatabaseException()
}
mDatabase.transformSeed = header.transformSeed
assignMasterKey.invoke()
// Select algorithm
@@ -310,18 +311,11 @@ class DatabaseInputKDB(database: DatabaseKDB)
stopContentTimer()
} catch (e: LoadDatabaseException) {
mDatabase.clearAll()
throw e
} catch (e: IOException) {
mDatabase.clearAll()
throw IODatabaseException(e)
} catch (e: OutOfMemoryError) {
} catch (e: Error) {
mDatabase.clearAll()
if (e is OutOfMemoryError)
throw NoMemoryDatabaseException(e)
} catch (e: Exception) {
mDatabase.clearAll()
throw LoadDatabaseException(e)
throw DatabaseInputException(e)
}
return mDatabase

View File

@@ -99,7 +99,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
this.isRAMSufficient = method
}
@Throws(LoadDatabaseException::class)
@Throws(DatabaseInputException::class)
override fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)): DatabaseKDBX {
@@ -114,6 +114,8 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
hashOfHeader = headerAndHash.hash
val pbHeader = headerAndHash.header
val transformSeed = header.transformSeed
mDatabase.transformSeed = transformSeed
assignMasterKey.invoke()
mDatabase.makeFinalKey(header.masterSeed)
@@ -155,7 +157,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
throw InvalidCredentialsDatabaseException()
}
val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
val hmacKey = mDatabase.hmacKey ?: throw DatabaseInputException()
val blockKey = HmacBlock.getHmacKey64(hmacKey, UnsignedLong.MAX_BYTES)
val hmac: Mac = HmacBlock.getHmacSha256(blockKey)
@@ -187,7 +189,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
try {
randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey)
} catch (e: Exception) {
throw LoadDatabaseException(e)
throw DatabaseInputException(e)
}
val xmlPullParserFactory = XmlPullParserFactory.newInstance().apply {
@@ -200,19 +202,12 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
stopContentTimer()
} catch (e: LoadDatabaseException) {
throw e
} catch (e: XmlPullParserException) {
throw IODatabaseException(e)
} catch (e: IOException) {
} catch (e: Error) {
if (e is OutOfMemoryError)
throw NoMemoryDatabaseException(e)
if (e.message?.contains("Hash failed with code") == true)
throw KDFMemoryDatabaseException(e)
else
throw IODatabaseException(e)
} catch (e: OutOfMemoryError) {
throw NoMemoryDatabaseException(e)
} catch (e: Exception) {
throw LoadDatabaseException(e)
throw DatabaseInputException(e)
}
return mDatabase
@@ -227,7 +222,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
val fieldId = dataInputStream.read().toByte()
val size = dataInputStream.readBytes4ToUInt().toKotlinInt()
if (size < 0) throw IOException("Corrupted file")
if (size < 0) throw CorruptedDatabaseException()
var data = ByteArray(0)
try {
@@ -238,7 +233,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
}
} catch (e: Exception) {
// OOM only if corrupted file
throw IOException("Corrupted file")
throw CorruptedDatabaseException()
}
readStream = true
@@ -297,7 +292,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
Binaries
}
@Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
@Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
private fun readDocumentStreamed(xpp: XmlPullParser) {
ctxGroups.clear()
@@ -324,11 +319,11 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
}
// Error checks
if (ctx != KdbContext.Null) throw IOException("Malformed")
if (ctxGroups.size != 0) throw IOException("Malformed")
if (ctx != KdbContext.Null) throw XMLMalformedDatabaseException()
if (ctxGroups.size != 0) throw XMLMalformedDatabaseException()
}
@Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
@Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext {
val name = xpp.name
when (ctx) {
@@ -352,7 +347,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
if (encodedHash.isNotEmpty() && hashOfHeader != null) {
val hash = Base64.decode(encodedHash, BASE_64_FLAG)
if (!Arrays.equals(hash, hashOfHeader)) {
throw LoadDatabaseException()
throw DatabaseInputException()
}
}
} else if (name.equals(DatabaseKDBXXML.ElemSettingsChanged, ignoreCase = true)) {
@@ -824,7 +819,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
if (ctx != null) {
contextName = ctx.name
}
throw RuntimeException("Invalid end element: Context " + contextName + "End element: " + name)
throw XMLMalformedDatabaseException("Invalid end element: Context " + contextName + "End element: " + name)
}
}

View File

@@ -26,7 +26,7 @@ import java.io.OutputStream
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(protected var mOutputStream: OutputStream) {
abstract class DatabaseOutput<Header : DatabaseHeader> {
@Throws(DatabaseOutputException::class)
protected open fun setIVs(header: Header): SecureRandom {
@@ -44,9 +44,7 @@ abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(pro
}
@Throws(DatabaseOutputException::class)
abstract fun output()
@Throws(DatabaseOutputException::class)
abstract fun outputHeader(outputStream: OutputStream): Header
abstract fun writeDatabase(outputStream: OutputStream,
assignMasterKey: () -> Unit)
}

View File

@@ -39,9 +39,8 @@ import java.security.*
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
outputStream: OutputStream)
: DatabaseOutput<DatabaseHeaderKDB>(outputStream) {
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB)
: DatabaseOutput<DatabaseHeaderKDB>() {
private var headerHashBlock: ByteArray? = null
@@ -60,15 +59,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
@Throws(DatabaseOutputException::class)
override fun output() {
override fun writeDatabase(outputStream: OutputStream,
assignMasterKey: () -> Unit) {
// Before we output the header, we should sort our list of groups
// and remove any orphaned nodes that are no longer part of the tree hierarchy
// also remove the virtual root not present in kdb
val rootGroup = mDatabaseKDB.rootGroup
sortNodesForOutput()
val header = outputHeader(mOutputStream)
val header = outputHeader(outputStream, assignMasterKey)
val finalKey = getFinalKey(header)
val cipher: Cipher = try {
@@ -81,7 +80,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
try {
val cos = CipherOutputStream(mOutputStream, cipher)
val cos = CipherOutputStream(outputStream, cipher)
val bos = BufferedOutputStream(cos)
outputPlanGroupAndEntries(bos)
bos.flush()
@@ -107,7 +106,8 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
@Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
private fun outputHeader(outputStream: OutputStream,
assignMasterKey: () -> Unit): DatabaseHeaderKDB {
// Build header
val header = DatabaseHeaderKDB()
header.signature1 = DatabaseHeaderKDB.DBSIG_1
@@ -132,6 +132,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
setIVs(header)
mDatabaseKDB.transformSeed = header.transformSeed
assignMasterKey()
// Header checksum
val headerDigest: MessageDigest = HashManager.getHash256()

View File

@@ -56,9 +56,8 @@ import javax.crypto.CipherOutputStream
import kotlin.experimental.or
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
outputStream: OutputStream)
: DatabaseOutput<DatabaseHeaderKDBX>(outputStream) {
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX)
: DatabaseOutput<DatabaseHeaderKDBX>() {
private var randomStream: StreamCipher? = null
private lateinit var xml: XmlSerializer
@@ -67,43 +66,34 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private var headerHmac: ByteArray? = null
@Throws(DatabaseOutputException::class)
override fun output() {
override fun writeDatabase(outputStream: OutputStream,
assignMasterKey: () -> Unit) {
try {
header = outputHeader(mOutputStream)
header = outputHeader(outputStream, assignMasterKey)
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
val cos = attachStreamEncryptor(header!!, mOutputStream)
val cos = attachStreamEncryptor(header!!, outputStream)
cos.write(header!!.streamStartBytes)
HashedBlockOutputStream(cos)
} else {
mOutputStream.write(hashOfHeader!!)
mOutputStream.write(headerHmac!!)
outputStream.write(hashOfHeader!!)
outputStream.write(headerHmac!!)
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!))
attachStreamEncryptor(header!!, HmacBlockOutputStream(outputStream, mDatabaseKDBX.hmacKey!!))
}
val xmlOutputStream: OutputStream
try {
xmlOutputStream = when(mDatabaseKDBX.compressionAlgorithm) {
when(mDatabaseKDBX.compressionAlgorithm) {
CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
else -> osPlain
}
}.use { xmlOutputStream ->
if (!header!!.version.isBefore(FILE_VERSION_40)) {
outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream)
}
outputDatabase(xmlOutputStream)
xmlOutputStream.close()
} catch (e: IllegalArgumentException) {
throw DatabaseOutputException(e)
} catch (e: IllegalStateException) {
throw DatabaseOutputException(e)
}
} catch (e: IOException) {
} catch (e: Exception) {
throw DatabaseOutputException(e)
}
}
@@ -322,11 +312,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
@Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDBX {
private fun outputHeader(outputStream: OutputStream,
assignMasterKey: () -> Unit): DatabaseHeaderKDBX {
try {
val header = DatabaseHeaderKDBX(mDatabaseKDBX)
setIVs(header)
mDatabaseKDBX.transformSeed = header.transformSeed
assignMasterKey.invoke()
val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream)
pho.output()

View File

@@ -0,0 +1,35 @@
package com.kunzisoft.keepass.hardware
enum class HardwareKey(val value: String) {
FIDO2_SECRET("FIDO2 secret"),
CHALLENGE_RESPONSE_YUBIKEY("Yubikey challenge-response");
override fun toString(): String {
return value
}
companion object {
val DEFAULT = FIDO2_SECRET
fun getStringValues(): List<String> {
return values().map { it.value }
}
fun fromPosition(position: Int): HardwareKey {
return when (position) {
0 -> FIDO2_SECRET
1 -> CHALLENGE_RESPONSE_YUBIKEY
else -> DEFAULT
}
}
fun getHardwareKeyFromString(text: String?): HardwareKey? {
if (text == null)
return null
values().find { it.value == text }?.let {
return it
}
return DEFAULT
}
}
}

View File

@@ -0,0 +1,141 @@
package com.kunzisoft.keepass.hardware
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.launch
class HardwareKeyResponseHelper {
private var activity: FragmentActivity? = null
private var fragment: Fragment? = null
private var getChallengeResponseResultLauncher: ActivityResultLauncher<Intent>? = null
constructor(context: FragmentActivity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?,
extra: Bundle?) -> Unit) {
val resultCallback = ActivityResultCallback<ActivityResult> { result ->
if (result.resultCode == Activity.RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
onChallengeResponded.invoke(challengeResponse,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
} else {
Log.e(TAG, "Response from challenge error")
onChallengeResponded.invoke(null,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
}
}
getChallengeResponseResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
} else {
activity?.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
}
}
fun launchChallengeForResponse(hardwareKey: HardwareKey, seed: ByteArray?) {
when (hardwareKey) {
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
// Send to the driver
getChallengeResponseResultLauncher!!.launch(
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
)
Log.d(TAG, "Challenge sent")
}
}
}
companion object {
private val TAG = HardwareKeyResponseHelper::class.java.simpleName
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
private const val EXTRA_BUNDLE_KEY = "EXTRA_BUNDLE_KEY"
fun isHardwareKeyAvailable(
activity: FragmentActivity,
hardwareKey: HardwareKey,
showDialog: Boolean = true
): Boolean {
return when (hardwareKey) {
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
if (showDialog)
UnderDevelopmentFeatureDialogFragment()
.show(activity.supportFragmentManager, "underDevFeatureDialog")
false
}
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
val yubikeyDriverAvailable =
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(activity.packageManager) != null
if (showDialog && !yubikeyDriverAvailable)
showHardwareKeyDriverNeeded(activity, hardwareKey)
yubikeyDriverAvailable
}
}
}
private fun showHardwareKeyDriverNeeded(
activity: FragmentActivity,
hardwareKey: HardwareKey
) {
activity.lifecycleScope.launch {
val builder = AlertDialog.Builder(activity)
builder
.setMessage(
activity.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
UriUtil.gotoUrl(activity, activity.getString(R.string.key_driver_url))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
}
}

View File

@@ -41,11 +41,6 @@ class CipherEncryptDatabase(): Parcelable {
parcel.readByteArray(specParameters)
}
fun replaceContent(copy: CipherEncryptDatabase) {
this.encryptedValue = copy.encryptedValue
this.specParameters = copy.specParameters
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(databaseUri, flags)
parcel.writeEnum(credentialStorage)

View File

@@ -1,9 +1,11 @@
package com.kunzisoft.keepass.model
import android.net.Uri
import com.kunzisoft.keepass.hardware.HardwareKey
data class DatabaseFile(var databaseUri: Uri? = null,
var keyFileUri: Uri? = null,
var hardwareKey: HardwareKey? = null,
var databaseDecodedPath: String? = null,
var databaseAlias: String? = null,
var databaseFileExists: Boolean = false,

View File

@@ -1,32 +0,0 @@
package com.kunzisoft.keepass.model
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
data class MainCredential(var masterPassword: String? = null, var keyFileUri: Uri? = null): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readParcelable(Uri::class.java.classLoader)) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(masterPassword)
parcel.writeParcelable(keyFileUri, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<MainCredential> {
override fun createFromParcel(parcel: Parcel): MainCredential {
return MainCredential(parcel)
}
override fun newArray(size: Int): Array<MainCredential?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,13 @@
package com.kunzisoft.keepass.model
import androidx.annotation.StringRes
data class ProgressMessage(
@StringRes
var titleId: Int,
@StringRes
var messageId: Int? = null,
@StringRes
var warningId: Int? = null,
var cancelable: (() -> Unit)? = null
)

View File

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

View File

@@ -27,6 +27,7 @@ import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.annotation.StringRes
import androidx.media.app.NotificationCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity
@@ -37,12 +38,14 @@ import com.kunzisoft.keepass.database.action.node.*
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.MainCredential
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
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.hardware.HardwareKey
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.ProgressMessage
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -53,6 +56,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.util.*
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
@@ -61,20 +65,25 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private var mDatabase: Database? = null
// File description
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
private var mLastLocalSaveTime: Long = 0
private val mainScope = CoroutineScope(Dispatchers.Main)
private var mDatabaseListeners = LinkedList<DatabaseListener>()
private var mDatabaseInfoListeners = LinkedList<DatabaseInfoListener>()
private var mDatabaseListeners = mutableListOf<DatabaseListener>()
private var mDatabaseInfoListeners = mutableListOf<DatabaseInfoListener>()
private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
private var mActionTaskListeners = mutableListOf<ActionTaskListener>()
// Channel to connect asynchronously a listener or a response
private var mRequestChallengeListenerChannel: Channel<RequestChallengeListener>? = null
private var mResponseChallengeChannel: Channel<ByteArray?>? = null
private var mActionRunning = false
private var mTaskRemovedRequested = false
private var mCreationState = false
private var mSaveState = false
private var mIconId: Int = R.drawable.notification_ic_database_load
private var mTitleId: Int = R.string.database_opened
private var mMessageId: Int? = null
private var mWarningId: Int? = null
private var mProgressMessage: ProgressMessage = ProgressMessage(R.string.database_opened)
override fun retrieveChannelId(): String {
return CHANNEL_DATABASE_ID
@@ -114,6 +123,25 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
mActionTaskListeners.remove(actionTaskListener)
}
fun addRequestChallengeListener(requestChallengeListener: RequestChallengeListener) {
mainScope.launch {
val requestChannel = mRequestChallengeListenerChannel
if (requestChannel == null || requestChannel.isEmpty) {
initializeChallengeResponse()
mRequestChallengeListenerChannel?.send(requestChallengeListener)
} else {
cancelChallengeResponse(R.string.error_challenge_already_requested)
}
}
}
fun removeRequestChallengeListener() {
mainScope.launch {
mRequestChallengeListenerChannel?.cancel()
mRequestChallengeListenerChannel = null
}
}
}
interface DatabaseListener {
@@ -126,9 +154,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
interface ActionTaskListener {
fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?)
fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?)
fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result)
fun onStartAction(database: Database,
progressMessage: ProgressMessage)
fun onUpdateAction(database: Database,
progressMessage: ProgressMessage)
fun onStopAction(database: Database,
actionTask: String,
result: ActionRunnable.Result)
}
interface RequestChallengeListener {
fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?)
}
fun checkDatabase() {
@@ -165,7 +201,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
Log.i(TAG, "Database file modified " +
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
// Call listener to indicate a change in database info
if (!mCreationState && previousDatabaseInfo != null) {
if (!mSaveState && previousDatabaseInfo != null) {
mDatabaseInfoListeners.forEach { listener ->
listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo)
}
@@ -197,12 +233,53 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mDatabase?.let { database ->
if (mActionRunning) {
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId)
actionTaskListener.onStartAction(
database, mProgressMessage
)
}
}
}
}
private fun initializeChallengeResponse() {
// Init the channels
if (mRequestChallengeListenerChannel == null) {
mRequestChallengeListenerChannel = Channel(0)
}
if (mResponseChallengeChannel == null) {
mResponseChallengeChannel = Channel(0)
}
}
private fun closeChallengeResponse() {
mRequestChallengeListenerChannel?.close()
mResponseChallengeChannel?.close()
mRequestChallengeListenerChannel = null
mResponseChallengeChannel = null
}
private fun cancelChallengeResponse(@StringRes error: Int) {
mRequestChallengeListenerChannel?.cancel(CancellationException(getString(error)))
mRequestChallengeListenerChannel = null
mResponseChallengeChannel?.cancel(CancellationException(getString(error)))
mResponseChallengeChannel = null
}
fun respondToChallenge(response: ByteArray) {
mainScope.launch {
val responseChannel = mResponseChallengeChannel
if (responseChannel == null || responseChannel.isEmpty) {
if (response.isEmpty()) {
cancelChallengeResponse(R.string.error_no_response_from_challenge)
} else {
mResponseChallengeChannel?.send(response)
}
} else {
cancelChallengeResponse(R.string.error_response_already_provided)
}
}
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return mActionTaskBinder
@@ -219,8 +296,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
// Get save state
mSaveState = if (intent != null) {
if (intent.hasExtra(SAVE_DATABASE_KEY)) {
!database.isReadOnly && intent.getBooleanExtra(
SAVE_DATABASE_KEY,
mSaveState
)
} else (intent.action == ACTION_DATABASE_CREATE_TASK
|| intent.action == ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|| intent.action == ACTION_DATABASE_SAVE)
} else false
// Create the notification
buildMessage(intent, database.isReadOnly)
buildNotification(intent)
val intentAction = intent?.action
@@ -272,13 +361,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mActionRunning = true
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
putExtra(DATABASE_TASK_TITLE_KEY, mTitleId)
putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId)
putExtra(DATABASE_TASK_WARNING_KEY, mWarningId)
putExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId)
putExtra(DATABASE_TASK_MESSAGE_KEY, mProgressMessage.messageId)
putExtra(DATABASE_TASK_WARNING_KEY, mProgressMessage.warningId)
})
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId)
actionTaskListener.onStartAction(
database, mProgressMessage
)
}
},
@@ -353,61 +444,51 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
private fun buildMessage(intent: Intent?, readOnly: Boolean) {
private fun buildNotification(intent: Intent?) {
// Assign elements for updates
val intentAction = intent?.action
var saveAction = false
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) {
saveAction = !readOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
}
mIconId = if (intentAction == null)
// Get icon depending action state
val iconId = if (intentAction == null)
R.drawable.notification_ic_database_open
else
R.drawable.notification_ic_database_load
R.drawable.notification_ic_database_action
mTitleId = when {
saveAction -> {
R.string.saving_database
}
intentAction == null -> {
// Title depending on action
mProgressMessage.titleId =
if (intentAction == null) {
R.string.database_opened
}
else -> {
when (intentAction) {
} else when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
ACTION_DATABASE_ASSIGN_PASSWORD_TASK,
ACTION_DATABASE_SAVE -> R.string.saving_database
else -> {
if (mSaveState)
R.string.saving_database
else
R.string.command_execution
}
}
}
}
mMessageId = when (intentAction) {
ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> null
else -> null
}
// Updated later
mProgressMessage.messageId = null
mWarningId =
if (!saveAction
|| intentAction == ACTION_DATABASE_LOAD_TASK
|| intentAction == ACTION_DATABASE_MERGE_TASK
|| intentAction == ACTION_DATABASE_RELOAD_TASK)
null
else
// Warning if data is saved
mProgressMessage.warningId =
if (mSaveState)
R.string.do_not_kill_app
else
null
val notificationBuilder = buildNewNotification().apply {
setSmallIcon(mIconId)
setSmallIcon(iconId)
intent?.let {
setContentTitle(getString(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mTitleId)))
setContentTitle(getString(
intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId))
)
}
setAutoCancel(false)
setContentIntent(null)
@@ -513,15 +594,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
override fun updateMessage(resId: Int) {
mMessageId = resId
private fun notifyProgressMessage() {
mDatabase?.let { database ->
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onUpdateAction(database, mTitleId, mMessageId, mWarningId)
actionTaskListener.onUpdateAction(
database, mProgressMessage
)
}
}
}
override fun updateMessage(resId: Int) {
mProgressMessage.messageId = resId
notifyProgressMessage()
}
override fun actionOnLock() {
if (!TimeoutHelper.temporarilyDisableLock) {
closeDatabase(mDatabase)
@@ -539,6 +626,39 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
super.onTaskRemoved(rootIntent)
}
private fun retrieveResponseFromChallenge(hardwareKey: HardwareKey,
seed: ByteArray?): ByteArray {
// Request a challenge - response
var response: ByteArray
runBlocking {
// Initialize the channels
initializeChallengeResponse()
val previousMessage = mProgressMessage.copy()
mProgressMessage.apply {
messageId = R.string.waiting_challenge_request
cancelable = {
cancelChallengeResponse(R.string.error_cancel_by_user)
}
}
// Send the request
notifyProgressMessage()
val challengeResponseRequestListener = mRequestChallengeListenerChannel?.receive()
challengeResponseRequestListener?.onChallengeResponseRequested(hardwareKey, seed)
// Wait the response
mProgressMessage.apply {
messageId = R.string.waiting_challenge_response
}
notifyProgressMessage()
response = mResponseChallengeChannel?.receive() ?: byteArrayOf()
// Close channels
closeChallengeResponse()
// Restore previous message
mProgressMessage = previousMessage
notifyProgressMessage()
}
return response
}
private fun buildDatabaseCreateActionTask(intent: Intent, database: Database): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY)
@@ -550,15 +670,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (databaseUri == null)
return null
mCreationState = true
return CreateDatabaseRunnable(this,
database,
databaseUri,
getString(R.string.database_default_name),
getString(R.string.database),
getString(R.string.template_group_name),
mainCredential
mainCredential,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
) { result ->
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
@@ -586,13 +707,14 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (databaseUri == null)
return null
mCreationState = false
return LoadDatabaseRunnable(
this,
database,
databaseUri,
mainCredential,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
readOnly,
cipherEncryptDatabase,
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
@@ -626,6 +748,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database,
databaseToMergeUri,
databaseToMergeMainCredential,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
this
) { result ->
// No need to add each info to reload database
@@ -653,7 +778,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database,
databaseUri,
intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
)
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} else {
null
}
@@ -687,7 +814,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
newGroup,
parent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -712,7 +842,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
oldGroup,
newGroup,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -737,7 +870,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
newEntry,
parent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -762,7 +898,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
oldEntry,
newEntry,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -783,7 +922,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
getListNodesFromBundle(database, intent.extras!!),
newParent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -804,7 +946,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
getListNodesFromBundle(database, intent.extras!!),
newParent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -820,7 +965,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database,
getListNodesFromBundle(database, intent.extras!!),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} else {
null
}
@@ -838,7 +986,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database,
mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -857,7 +1008,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database,
mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -881,7 +1035,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
oldElement,
newElement,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply {
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}.apply {
mAfterSaveDatabase = { result ->
result.data = intent.extras
}
@@ -897,7 +1053,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return RemoveUnlinkedDataDatabaseRunnable(this,
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply {
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}.apply {
mAfterSaveDatabase = { result ->
result.data = intent.extras
}
@@ -911,7 +1069,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
return SaveDatabaseRunnable(this,
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
null,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
).apply {
mAfterSaveDatabase = { result ->
result.data = intent.extras
@@ -936,6 +1098,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
SaveDatabaseRunnable(this,
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
null,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
databaseCopyUri)
} else {
null
@@ -1002,9 +1168,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time
const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
private var mLastLocalSaveTime: Long = 0
fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> {
val nodesAction = ArrayList<Node>()
bundle.getParcelableArrayList<NodeId<*>>(GROUPS_ID_KEY)?.forEach {

View File

@@ -96,6 +96,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.remember_keyfile_locations_default))
}
fun rememberHardwareKey(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.remember_hardware_key_key),
context.resources.getBoolean(R.bool.remember_hardware_key_default))
}
fun automaticallyFocusSearch(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.auto_focus_search_key),
@@ -479,6 +485,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.enable_keep_screen_on_default))
}
fun isScreenshotModeEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_screenshot_mode_key),
context.resources.getBoolean(R.bool.enable_screenshot_mode_key_default))
}
fun isAdvancedUnlockEnable(context: Context): Boolean {
return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context)
}

View File

@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionErrorIfNeeded

View File

@@ -24,15 +24,18 @@ import android.app.Dialog
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import java.lang.Exception
import kotlinx.coroutines.launch
open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
open class ProgressTaskDialogFragment : DialogFragment() {
@StringRes
private var title = UNDEFINED
@@ -40,10 +43,12 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
private var message = UNDEFINED
@StringRes
private var warning = UNDEFINED
private var cancellable: (() -> Unit)? = null
private var titleView: TextView? = null
private var messageView: TextView? = null
private var warningView: TextView? = null
private var cancelButton: Button? = null
private var progressView: ProgressBar? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -63,11 +68,13 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
titleView = root.findViewById(R.id.progress_dialog_title)
messageView = root.findViewById(R.id.progress_dialog_message)
warningView = root.findViewById(R.id.progress_dialog_warning)
cancelButton = root.findViewById(R.id.progress_dialog_cancel)
progressView = root.findViewById(R.id.progress_dialog_bar)
updateTitle(title)
updateMessage(message)
updateWarning(warning)
setCancellable(cancellable)
isCancelable = false
@@ -84,7 +91,7 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
}
private fun updateView(textView: TextView?, @StringRes resId: Int) {
activity?.runOnUiThread {
activity?.lifecycleScope?.launch {
if (resId == UNDEFINED) {
textView?.visibility = View.GONE
} else {
@@ -94,21 +101,35 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
}
}
fun updateTitle(@StringRes resId: Int) {
this.title = resId
private fun updateCancelable() {
activity?.lifecycleScope?.launch {
cancelButton?.isVisible = cancellable != null
cancelButton?.setOnClickListener {
cancellable?.invoke()
}
}
}
fun updateTitle(@StringRes resId: Int?) {
this.title = resId ?: UNDEFINED
updateView(titleView, title)
}
override fun updateMessage(@StringRes resId: Int) {
this.message = resId
fun updateMessage(@StringRes resId: Int?) {
this.message = resId ?: UNDEFINED
updateView(messageView, message)
}
fun updateWarning(@StringRes resId: Int) {
this.warning = resId
fun updateWarning(@StringRes resId: Int?) {
this.warning = resId ?: UNDEFINED
updateView(warningView, warning)
}
fun setCancellable(cancellable: (() -> Unit)?) {
this.cancellable = cancellable
updateCancelable()
}
companion object {
private val TAG = ProgressTaskDialogFragment::class.java.simpleName
const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment"

View File

@@ -126,6 +126,26 @@ object ParcelableUtil {
}
}
fun Parcel.readByteArrayCompat(): ByteArray? {
val dataLength = readInt()
return if (dataLength >= 0) {
val data = ByteArray(dataLength)
readByteArray(data)
data
} else {
null
}
}
fun Parcel.writeByteArrayCompat(data: ByteArray?) {
if (data != null) {
writeInt(data.size)
writeByteArray(data)
} else {
writeInt(-1)
}
}
inline fun <reified T : Enum<T>> Parcel.readEnum() =
readString()?.let { enumValueOf<T>(it) }

View File

@@ -226,10 +226,10 @@ object UriUtil {
}
}
fun getUriFromIntent(intent: Intent, key: String): Uri? {
fun getUriFromIntent(intent: Intent?, key: String): Uri? {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
val clipData = intent.clipData
val clipData = intent?.clipData
if (clipData != null) {
if (clipData.description.label == key) {
if (clipData.itemCount == 1) {
@@ -242,7 +242,7 @@ object UriUtil {
}
}
} catch (e: Exception) {
return intent.getParcelableExtra(key)
return intent?.getParcelableExtra(key)
}
return null
}
@@ -269,11 +269,15 @@ object UriUtil {
fun contributingUser(context: Context): Boolean {
return (Education.isEducationScreenReclickedPerformed(context)
|| isExternalAppInstalled(context, "com.kunzisoft.keepass.pro", false)
|| isExternalAppInstalled(
context,
context.getString(R.string.keepro_app_id),
false
)
)
}
private fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean {
fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean {
try {
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
Education.setEducationScreenReclickedPerformed(context)
@@ -295,10 +299,11 @@ object UriUtil {
}
try {
if (launchIntent == null) {
// TODO F-Droid
context.startActivity(
Intent(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse("https://play.google.com/store/apps/details?id=$packageName"))
.setData(Uri.parse(context.getString(R.string.play_store_url, packageName)))
)
} else {
context.startActivity(launchIntent)

View File

@@ -0,0 +1,149 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.text.InputType
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Filter
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
class HardwareKeySelectionView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: ConstraintLayout(context, attrs, defStyle) {
private var mHardwareKey: HardwareKey? = null
private val hardwareKeyLayout: TextInputLayout
private val hardwareKeyCompletion: AppCompatAutoCompleteTextView
var selectionListener: ((HardwareKey)-> Unit)? = null
private val mHardwareKeyAdapter = ArrayAdapterNoFilter(context)
private class ArrayAdapterNoFilter(context: Context)
: ArrayAdapter<String>(context, android.R.layout.simple_list_item_1) {
val hardwareKeys = HardwareKey.values()
override fun getCount(): Int {
return hardwareKeys.size
}
override fun getItem(position: Int): String {
return hardwareKeys[position].value
}
override fun getItemId(position: Int): Long {
// Or just return p0
return hardwareKeys[position].hashCode().toLong()
}
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(p0: CharSequence?): FilterResults {
return FilterResults().apply {
values = hardwareKeys
}
}
override fun publishResults(p0: CharSequence?, p1: FilterResults?) {
notifyDataSetChanged()
}
}
}
}
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_hardware_key_selection, this)
hardwareKeyLayout = findViewById(R.id.input_entry_hardware_key_layout)
hardwareKeyCompletion = findViewById(R.id.input_entry_hardware_key_completion)
hardwareKeyCompletion.isFocusable = false
hardwareKeyCompletion.isFocusableInTouchMode = false
//hardwareKeyCompletion.isEnabled = false
hardwareKeyCompletion.isCursorVisible = false
hardwareKeyCompletion.setTextIsSelectable(false)
hardwareKeyCompletion.inputType = InputType.TYPE_NULL
hardwareKeyCompletion.setAdapter(mHardwareKeyAdapter)
hardwareKeyCompletion.setOnClickListener {
hardwareKeyCompletion.showDropDown()
}
hardwareKeyCompletion.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
mHardwareKey = HardwareKey.fromPosition(position)
mHardwareKey?.let { hardwareKey ->
selectionListener?.invoke(hardwareKey)
}
}
}
var hardwareKey: HardwareKey?
get() {
return mHardwareKey
}
set(value) {
mHardwareKey = value
hardwareKeyCompletion.setText(value?.toString() ?: "")
}
var error: CharSequence?
get() = hardwareKeyLayout.error
set(value) {
hardwareKeyLayout.error = value
}
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val saveState = SavedState(superState)
saveState.mHardwareKey = this.mHardwareKey
return saveState
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state !is SavedState) {
super.onRestoreInstanceState(state)
return
}
super.onRestoreInstanceState(state.superState)
this.mHardwareKey = state.mHardwareKey
}
internal class SavedState : BaseSavedState {
var mHardwareKey: HardwareKey? = null
constructor(superState: Parcelable?) : super(superState)
private constructor(parcel: Parcel) : super(parcel) {
mHardwareKey = parcel.readEnum<HardwareKey>()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeEnum(mHardwareKey)
}
companion object CREATOR : Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -39,19 +39,24 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
class MainCredentialView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var passwordTextView: EditText
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxPasswordView: CompoundButton
private var passwordTextView: EditText
private var checkboxKeyFileView: CompoundButton
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxHardwareView: CompoundButton
private var hardwareKeySelectionView: HardwareKeySelectionView
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onValidateListener: (() -> Unit)? = null
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
@@ -60,15 +65,17 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_main_credentials, this)
passwordTextView = findViewById(R.id.password_text_view)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
passwordTextView = findViewById(R.id.password_text_view)
checkboxKeyFileView = findViewById(R.id.keyfile_checkbox)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxHardwareView = findViewById(R.id.hardware_key_checkbox)
hardwareKeySelectionView = findViewById(R.id.hardware_key_selection)
val onEditorActionListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == EditorInfo.IME_ACTION_DONE) {
onValidateListener?.invoke()
validateCredential()
return true
}
return false
@@ -91,7 +98,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
) {
onValidateListener?.invoke()
validateCredential()
handled = true
}
handled
@@ -100,10 +107,30 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
onPasswordChecked?.onCheckedChanged(view, checked)
}
checkboxKeyFileView.setOnCheckedChangeListener { view, checked ->
if (checked) {
if (keyFileSelectionView.uri == null) {
checkboxKeyFileView.isChecked = false
}
}
onKeyFileChecked?.onCheckedChanged(view, checked)
}
checkboxHardwareView.setOnCheckedChangeListener { view, checked ->
if (checked) {
if (hardwareKeySelectionView.hardwareKey == null) {
checkboxHardwareView.isChecked = false
}
}
onHardwareKeyChecked?.onCheckedChanged(view, checked)
}
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) {
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
hardwareKeySelectionView.selectionListener = { _ ->
checkboxHardwareView.isChecked = true
}
}
fun validateCredential() {
onValidateListener?.invoke()
}
fun populatePasswordTextView(text: String?) {
@@ -118,7 +145,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
}
}
fun populateKeyFileTextView(uri: Uri?) {
fun populateKeyFileView(uri: Uri?) {
if (uri == null || uri.toString().isEmpty()) {
keyFileSelectionView.uri = null
if (checkboxKeyFileView.isChecked)
@@ -130,16 +157,36 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
}
}
fun populateHardwareKeyView(hardwareKey: HardwareKey?) {
if (hardwareKey == null) {
hardwareKeySelectionView.hardwareKey = null
if (checkboxHardwareView.isChecked)
checkboxHardwareView.isChecked = false
} else {
hardwareKeySelectionView.hardwareKey = hardwareKey
if (!checkboxHardwareView.isChecked)
checkboxHardwareView.isChecked = true
}
}
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) {
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
}
fun isFill(): Boolean {
return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked
return checkboxPasswordView.isChecked
|| (checkboxKeyFileView.isChecked && keyFileSelectionView.uri != null)
|| (checkboxHardwareView.isChecked && hardwareKeySelectionView.hardwareKey != null)
}
fun getMainCredential(): MainCredential {
return MainCredential().apply {
this.masterPassword = if (checkboxPasswordView.isChecked)
this.password = if (checkboxPasswordView.isChecked)
passwordTextView.text?.toString() else null
this.keyFileUri = if (checkboxKeyFileView.isChecked)
keyFileSelectionView.uri else null
this.hardwareKey = if (checkboxHardwareView.isChecked)
hardwareKeySelectionView.hardwareKey else null
}
}
@@ -151,7 +198,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
// TODO HARDWARE_KEY
return when (mCredentialStorage) {
CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked
CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked
CredentialStorage.KEY_FILE -> false
CredentialStorage.HARDWARE_KEY -> false
}
}

View File

@@ -68,7 +68,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener { label, value ->
mOnCopyActionClickListener
?.invoke(Field(label, ProtectedString(false, value)))
?.invoke(Field(label, ProtectedString(true, value)))
}
} else {
setCopyButtonState(TextFieldView.ButtonState.GONE)

View File

@@ -0,0 +1,25 @@
package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class ChallengeResponseViewModel: ViewModel() {
val dataResponded : LiveData<ByteArray?> get() = _dataResponded
private val _dataResponded = MutableLiveData<ByteArray?>()
fun respond(byteArray: ByteArray) {
_dataResponded.value = byteArray
}
fun resendResponse() {
dataResponded.value?.let {
_dataResponded.value = it
}
}
fun consumeResponse() {
_dataResponded.value = null
}
}

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.app.database.IOActionTask
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
@@ -72,8 +73,12 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
}
}
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?) {
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseUri, keyFileUri) { databaseFileAdded ->
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?, hardwareKey: HardwareKey?) {
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(
databaseUri,
keyFileUri,
hardwareKey
) { databaseFileAdded ->
databaseFileAdded?.let { _ ->
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
this.databaseFileAction = DatabaseFileAction.ADD
@@ -96,6 +101,7 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
.find { it.databaseUri == databaseFileUpdated.databaseUri }
?.apply {
keyFileUri = databaseFileUpdated.keyFileUri
hardwareKey = databaseFileUpdated.hardwareKey
databaseAlias = databaseFileUpdated.databaseAlias
databaseFileExists = databaseFileUpdated.databaseFileExists
databaseLastModified = databaseFileUpdated.databaseLastModified

View File

Before

Width:  |  Height:  |  Size: 897 B

After

Width:  |  Height:  |  Size: 897 B

View File

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 657 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -34,7 +34,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent">
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -118,6 +118,11 @@
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/activity_about_privacy_text"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/activity_about_contribution_text"
android:layout_marginTop="8dp"
@@ -179,4 +184,5 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,16 +17,22 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true"
android:fitsSystemWindows="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
@@ -179,3 +185,6 @@
android:layout_gravity="start|bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -84,7 +84,7 @@
android:layout_height="?attr/actionBarSize"
android:theme="?attr/toolbarActionAppearance"
android:layout_gravity="bottom"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/entry_edit_validate"
@@ -96,7 +96,7 @@
android:tint="?attr/colorOnAccentColor"
app:fabSize="mini"
app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
@@ -105,7 +105,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"/>
<ProgressBar
android:id="@+id/loading"
@@ -119,4 +119,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -146,7 +146,7 @@
android:id="@+id/file_selection_buttons_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
@@ -194,4 +194,5 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -27,7 +27,7 @@
android:filterTouchesWhenObscured="true"
android:fitsSystemWindows="true">
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/activity_group_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -36,16 +36,17 @@
android:id="@+id/special_mode_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/toolbarSpecialAppearance" />
android:theme="?attr/toolbarSpecialAppearance"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:title="@string/app_name"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_below="@+id/special_mode_view"
android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance" >
android:theme="?attr/toolbarAppearance"
android:title="@string/app_name"
app:layout_constraintTop_toBottomOf="@+id/special_mode_view">
<FrameLayout
android:id="@+id/database_name_container"
android:layout_width="wrap_content"
@@ -64,9 +65,11 @@
<FrameLayout
android:layout_width="48dp"
android:layout_height="?attr/actionBarSize"
android:layout_below="@+id/special_mode_view"
android:layout_marginStart="50dp"
android:layout_marginLeft="50dp">
android:layout_marginLeft="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/special_mode_view">
<ImageView
android:id="@+id/database_color"
android:layout_width="12dp"
@@ -91,9 +94,9 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/group_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar"
android:layout_above="@+id/toolbar_action">
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/toolbar_action"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
@@ -159,7 +162,7 @@
android:layout_height="?attr/actionBarSize"
android:visibility="gone"
android:theme="?attr/toolbarActionAppearance"
android:layout_alignParentBottom="true" />
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<FrameLayout
android:layout_width="match_parent"
@@ -177,9 +180,10 @@
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/>
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
</RelativeLayout>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.kunzisoft.keepass.view.NavigationDatabaseView
android:id="@+id/database_nav_view"

View File

@@ -43,7 +43,7 @@
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
android:theme="?attr/toolbarActionAppearance"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/icon_picker_upload"
@@ -53,15 +53,17 @@
android:contentDescription="@string/validate"
android:src="@drawable/ic_file_upload_white_24dp"
app:fabSize="mini"
app:layout_constraintTop_toTopOf="@+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/toolbar" />
<include
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintStart_toStartOf="parent" />
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -14,7 +14,7 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
<ProgressBar
@@ -30,4 +30,6 @@
android:layout_gravity="center"
android:contentDescription="@string/entry_attachments" />
</FrameLayout>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -43,7 +43,7 @@
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
android:theme="?attr/toolbarActionAppearance"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/key_generator_validation"
@@ -55,7 +55,7 @@
android:tint="?attr/colorOnAccentColor"
app:fabSize="mini"
app:layout_constraintTop_toTopOf="@+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
@@ -64,5 +64,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,7 +17,7 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -28,6 +28,7 @@
tools:targetApi="o">
<com.kunzisoft.keepass.view.SpecialModeView
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/special_mode_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -36,11 +37,11 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/activity_password_coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
android:background="@drawable/background_repeat"
android:backgroundTint="?android:attr/textColor"
android:layout_below="@+id/special_mode_view"
android:layout_above="@+id/activity_password_footer">
app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
app:layout_constraintBottom_toTopOf="@+id/activity_password_footer">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
@@ -58,7 +59,7 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="144dp"
android:minHeight="106dp"
android:layout_marginTop="?attr/actionBarSize"
android:background="?attr/colorPrimary">
<LinearLayout
@@ -156,7 +157,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_alignParentBottom="true">
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
<LinearLayout
android:id="@+id/activity_password_info_container"
android:layout_width="match_parent"
@@ -193,4 +194,5 @@
android:text="@string/menu_open" />
</LinearLayout>
</RelativeLayout>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,15 +17,22 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/toolbar_coordinator"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:filterTouchesWhenObscured="true"
android:background="?android:attr/windowBackground"
android:fitsSystemWindows="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
@@ -46,5 +53,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -58,6 +58,16 @@
android:layout_marginEnd="20dp"
style="@style/KeepassDXStyle.TextAppearance.Warning"/>
<Button
android:id="@+id/progress_dialog_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp"
android:text="@string/entry_cancel" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_dialog_bar"
app:indicatorColor="?attr/colorAccent"

View File

@@ -20,6 +20,7 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:filterTouchesWhenObscured="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
@@ -115,7 +116,7 @@
android:orientation="vertical">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox"
android:id="@+id/keyfile_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_keyfile"/>
@@ -126,9 +127,41 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/keyfile_checkox"
app:layout_constraintStart_toEndOf="@+id/keyfile_checkbox"
app:layout_constraintEnd_toEndOf="parent" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_view_hardware_key"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_margin"
android:orientation="vertical">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/hardware_key_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hardware_key"/>
<com.kunzisoft.keepass.view.HardwareKeySelectionView
android:id="@+id/hardware_key_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/hardware_key_checkbox"
app:layout_constraintEnd_toEndOf="parent"
android:importantForAccessibility="no"
android:importantForAutofill="no" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container_hardware_key"
android:layout_marginBottom="@dimen/default_margin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants"
android:importantForAccessibility="no"
tools:ignore="UnusedAttribute">
<com.google.android.material.textfield.TextInputLayout
style="@style/KeepassDXStyle.TextInputLayout.ExposedMenu"
android:id="@+id/input_entry_hardware_key_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hardware_key">
<androidx.appcompat.widget.AppCompatAutoCompleteTextView
android:id="@+id/input_entry_hardware_key_completion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>

View File

@@ -63,7 +63,7 @@
android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox"
android:id="@+id/keyfile_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/keyfile_selection"
@@ -76,9 +76,35 @@
android:id="@+id/keyfile_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_toRightOf="@+id/keyfile_checkox"
android:layout_toEndOf="@+id/keyfile_checkox"
android:layout_toEndOf="@+id/keyfile_checkbox"
android:layout_toRightOf="@+id/keyfile_checkbox"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:minHeight="48dp" />
</RelativeLayout>
<!-- Hardware key -->
<RelativeLayout
android:id="@+id/container_hardware_key"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/hardware_key_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/hardware_key_selection"
android:layout_marginTop="22dp"
android:contentDescription="@string/content_description_hardware_key_checkbox"
android:focusable="false"
android:gravity="center_vertical" />
<com.kunzisoft.keepass.view.HardwareKeySelectionView
android:id="@+id/hardware_key_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/hardware_key_checkbox"
android:layout_toRightOf="@+id/hardware_key_checkbox"
android:importantForAccessibility="no"
android:importantForAutofill="no" />
</RelativeLayout>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/screenshot_mode_banner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/grey"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:text="@string/screenshot_mode_banner_text"
android:textColor="@color/white"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</merge>

View File

@@ -118,8 +118,8 @@
<string name="select_to_copy">اختر لنسخ %1$s إلى الحافظة</string>
<string name="retrieving_db_key">يجلب مفتاح قاعدة البيانات…</string>
<string name="default_checkbox">استخدامها كقاعدة بيانات افتراضية</string>
<string name="html_about_licence">KeePassDX © %1$d كونزيسوفت <strong>مفتوح المصدر</strong> و <strong>بدون اعلانات</strong>.
\n يوزع كما هو، بدون ضمان, تحت ترخيص <strong>GPLv3</strong>.</string>
<string name="html_about_licence">KeePassDX © %1$d كونزيسوفت &lt;strong&gt;مفتوح المصدر&lt;/strong&gt; و &lt;strong&gt;بدون اعلانات&lt;/strong&gt;.
\n يوزع كما هو، بدون ضمان, تحت ترخيص &lt;strong&gt;GPLv3&lt;/strong&gt;.</string>
<string name="entry_accessed">نُفذ إليه</string>
<string name="entry_expires">تنتهي صلاحيته في</string>
<string name="entry_keyfile">ملف المفتاح</string>

View File

@@ -300,15 +300,15 @@
<string name="education_read_only_title">Protecció contra escriptura de la base de dades</string>
<string name="education_donation_summary">Ajudeu a augmentar lestabilitat i la seguretat i a crear més funcionalitats.</string>
<string name="education_donation_title">Participació</string>
<string name="html_text_dev_feature_buy_pro">En comprar la versió <strong>professional</strong>,</string>
<string name="html_text_dev_feature_contibute">En <strong>col·laborar-hi</strong>,</string>
<string name="html_text_dev_feature_buy_pro">En comprar la versió &lt;strong&gt;professional&lt;/strong&gt;,</string>
<string name="html_text_dev_feature_contibute">En &lt;strong&gt;col·laborar-hi&lt;/strong&gt;,</string>
<string name="content_description_keyfile_checkbox">Casella del fitxer de la clau</string>
<string name="content_description_password_checkbox">Casella de la contrasenya</string>
<string name="content_description_otp_information">Informació de la contrasenya dun sol ús</string>
<string name="content_description_credentials_information">Informació de les dades daccés</string>
<string name="content_description_add_item">Afegeix un element</string>
<string name="education_lock_title">Bloca la base de dades</string>
<string name="html_text_feature_generosity">Aquest <strong>estil visual</strong> és disponible gràcies a la vostra generositat.</string>
<string name="html_text_feature_generosity">Aquest &lt;strong&gt;estil visual&lt;/strong&gt; és disponible gràcies a la vostra generositat.</string>
<string name="html_text_dev_feature_upgrade">No us oblideu de mantenir laplicació actualitzada instal·lant les versions noves.</string>
<string name="icon_section_standard">Estàndard</string>
<string name="show_uuid_title">Mostra lUUID</string>

View File

@@ -263,13 +263,13 @@
<string name="education_sort_summary">Vyberte řazení položek a skupin.</string>
<string name="education_donation_title">Zapojit se</string>
<string name="education_donation_summary">Zapojte se a pomozte zvýšit stabilitu, zabezpečení a doplnění dalších funkcí.</string>
<string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel je tato <strong>bez reklam</strong>, je <strong>svobodný software pod copyleft licencí</strong> a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string>
<string name="html_text_buy_pro">Zakoupením varianty „pro“ získáte přístup k tomuto <strong>vizuálnímu stylu</strong> a hlavně pomůžete <strong>uskutečnění komunitních projektů.</strong></string>
<string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel je tato &lt;strong&gt;bez reklam&lt;/strong&gt;, je &lt;strong&gt;svobodný software pod copyleft licencí&lt;/strong&gt; a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string>
<string name="html_text_buy_pro">Zakoupením varianty „pro“ získáte přístup k tomuto &lt;strong&gt;vizuálnímu stylu&lt;/strong&gt; a hlavně pomůžete &lt;strong&gt;uskutečnění komunitních projektů.&lt;/strong&gt;</string>
<string name="html_text_feature_generosity">Tento &lt;strong&gt;vizuální styl&lt;/strong&gt; je k dispozici díky vaší štědrosti.</string>
<string name="html_text_donation">Pro zajištění svobody nás všech a pokračování aktivity počítáme s Vaším &lt;strong&gt;přispěním.&lt;/strong&gt;</string>
<string name="html_text_dev_feature">Tato funkce je &lt;strong&gt;ve vývoji&lt;/strong&gt; a potřebuje Váš &lt;strong&gt;příspěvek&lt;/strong&gt;, aby byla brzy k dispozici.</string>
<string name="html_text_dev_feature_buy_pro">Zakoupením &lt;strong&gt;pro&lt;/strong&gt; varianty,</string>
<string name="html_text_dev_feature_contibute"><strong>Podpořením vývoje</strong>,</string>
<string name="html_text_dev_feature_contibute">&lt;strong&gt;Podpořením vývoje&lt;/strong&gt;,</string>
<string name="html_text_dev_feature_encourage">povzbudíte vývojáře k doplnění &lt;strong&gt;nových funkcí&lt;/strong&gt; a &lt;strong&gt;opravám chyb&lt;/strong&gt; dle vašich připomínek.</string>
<string name="html_text_dev_feature_thanks">Mnohokrát děkujeme za Váš příspěvek.</string>
<string name="html_text_dev_feature_work_hard">Tvrdě pracujeme na brzkém vydání této funkce.</string>
@@ -419,9 +419,9 @@
<string name="hide_broken_locations_title">Skrýt chybné odkazy na databáze</string>
<string name="hide_broken_locations_summary">Skrýt chybné odkazy v seznamu nedávných databází</string>
<string name="warning_database_read_only">Udělit právo zápisu pro uložení změn v databázi</string>
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft je <strong>open source</strong> a <strong>bez reklam</strong>.
\nJe poskytován jak je, od licencí <strong>GPLv3</strong>, bez jakékoli záruky.</string>
<string name="html_about_contribution">Abychom si <strong>udrželi svoji svobodu</strong>, <strong>mohli opravovat chyby</strong>, <strong>přidávat nové funkce</strong> a <strong>byli pořád aktivní</strong>, počítáme s Vaším <strong>přispěním</strong>.</string>
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft je &lt;strong&gt;open source&lt;/strong&gt; a &lt;strong&gt;bez reklam&lt;/strong&gt;.
\nJe poskytován jak je, od licencí &lt;strong&gt;GPLv3&lt;/strong&gt;, bez jakékoli záruky.</string>
<string name="html_about_contribution">Abychom si &lt;strong&gt;udrželi svoji svobodu&lt;/strong&gt;, &lt;strong&gt;mohli opravovat chyby&lt;/strong&gt;, &lt;strong&gt;přidávat nové funkce&lt;/strong&gt; a &lt;strong&gt;byli pořád aktivní&lt;/strong&gt;, počítáme s Vaším &lt;strong&gt;přispěním&lt;/strong&gt;.</string>
<string name="error_create_database">Nepodařilo se vytvořit soubor databáze.</string>
<string name="entry_add_attachment">Přidat přílohu</string>
<string name="discard">Zahodit</string>

View File

@@ -265,7 +265,7 @@
<string name="html_text_ad_free">I modsætning til andre programmer til adgangskodeadministration er denne &lt;strong&gt;annoncefri&lt;/strong&gt;, &lt;strong&gt;copyleft fri software&lt;/strong&gt;, og indsamler ikke personlige data, uanset hvilken version der bruges.</string>
<string name="html_text_buy_pro">Ved at købe pro-versionen, er der adgang til &lt;strong&gt;visuel stil&lt;/strong&gt;, og det vil især hjælpe &lt;strong&gt;gennemførelsen af lokale projekter.&lt;/strong&gt;</string>
<string name="html_text_feature_generosity">Denne &lt;strong&gt;visuelle stil&lt;/strong&gt; er tilgængelige takket være bidrag.</string>
<string name="html_text_donation">For at bevare uafhængighed og altid at være aktiv, håber vi på dit <strong>bidrag.</strong></string>
<string name="html_text_donation">For at bevare uafhængighed og altid at være aktiv, håber vi på dit &lt;strong&gt;bidrag.&lt;/strong&gt;</string>
<string name="html_text_dev_feature">Funktionen er &lt;strong&gt;under udvikling&lt;/strong&gt;, og det kræver &lt;strong&gt;bidrag&lt;/strong&gt;, for snart at være tilgængelig.</string>
<string name="html_text_dev_feature_buy_pro">Ved at købe &lt;strong&gt;pro&lt;/strong&gt; versionen,</string>
<string name="html_text_dev_feature_contibute">Ved at &lt;strong&gt;bidrage&lt;/strong&gt;,</string>

View File

@@ -150,7 +150,7 @@
</string-array>
<string name="warning_empty_password">Soll das Entsperren ohne Passwort wirklich möglich sein\?</string>
<string name="warning_no_encryption_key">Soll wirklich kein Verschlüsselungsschlüssel verwendet werden\?</string>
<string name="menu_appearance_settings">Aussehen</string>
<string name="menu_appearance_settings">Erscheinungsbild</string>
<string name="password_size_title">Generierte Passwortlänge</string>
<string name="password_size_summary">Legt die Standardlänge des generierten Passworts fest</string>
<string name="clipboard_notifications_title">Zwischenablage-Benachrichtigung</string>
@@ -162,7 +162,7 @@
<string name="path">Pfad</string>
<string name="file_name">Dateiname</string>
<string name="unavailable_feature_text">Dieses Feature konnte nicht gestartet werden.</string>
<string name="biometric_unlock_enable_summary">Ermöglicht Ihre Biometrie zu scannen, um die Datenbank zu öffnen.</string>
<string name="biometric_unlock_enable_summary">Ermöglicht das Scannen Ihrer biometrischen Daten, um die Datenbank zu öffnen</string>
<string name="advanced_unlock">Moderne Entsperrung</string>
<string name="biometric_unlock_enable_title">Biometrische Entsperrung</string>
<string name="lock">Sperren</string>
@@ -252,7 +252,7 @@
<string name="education_donation_summary">Mithelfen, um Stabilität und Sicherheit zu verbessern sowie weitere Funktionen zu ermöglichen.</string>
<string name="html_text_ad_free">Anders als viele andere Passwortmanager ist dieser &lt;strong&gt;werbefrei&lt;/strong&gt;, &lt;strong&gt;quelloffen&lt;/strong&gt; und unter einer &lt;strong&gt;Copyleft-Lizenz&lt;/strong&gt;. Es werden keine persönlichen Daten gesammelt, in welcher Form auch immer, unabhängig von der verwendeten Version (kostenlos oder Pro).</string>
<string name="html_text_buy_pro">Mit dem Kauf der Pro-Version erhalten Sie Zugriff auf diesen &lt;strong&gt;visuellen Stil&lt;/strong&gt; und unterstützen insbesondere &lt;strong&gt;die Umsetzung gemeinschaftlicher Projekte.&lt;/strong&gt;</string>
<string name="html_text_feature_generosity">Dieser <strong>visuelle Stil</strong> ist dank Ihrer Großzügigkeit verfügbar.</string>
<string name="html_text_feature_generosity">Dieser &lt;strong&gt;visuelle Stil&lt;/strong&gt; ist dank Ihrer Großzügigkeit verfügbar.</string>
<string name="html_text_donation">Um unsere Freiheit zu bewahren und immer aktiv zu bleiben, zählen wir auf Ihren &lt;strong&gt;Beitrag.&lt;/strong&gt;</string>
<string name="html_text_dev_feature">Diese Funktion ist &lt;strong&gt;in Entwicklung&lt;/strong&gt; und erfordert &lt;strong&gt;Ihren Beitrag&lt;/strong&gt;, um bald verfügbar zu sein.</string>
<string name="html_text_dev_feature_buy_pro">Durch den Kauf der &lt;strong&gt;Pro-Version&lt;/strong&gt;,</string>
@@ -260,7 +260,7 @@
<string name="html_text_dev_feature_encourage">Sie ermutigen die Entwickler:innen, &lt;strong&gt;neue Funktionen&lt;/strong&gt; einzuführen und gemäß Ihren Anmerkungen &lt;strong&gt;Fehler zu beheben&lt;/strong&gt;.</string>
<string name="html_text_dev_feature_thanks">Vielen Dank für Ihre Unterstützung.</string>
<string name="html_text_dev_feature_work_hard">Wir bemühen uns, diese Funktion bald zu veröffentlichen.</string>
<string name="html_text_dev_feature_upgrade">Denken Sie daran, Ihre App auf dem neuesten Stand zu halten, indem Sie neue Versionen installieren.</string>
<string name="html_text_dev_feature_upgrade">Denken Sie daran, Ihre App durch die Installation neuer Versionen auf dem aktuellsten Stand zu halten.</string>
<string name="download">Download</string>
<string name="contribute">Unterstützen</string>
<string name="icon_pack_choose_title">Symbolpaket</string>
@@ -285,7 +285,7 @@
\n„Schreibgeschützt“ verhindert unbeabsichtigte Änderungen an der Datenbank.
\nMit „Änderbar“ können Sie alle Elemente nach Belieben hinzufügen, löschen oder ändern.</string>
<string name="edit_entry">Eintrag bearbeiten</string>
<string name="error_load_database">Datenbank kann nicht geladen werden.</string>
<string name="error_load_database">Die Datenbank konnte nicht geladen werden.</string>
<string name="error_load_database_KDF_memory">Laden des Schlüssels fehlgeschlagen. Bitte versuchen, die „Speicherplatznutzung“ von KDF zu verringern.</string>
<string name="list_entries_show_username_title">Benutzernamen anzeigen</string>
<string name="list_entries_show_username_summary">Benutzernamen in Eintragslisten anzeigen</string>
@@ -303,16 +303,16 @@
<string name="keyboard_notification_entry_content_text">%1$s</string>
<string name="keyboard_notification_entry_clear_close_title">Beim Schließen löschen</string>
<string name="keyboard_notification_entry_clear_close_summary">Datenbank schließen, wenn die Benachrichtigung geschlossen wird</string>
<string name="keyboard_appearance_category">Aussehen</string>
<string name="keyboard_appearance_category">Erscheinungsbild</string>
<string name="keyboard_theme_title">Tastaturdesign</string>
<string name="keyboard_keys_category">Tasten</string>
<string name="keyboard_key_vibrate_title">Vibrierende Tastendrücke</string>
<string name="keyboard_key_sound_title">Hörbare Tastendrücke</string>
<string name="selection_mode">Auswahlmodus</string>
<string name="remember_database_locations_title">Datenbank-Speicherorte merken</string>
<string name="remember_database_locations_summary">Verfolgt, wo Datenbanken gespeichert sind</string>
<string name="remember_database_locations_summary">Verfolgt den Speicherort der Datenbanken</string>
<string name="remember_keyfile_locations_title">Schlüsseldatei-Speicherorte merken</string>
<string name="remember_keyfile_locations_summary">Verfolgt, wo Schlüsseldateien gespeichert sind</string>
<string name="remember_keyfile_locations_summary">Verfolgt den Speicherort der Schlüsseldateien</string>
<string name="show_recent_files_title">Zuletzt verwendete Dateien anzeigen</string>
<string name="show_recent_files_summary">Speicherort zuletzt verwendeter Datenbanken anzeigen</string>
<string name="hide_broken_locations_title">Defekte Datenbankverknüpfungen ausblenden</string>
@@ -349,7 +349,7 @@
<string name="content_description_background">Hintergrund</string>
<string name="content_description_update_from_list">Aktualisieren</string>
<string name="content_description_keyboard_close_fields">Felder schließen</string>
<string name="error_create_database_file">Es ist nicht möglich, eine Datenbank mit diesem Passwort und dieser Schlüsseldatei zu erstellen.</string>
<string name="error_create_database_file">Die Datenbank kann mit diesem Passwort und dieser Schlüsseldatei nicht erstellt werden.</string>
<string name="menu_advanced_unlock_settings">Modernes Entsperren</string>
<string name="biometric">Biometrisch</string>
<string name="enable">Aktivieren</string>
@@ -385,7 +385,7 @@
<string name="contains_duplicate_uuid_procedure">Problem lösen, indem neue UUIDs für Duplikate generiert werden und danach fortfahren\?</string>
<string name="database_opened">Datenbank geöffnet</string>
<string name="clipboard_explanation_summary">Eintragsfelder mithilfe der Zwischenablage des Geräts kopieren</string>
<string name="advanced_unlock_explanation_summary">Modernes Entsperren verwenden, um eine Datenbank einfacher zu öffnen.</string>
<string name="advanced_unlock_explanation_summary">Modernes Entsperren verwenden, um eine Datenbank einfacher zu öffnen</string>
<string name="database_data_compression_title">Datenkompression</string>
<string name="database_data_compression_summary">Datenkompression reduziert die Datenbankgröße</string>
<string name="max_history_items_title">Maximale Anzahl</string>
@@ -416,8 +416,8 @@
<string name="entry_attachments">Anhänge</string>
<string name="menu_restore_entry_history">Historie wiederherstellen</string>
<string name="menu_delete_entry_history">Historie löschen</string>
<string name="keyboard_auto_go_action_title">Auto-Key-Aktion</string>
<string name="keyboard_auto_go_action_summary">Aktion der Go-Taste, die automatisch nach dem Drücken einer Feldtaste ausgeführt wird</string>
<string name="keyboard_auto_go_action_title">Automatische Tastenaktion</string>
<string name="keyboard_auto_go_action_summary">Nach dem Drücken einer Feldtaste automatisch die Eingabetaste ausführen</string>
<string name="download_attachment">%1$s herunterladen</string>
<string name="download_initialization">Initialisieren </string>
<string name="download_progression">Fortschritt: %1$d%%</string>
@@ -440,7 +440,7 @@
<string name="warning_database_read_only">Datei Schreibrechte gewähren, um Datenbankänderungen zu speichern</string>
<string name="education_setup_OTP_summary">Einrichten einer Einmal-Passwortverwaltung (HOTP / TOTP), um ein Token zu generieren, das für die Zwei-Faktor-Authentifizierung (2FA) angefordert wird.</string>
<string name="education_setup_OTP_title">OTP einrichten</string>
<string name="error_create_database">Es ist nicht möglich, eine Datenbankdatei zu erstellen.</string>
<string name="error_create_database">Die Datenbankdatei kann nicht erstellt werden.</string>
<string name="entry_add_attachment">Anhang hinzufügen</string>
<string name="discard">Verwerfen</string>
<string name="discard_changes">Änderungen verwerfen\?</string>
@@ -464,7 +464,7 @@
<string name="content_description_add_item">Element hinzufügen</string>
<string name="filter">Filter</string>
<string name="keyboard_change">Tastatur wechseln</string>
<string name="keyboard_previous_fill_in_title">Auto-Key-Aktion</string>
<string name="keyboard_previous_fill_in_title">Automatische Tastenaktion</string>
<string name="keyboard_previous_database_credentials_title">Datenbank-Anmeldebildschirm</string>
<string name="keyboard_previous_fill_in_summary">Nach dem Ausführen der automatischen Tastenaktion automatisch zur vorherigen Tastatur wechseln</string>
<string name="keyboard_previous_database_credentials_summary">Auf dem Datenbank-Anmeldebildschirm automatisch zur vorherigen Tastatur wechseln</string>
@@ -531,7 +531,7 @@
<string name="device_credential_unlock_enable_title">Geräteanmeldedaten entsperren</string>
<string name="device_credential">Geräteanmeldedaten</string>
<string name="credential_before_click_advanced_unlock_button">Geben Sie das Passwort ein und klicken Sie dann auf diesen Knopf.</string>
<string name="advanced_unlock_prompt_not_initialized">Dialog für modernes Entsperren konnte nicht gestartet werden.</string>
<string name="advanced_unlock_prompt_not_initialized">Der Dialog für modernes Entsperren konnte nicht gestartet werden.</string>
<string name="advanced_unlock_scanning_error">Fehler beim modernen Entsperren: %1$s</string>
<string name="advanced_unlock_not_recognized">Abdruck zum modernen Entsperren nicht erkannt</string>
<string name="advanced_unlock_invalid_key">Schlüssel zum modernen Entsperren nicht lesbar. Bitte löschen Sie ihn und wiederholen Sie die Prozedur zur Entsperrerkennung.</string>
@@ -658,4 +658,25 @@
<string name="character_count">Anzahl der Zeichen: %1$d</string>
<string name="exclude_ambiguous_chars">Mehrdeutige Zeichen ausschließen</string>
<string name="title_case">Groß-/Kleinschreibung des Titels</string>
<string name="content_description_hardware_key_checkbox">Hardwareschlüssel-Kontrollkästchen</string>
<string name="hardware_key">Hardwareschlüssel</string>
<string name="error_no_hardware_key">Hardwareschlüssel auswählen.</string>
<string name="error_XML_malformed">XML fehlerhaft.</string>
<string name="waiting_challenge_response">Warte auf die Response-Antwort …</string>
<string name="waiting_challenge_request">Warte auf die Challenge-Aufgabe …</string>
<string name="error_cancel_by_user">Vom Benutzer abgebrochen.</string>
<string name="error_driver_required">Treiber für %1$s ist erforderlich.</string>
<string name="error_unable_merge_database_kdb">Die Zusammenführung aus einer Datenbank V1 ist nicht möglich.</string>
<string name="error_location_unknown">Der Speicherort der Datenbank ist unbekannt, Datenbank-Aktion kann nicht ausgeführt werden.</string>
<string name="error_hardware_key_unsupported">Der Hardwareschlüssel wird nicht unterstützt.</string>
<string name="error_empty_key">Der Schlüssel darf nicht leer sein.</string>
<string name="corrupted_file">Die Datei ist beschädigt.</string>
<string name="error_no_response_from_challenge">Die Response-Antwort kann nicht abgerufen werden.</string>
<string name="error_challenge_already_requested">Die Challenge-Aufgabe wurde bereits angefordert.</string>
<string name="error_response_already_provided">Die Response-Antwort wurde bereits übertragen.</string>
<string name="enable_screenshot_mode_title">Screenshot-Modus</string>
<string name="enable_screenshot_mode_summary">Erlauben Sie Apps von Drittanbietern, Screenshots der Anwendung aufzuzeichnen oder zu erstellen</string>
<string name="screenshot_mode_banner_text">Screenshot-Modus</string>
<string name="remember_hardware_key_title">Hardwareschlüssel merken</string>
<string name="remember_hardware_key_summary">Verfolgt die verwendeten Hardwareschlüssel</string>
</resources>

View File

@@ -267,7 +267,7 @@
\nΤο \"Προστατευμένο από εγγραφή\" αποτρέπει τυχόν μη επιθυμητές αλλαγές στη βάση δεδομένων.
\nΤο \"Τροποποιητικό\" σάς επιτρέπει να προσθέσετε, να διαγράψετε ή να τροποποιήσετε όλα τα στοιχεία όπως επιθυμείτε.</string>
<string name="edit_entry">Επεξεργασία καταχώρησης</string>
<string name="error_load_database">Δεν ήταν δυνατή η φόρτωση της βάσης δεδομένων σας.</string>
<string name="error_load_database">Δεν ήταν δυνατή η φόρτωση της βάσης δεδομένων.</string>
<string name="error_load_database_KDF_memory">Δεν ήταν δυνατή η φόρτωση του κλειδιού. Προσπαθήστε να μειώσετε την KDF \"Χρήση μνήμης\".</string>
<string name="list_entries_show_username_title">Εμφάνιση ονομάτων χρηστών</string>
<string name="list_entries_show_username_summary">Εμφάνιση ονομάτων χρηστών σε λίστες καταχώρησης</string>
@@ -645,4 +645,26 @@
<string name="upper_case">ΚΕΦΑΛΑΙΑ ΓΡΑΜΜΑΤΑ</string>
<string name="word_separator">Διαχωριστής</string>
<string name="character_count">Αριθμός χαρακτήρων: %1$d</string>
<string name="error_no_hardware_key">Επιλέξτε ένα κλειδί υλικού.</string>
<string name="error_XML_malformed">Το XML είναι εσφαλμένο.</string>
<string name="error_cancel_by_user">Ακυρώθηκε από τον χρήστη.</string>
<string name="error_driver_required">Απαιτείται πρόγραμμα οδήγησης για %1$s.</string>
<string name="error_hardware_key_unsupported">Το κλειδί υλικού δεν υποστηρίζεται.</string>
<string name="remember_hardware_key_title">Να θυμάται τα κλειδιά υλικού</string>
<string name="remember_hardware_key_summary">Παρακολουθεί τα κλειδιά υλικού που χρησιμοποιούνται</string>
<string name="enable_screenshot_mode_summary">Επιτρέψτε σε εφαρμογές τρίτων να καταγράφουν ή να λαμβάνουν στιγμιότυπα οθόνης της εφαρμογής</string>
<string name="waiting_challenge_request">Αναμονή για το αίτημα πρόκλησης…</string>
<string name="waiting_challenge_response">Αναμονή για το αίτημα πρόκλησης…</string>
<string name="content_description_hardware_key_checkbox">Πλαίσιο ελέγχου κλειδιού υλικού</string>
<string name="hardware_key">Κλειδί υλικού</string>
<string name="error_no_response_from_challenge">Δεν είναι δυνατή η λήψη της απάντησης από την πρόκληση.</string>
<string name="enable_screenshot_mode_title">Λειτουργία στιγμιότυπου οθόνης</string>
<string name="screenshot_mode_banner_text">Λειτουργία στιγμιότυπου οθόνης</string>
<string name="error_challenge_already_requested">Η πρόκληση έχει ήδη ζητηθεί</string>
<string name="error_response_already_provided">Η απάντηση έχει ήδη δοθεί.</string>
<string name="error_location_unknown">Η θέση της βάσης δεδομένων είναι άγνωστη, η ενέργεια της βάσης δεδομένων δεν μπορεί να εκτελεστεί.</string>
<string name="error_unable_merge_database_kdb">Δεν είναι δυνατή η συγχώνευση από μια βάση δεδομένων V1.</string>
<string name="error_empty_key">Το κλειδί δεν μπορεί να είναι κενό.</string>
<string name="corrupted_file">Κατεστραμμένο αρχείο.</string>
<string name="html_about_privacy">&lt;strong&gt;Δεν ανακτώνται δεδομένα χρήστη&lt;/strong&gt;, αυτή η εφαρμογή δεν συνδέεται με κανένα διακομιστή, λειτουργεί μόνο τοπικά και σέβεται πλήρως το απόρρητο των χρηστών.</string>
</resources>

View File

@@ -20,5 +20,34 @@
\n
\nGroups (~folders) organise entries in your database.</string>
<string name="error_otp_type">The existing OTP type is not recognised by this form, its validation may no longer correctly generate the token.</string>
<string name="html_text_buy_pro">By buying the pro version, you will have access to this <strong>visual style</strong> and you will especially help <strong>the realisation of community projects.</strong></string>
<string name="html_text_buy_pro">By buying the pro version, you will have access to this &lt;strong&gt;visual style&lt;/strong&gt; and you will especially help &lt;strong&gt;the realisation of community projects.&lt;/strong&gt;</string>
<string name="key_derivation_function">Key derivation function</string>
<string name="feedback">Feedback</string>
<string name="homepage">Homepage</string>
<string name="accept">Accept</string>
<string name="add_entry">Add entry</string>
<string name="edit_entry">Edit entry</string>
<string name="add_group">Add group</string>
<string name="master_key">Master key</string>
<string name="security">Security</string>
<string name="encryption">Encryption</string>
<string name="contact">Contact</string>
<string name="contribution">Contribution</string>
<string name="about_description">password</string>
<string name="encryption_algorithm">Encryption</string>
<string name="app_timeout">Timeout</string>
<string name="app_timeout_summary">database</string>
<string name="application">App</string>
<string name="brackets">Brackets</string>
<string name="extended_ASCII">Extended ASCII</string>
<string name="allow">Allow</string>
<string name="content_description_background">Background</string>
<string name="content_description_open_file">Open file</string>
<string name="content_description_node_children">Node children</string>
<string name="content_description_add_node">Add node</string>
<string name="content_description_add_entry">Add entry</string>
<string name="content_description_add_group">Add group</string>
<string name="content_description_add_item">Add item</string>
<string name="content_description_file_information">File info</string>
<string name="content_description_credentials_information">Credentials info</string>
</resources>

View File

@@ -243,9 +243,9 @@
<string name="education_sort_summary">Ordenar registros y grupos de acuerdo a parámetros específicos.</string>
<string name="education_donation_title">Participar</string>
<string name="education_donation_summary">Participe para aumentar la estabilidad, la seguridad y agregar más funciones.</string>
<string name="html_text_ad_free">A diferencia de muchas aplicaciones de gestión de contraseñas, esta <strong>no tiene publicidad</strong>, es <strong>libre, con licencia «copyleft»</strong> y no recopila datos personales en sus servidores, sin importar la versión que use.</string>
<string name="html_text_ad_free">A diferencia de muchas aplicaciones de gestión de contraseñas, esta &lt;strong&gt;no tiene publicidad&lt;/strong&gt;, es &lt;strong&gt;libre, con licencia «copyleft»&lt;/strong&gt; y no recopila datos personales en sus servidores, sin importar la versión que use.</string>
<string name="html_text_buy_pro">Al comprar la versión pro, tendrá acceso al &lt;strong&gt;estilo visual &lt;/strong&gt;y ayudará especialmente a &lt;strong&gt;la realización de proyectos comunitarios.&lt;/strong&gt;</string>
<string name="html_text_feature_generosity">Este <strong>estilo visual</strong> está disponible gracias a su generosidad.</string>
<string name="html_text_feature_generosity">Este &lt;strong&gt;estilo visual&lt;/strong&gt; está disponible gracias a su generosidad.</string>
<string name="html_text_donation">Para mantener nuestra libertad y estar siempre vigente, contamos con tu &lt;strong&gt;contribución.&lt;/strong&gt;</string>
<string name="html_text_dev_feature">Esta función está &lt;strong&gt;en desarrollo&lt;/strong&gt; y requiere de tu &lt;strong&gt;contribución&lt;/strong&gt; para estar disponible dentro de poco.</string>
<string name="html_text_dev_feature_buy_pro">Al comprar la versión &lt;strong&gt;pro&lt;/strong&gt;,</string>
@@ -499,7 +499,7 @@
<string name="autofill_application_id_blocklist_title">Lista de bloqueo de las aplicaciones</string>
<string name="autofill_ask_to_save_data_summary">Solicitar datos de guardado al completar el llenado de un formulario</string>
<string name="autofill_ask_to_save_data_title">Pedir que se guarden los datos</string>
<string name="autofill_save_search_info_summary">Intente guardar la información de la búsqueda cuando haga una selección de entrada manual</string>
<string name="autofill_save_search_info_summary">Trate de guardar la información de búsqueda al hacer una selección de entrada manual para facilitar los usos futuros</string>
<string name="autofill_save_search_info_title">Guardar la información de la búsqueda</string>
<string name="autofill_close_database_summary">Cerrar la base de datos después de una selección de autocompletado</string>
<string name="autofill_close_database_title">Cerrar la base de datos</string>
@@ -515,7 +515,7 @@
<string name="keyboard_previous_database_credentials_summary">Cambiar automáticamente al teclado anterior en la pantalla de credenciales de la base de datos</string>
<string name="keyboard_previous_database_credentials_title">Pantalla de credenciales de la base de datos</string>
<string name="keyboard_auto_go_action_title">Acción de la tecla automática</string>
<string name="keyboard_save_search_info_summary">Tras compartir informe a KeePassDX, cuando esté seleccionado un apunte, intente guardar el informe dentro del apunte para posibles futuros usos</string>
<string name="keyboard_save_search_info_summary">Trate de guardar la información compartida al hacer una selección de entrada manual para facilitar los usos futuros</string>
<string name="keyboard_save_search_info_title">Guardar información compartida</string>
<string name="show_uuid_summary">Muestra el UUID vinculado a una entrada o a un grupo</string>
<string name="show_uuid_title">Mostrar UUID</string>
@@ -607,6 +607,8 @@
<string name="warning_keyfile_integrity">El hash del archivo no está garantizado porque Android puede cambiar sus datos sobre la marcha. Cambia la extensión del archivo a .bin para una correcta integridad.</string>
<string name="enable_keep_screen_on_title">Mantener la pantalla encendida</string>
<string name="enable_keep_screen_on_summary">Mantenga la pantalla encendida cuando vea la entrada</string>
<string name="enable_screenshot_mode_title">Modo captura de pantalla</string>
<string name="enable_screenshot_mode_summary">Permitir que otras aplicaciones graben o tomen capturas de pantalla de la aplicación</string>
<string name="show_entry_colors_summary">Muestra los colores de primer y segundo plano en una entrada</string>
<string name="show_entry_colors_title">Colores de entrada</string>
<string name="menu_merge_database">Fusionar datos</string>
@@ -647,4 +649,5 @@
<string name="upper_case">MAYÚSCULAS</string>
<string name="title_case">Tipo Titular</string>
<string name="character_count">Conteo de caracteres: %1$d</string>
<string name="screenshot_mode_banner_text">Modo captura de pantalla</string>
</resources>

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