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) KeePassDX(3.4.5)
* Fix custom data in group (fix KeeShare) #1335 * Fix custom data in group (fix KeeShare) #1335
* Fix device credential unlocking #1344 * Fix device credential unlocking #1344

View File

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

View File

@@ -12,8 +12,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 32 targetSdkVersion 32
versionCode = 114 versionCode = 115
versionName = "3.4.5" versionName = "3.5.0 Beta01"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -93,7 +93,7 @@ android {
} }
} }
def room_version = "2.4.2" def room_version = "2.4.3"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@@ -101,14 +101,14 @@ dependencies {
implementation "androidx.appcompat:appcompat:$android_appcompat_version" implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.cardview:cardview:1.0.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.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.media:media:1.6.0' implementation 'androidx.media:media:1.6.0'
// Lifecycle - LiveData - ViewModel - Coroutines // Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:$android_core_version" 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" implementation "com.google.android.material:material:$android_material_version"
// Token auto complete // Token auto complete
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed // 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"/> <category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="file" /> <data android:scheme="file" />
<data android:scheme="content" /> <data android:scheme="content" />
<data android:mimeType="application/octet-stream" />
<data android:mimeType="application/x-kdb" /> <data android:mimeType="application/x-kdb" />
<data android:mimeType="application/x-kdbx" /> <data android:mimeType="application/x-kdbx" />
<data android:mimeType="application/x-keepass" /> <data android:mimeType="application/x-keepass" />

View File

@@ -77,6 +77,12 @@ class AboutActivity : StylishActivity() {
HtmlCompat.FROM_HTML_MODE_LEGACY) 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 { findViewById<TextView>(R.id.activity_about_contribution_text).apply {
movementMethod = LinkMovementMethod.getInstance() movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution), 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.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation 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.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
@@ -155,8 +156,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen -> mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri -> fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity( launchPasswordActivity(
databaseFileUri, databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey
) )
} }
} }
@@ -250,7 +252,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
?: MainCredential() ?: MainCredential()
databaseFilesViewModel.addDatabaseFile( databaseFilesViewModel.addDatabaseFile(
databaseUri, databaseUri,
mainCredential.keyFileUri mainCredential.keyFileUri,
mainCredential.hardwareKey
) )
} }
} }
@@ -297,10 +300,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() 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, MainCredentialActivity.launch(this,
databaseUri, databaseUri,
keyFile, keyFile,
hardwareKey,
{ exception -> { exception ->
fileNoFoundAction(exception) fileNoFoundAction(exception)
}, },
@@ -321,7 +325,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
private fun launchPasswordActivityWithPath(databaseUri: Uri) { private fun launchPasswordActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null) launchPasswordActivity(databaseUri, null, null)
// Delete flickering for kitkat <= // Delete flickering for kitkat <=
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
overridePendingTransition(0, 0) 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.education.GroupActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.GroupInfo 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.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK 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.AdvancedUnlockFragment
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.Database 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.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY 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 mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mRememberHardwareKey: Boolean = false
private var mReadOnly: Boolean = false private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false private var mForceReadOnly: Boolean = false
@@ -133,11 +137,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
PreferencesUtil.enableReadOnlyDatabase(this) PreferencesUtil.enableReadOnlyDatabase(this)
} }
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(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 -> mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) { if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri) mainCredentialView?.populateKeyFileView(uri)
} }
} }
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper) mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
@@ -171,6 +177,16 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
mAdvancedUnlockViewModel.checkUnlockAvailability() mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton() enableConfirmationButton()
} }
mainCredentialView?.onKeyFileChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
mainCredentialView?.onHardwareKeyChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
// Observe if default database // Observe if default database
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
@@ -204,10 +220,19 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
databaseKeyFileUri databaseKeyFileUri
} }
val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey
val hardwareKey =
if (mRememberHardwareKey
&& databaseHardwareKey == null) {
databaseFile?.hardwareKey
} else {
databaseHardwareKey
}
// Define title // Define title
filenameView?.text = databaseFile?.databaseAlias ?: "" filenameView?.text = databaseFile?.databaseAlias ?: ""
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri) onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
} }
} }
@@ -215,6 +240,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
super.onResume() super.onResume()
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
// Back to previous keyboard is setting activated // Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) { if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
@@ -332,24 +358,36 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private fun getUriFromIntent(intent: Intent?) { private fun getUriFromIntent(intent: Intent?) {
// If is a view intent // If is a view intent
val action = intent?.action val action = intent?.action
if (action != null if (action == VIEW_INTENT) {
&& action == VIEW_INTENT) { fillCredentials(
mDatabaseFileUri = intent.data intent.data,
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE)) UriUtil.getUriFromIntent(intent, KEY_KEYFILE),
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
)
} else { } else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME) fillCredentials(
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let { intent?.getParcelableExtra(KEY_FILENAME),
mainCredentialView?.populateKeyFileTextView(it) intent?.getParcelableExtra(KEY_KEYFILE),
} HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
)
} }
try { try {
intent?.removeExtra(KEY_KEYFILE) intent?.removeExtra(KEY_KEYFILE)
intent?.removeExtra(KEY_HARDWARE_KEY)
} catch (e: Exception) {} } catch (e: Exception) {}
mDatabaseFileUri?.let { mDatabaseFileUri?.let {
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it) 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?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
getUriFromIntent(intent) getUriFromIntent(intent)
@@ -358,7 +396,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private fun launchGroupActivityIfLoaded(database: Database) { private fun launchGroupActivityIfLoaded(database: Database) {
// Check if database really loaded // Check if database really loaded
if (database.loaded) { if (database.loaded) {
clearCredentialsViews(true) clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
GroupActivity.launch(this, GroupActivity.launch(this,
database, database,
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
@@ -408,7 +446,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential() val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
when (cipherDecryptDatabase.credentialStorage) { when (cipherDecryptDatabase.credentialStorage) {
CredentialStorage.PASSWORD -> { CredentialStorage.PASSWORD -> {
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue) mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
} }
CredentialStorage.KEY_FILE -> { CredentialStorage.KEY_FILE -> {
// TODO advanced unlock 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 // Define Key File text
if (mRememberKeyFile) { if (mRememberKeyFile) {
mainCredentialView?.populateKeyFileTextView(keyFileUri) mainCredentialView?.populateKeyFileView(keyFileUri)
}
// Define hardware key
if (mRememberHardwareKey) {
mainCredentialView?.populateHardwareKeyView(hardwareKey)
} }
// Define listener for validate button // Define listener for validate button
confirmButtonView?.setOnClickListener { loadDatabase() } confirmButtonView?.setOnClickListener {
mainCredentialView?.validateCredential()
}
// If Activity is launch with a password and want to open directly // If Activity is launch with a password and want to open directly
val intent = intent 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) mainCredentialView?.populatePasswordTextView(null)
if (clearKeyFile) { 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_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile" 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 VIEW_INTENT = "android.intent.action.VIEW"
private const val KEY_READ_ONLY = "KEY_READ_ONLY" private const val KEY_READ_ONLY = "KEY_READ_ONLY"
private const val KEY_PASSWORD = "password" private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" 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) { intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, MainCredentialActivity::class.java) val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile) intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null) if (keyFile != null)
intent.putExtra(KEY_KEYFILE, keyFile) intent.putExtra(KEY_KEYFILE, keyFile)
if (hardwareKey != null)
intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString())
intentBuildLauncher.invoke(intent) intentBuildLauncher.invoke(intent)
} }
@@ -680,8 +737,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launch(activity: Activity, fun launch(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?) { keyFile: Uri?,
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> hardwareKey: HardwareKey?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent) activity.startActivity(intent)
} }
} }
@@ -696,8 +754,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForSearchResult(activity: Activity, fun launchForSearchResult(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) { searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult( EntrySelectionHelper.startActivityForSearchModeResult(
activity, activity,
intent, intent,
@@ -715,8 +774,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForSaveResult(activity: Activity, fun launchForSaveResult(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) { searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSaveModeResult( EntrySelectionHelper.startActivityForSaveModeResult(
activity, activity,
intent, intent,
@@ -734,8 +794,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForKeyboardResult(activity: Activity, fun launchForKeyboardResult(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult( EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
activity, activity,
intent, intent,
@@ -754,10 +815,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForAutofillResult(activity: AppCompatActivity, fun launchForAutofillResult(activity: AppCompatActivity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
activityResultLauncher: ActivityResultLauncher<Intent>?, activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
@@ -775,8 +837,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForRegistration(activity: Activity, fun launchForRegistration(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
registerInfo: RegisterInfo?) { registerInfo: RegisterInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult( EntrySelectionHelper.startActivityForRegistrationModeResult(
activity, activity,
intent, intent,
@@ -792,6 +855,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launch(activity: AppCompatActivity, fun launch(activity: AppCompatActivity,
databaseUri: Uri, databaseUri: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit, fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit, onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit, onLaunchActivitySpecialMode: () -> Unit,
@@ -800,43 +864,67 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
try { try {
EntrySelectionHelper.doSpecialAction(activity.intent, EntrySelectionHelper.doSpecialAction(activity.intent,
{ {
MainCredentialActivity.launch(activity, launch(
databaseUri, keyFile) activity,
databaseUri,
keyFile,
hardwareKey
)
}, },
{ searchInfo -> // Search Action { searchInfo -> // Search Action
MainCredentialActivity.launchForSearchResult(activity, launchForSearchResult(
databaseUri, keyFile, activity,
searchInfo) databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo -> // Save Action { searchInfo -> // Save Action
MainCredentialActivity.launchForSaveResult(activity, launchForSaveResult(
databaseUri, keyFile, activity,
searchInfo) databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo -> // Keyboard Selection Action { searchInfo -> // Keyboard Selection Action
MainCredentialActivity.launchForKeyboardResult(activity, launchForKeyboardResult(
databaseUri, keyFile, activity,
searchInfo) databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo, autofillComponent -> // Autofill Selection Action { searchInfo, autofillComponent -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
MainCredentialActivity.launchForAutofillResult(activity, launchForAutofillResult(
databaseUri, keyFile, activity,
autofillActivityResultLauncher, databaseUri,
autofillComponent, keyFile,
searchInfo) hardwareKey,
autofillActivityResultLauncher,
autofillComponent,
searchInfo
)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
} else { } else {
onCancelSpecialMode() onCancelSpecialMode()
} }
}, },
{ registerInfo -> // Registration Action { registerInfo -> // Registration Action
MainCredentialActivity.launchForRegistration(activity, launchForRegistration(
databaseUri, keyFile, activity,
registerInfo) databaseUri,
keyFile,
hardwareKey,
registerInfo
)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
} }
) )

View File

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

View File

@@ -26,7 +26,7 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.database.element.MainCredential
class PasswordEncodingDialogFragment : DialogFragment() { 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_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY)) stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.download) { _, _ -> 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 { } 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_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY)) stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.contribute) { _, _ -> builder.setPositiveButton(R.string.contribute) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.contribution_url) UriUtil.gotoUrl(activity, R.string.contribution_url)
} }
} }
builder.setMessage(stringBuilder) 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.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener 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.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.HardwareKeySelectionView
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.PassKeyView import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.applyFontVisibility import com.kunzisoft.keepass.view.applyFontVisibility
@@ -45,18 +48,21 @@ import com.kunzisoft.keepass.view.applyFontVisibility
class SetMainCredentialDialogFragment : DatabaseDialogFragment() { class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mMasterPassword: String? = null 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 lateinit var keyFileCheckBox: CompoundButton
private var passwordRepeatTextInputLayout: TextInputLayout? = null private lateinit var keyFileSelectionView: KeyFileSelectionView
private var passwordRepeatView: TextView? = null
private var keyFileCheckBox: CompoundButton? = null private lateinit var hardwareKeyCheckBox: CompoundButton
private var keyFileSelectionView: KeyFileSelectionView? = null private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
private var mListener: AssignMainCredentialDialogListener? = null private var mListener: AssignMainCredentialDialogListener? = null
@@ -67,13 +73,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mNoKeyConfirmationDialog: AlertDialog? = null private var mNoKeyConfirmationDialog: AlertDialog? = null
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
private var mAllowNoMasterKey: Boolean = false
private val passwordTextWatcher = object : TextWatcher { private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) { override fun afterTextChanged(editable: Editable) {
passwordCheckBox?.isChecked = true passwordCheckBox.isChecked = true
} }
} }
@@ -113,10 +121,9 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity -> activity?.let { activity ->
var allowNoMasterKey = false
arguments?.apply { arguments?.apply {
if (containsKey(ALLOW_NO_MASTER_KEY_ARG)) 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) val builder = AlertDialog.Builder(activity)
@@ -128,63 +135,63 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
.setPositiveButton(android.R.string.ok) { _, _ -> } .setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .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) UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
} }
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox) passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
passKeyView = rootView?.findViewById(R.id.password_view) passwordView = rootView.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout) passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation) passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
passwordRepeatView?.applyFontVisibility() passwordRepeatView.applyFontVisibility()
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox) keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) 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 = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { pathUri -> uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile -> UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null keyFileSelectionView.error = null
keyFileCheckBox?.isChecked = true keyFileCheckBox.isChecked = true
keyFileSelectionView?.uri = pathUri keyFileSelectionView.uri = pathUri
if (lengthFile <= 0L) { if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog() 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() val dialog = builder.create()
dialog.setOnShowListener { dialog1 ->
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
if (passwordCheckBox != null && keyFileCheckBox!= null) { mMasterPassword = ""
dialog.setOnShowListener { dialog1 -> mKeyFileUri = null
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE) mHardwareKey = null
positiveButton.setOnClickListener {
mMasterPassword = "" approveMainCredential()
mKeyFile = null }
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
var error = verifyPassword() || verifyKeyFile() negativeButton.setOnClickListener {
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) { mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
error = true dismiss()
if (allowNoMasterKey)
showNoKeyConfirmationDialog()
else {
passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
}
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
dismiss()
}
} }
} }
@@ -194,67 +201,113 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
return super.onCreateDialog(savedInstanceState) 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 { private fun retrieveMainCredential(): MainCredential {
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
return MainCredential(masterPassword, keyFile) val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
return MainCredential(masterPassword, keyFileUri, hardwareKey)
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// To check checkboxes if a text is present // To check checkboxes if a text is present
passKeyView?.addTextChangedListener(passwordTextWatcher) passwordView.addTextChangedListener(passwordTextWatcher)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
passKeyView?.removeTextChangedListener(passwordTextWatcher) passwordView.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
} }
private fun showEmptyPasswordConfirmationDialog() { private fun showEmptyPasswordConfirmationDialog() {
@@ -262,10 +315,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
val builder = AlertDialog.Builder(it) val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password) builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) { mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) this@SetMainCredentialDialogFragment.dismiss()
this@SetMainCredentialDialogFragment.dismiss()
}
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
mEmptyPasswordConfirmationDialog = builder.create() mEmptyPasswordConfirmationDialog = builder.create()
@@ -299,8 +350,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}) })
.setPositiveButton(android.R.string.ok) { _, _ -> } .setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
keyFileCheckBox?.isChecked = false keyFileCheckBox.isChecked = false
keyFileSelectionView?.uri = null keyFileSelectionView.uri = null
} }
mEmptyKeyFileConfirmationDialog = builder.create() mEmptyKeyFileConfirmationDialog = builder.create()
mEmptyKeyFileConfirmationDialog?.show() mEmptyKeyFileConfirmationDialog?.show()

View File

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

View File

@@ -56,7 +56,7 @@ class ExternalFileHelper {
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) { fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
val resultCallback = ActivityResultCallback<Uri> { result -> val resultCallback = ActivityResultCallback<Uri?> { result ->
result?.let { uri -> result?.let { uri ->
UriUtil.takeUriPermission(activity?.contentResolver, uri) UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri) onFileSelected?.invoke(uri)
@@ -91,7 +91,7 @@ class ExternalFileHelper {
fun buildCreateDocument(typeString: String = "application/octet-stream", fun buildCreateDocument(typeString: String = "application/octet-stream",
onFileCreated: (fileCreated: Uri?)->Unit) { onFileCreated: (fileCreated: Uri?)->Unit) {
val resultCallback = ActivityResultCallback<Uri> { result -> val resultCallback = ActivityResultCallback<Uri?> { result ->
onFileCreated.invoke(result) onFileCreated.invoke(result)
} }
@@ -150,7 +150,7 @@ class ExternalFileHelper {
class OpenDocument : ActivityResultContracts.OpenDocument() { class OpenDocument : ActivityResultContracts.OpenDocument() {
@SuppressLint("InlinedApi") @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 { return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) 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 { override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply { return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE) 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.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database 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.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.ChallengeResponseViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
@@ -17,10 +18,12 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
protected var mDatabase: Database? = null protected var mDatabase: Database? = null
private val mChallengeResponseViewModel: ChallengeResponseViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this) mDatabaseTaskProvider = DatabaseTaskProvider(this, mChallengeResponseViewModel)
mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
val databaseWasReloaded = database?.wasReloaded == true 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?) { override fun onDatabaseRetrieved(database: Database?) {
mDatabase = database mDatabase = database
mDatabaseViewModel.defineDatabase(database) mDatabaseViewModel.defineDatabase(database)

View File

@@ -32,7 +32,6 @@ import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R 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.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper 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.database.element.node.NodeId
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.GroupInfo 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.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable 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.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log 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.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity 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.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 * 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) 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 // 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() { override fun onResume() {
@@ -94,6 +117,7 @@ abstract class StylishActivity : AppCompatActivity() {
Log.d(this.javaClass.name, "Theme change detected, restarting activity") Log.d(this.javaClass.name, "Theme change detected, restarting activity")
recreateActivity() recreateActivity()
} }
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
} }
private fun recreateActivity() { private fun recreateActivity() {

View File

@@ -23,8 +23,15 @@ import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import android.content.Context 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 class AppDatabase : RoomDatabase() {
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app.database
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.DatabaseFile import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -44,6 +45,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
DatabaseFile( DatabaseFile(
databaseUri, databaseUri,
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri), UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri), UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
fileDatabaseInfo.exists, fileDatabaseInfo.exists,
@@ -85,13 +87,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|| !hideBrokenLocations) { || !hideBrokenLocations) {
databaseFileListLoaded.add( databaseFileListLoaded.add(
DatabaseFile( DatabaseFile(
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri), UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri), UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri), HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias), UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
fileDatabaseInfo.exists, fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.getLastModificationString(), fileDatabaseInfo.exists,
fileDatabaseInfo.getSizeString() fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
) )
) )
} }
@@ -107,11 +110,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
).execute() ).execute()
} }
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null, fun addOrUpdateDatabaseUri(databaseUri: Uri,
keyFileUri: Uri? = null,
hardwareKey: HardwareKey? = null,
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) { databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
addOrUpdateDatabaseFile(DatabaseFile( addOrUpdateDatabaseFile(DatabaseFile(
databaseUri, databaseUri,
keyFileUri keyFileUri,
hardwareKey
), databaseFileAddedOrUpdatedResult) ), databaseFileAddedOrUpdatedResult)
} }
@@ -130,6 +136,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
?: fileDatabaseHistoryRetrieve?.databaseAlias ?: fileDatabaseHistoryRetrieve?.databaseAlias
?: "", ?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(), databaseFileToAddOrUpdate.keyFileUri?.toString(),
databaseFileToAddOrUpdate.hardwareKey?.value,
System.currentTimeMillis() System.currentTimeMillis()
) )
@@ -147,13 +154,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
fileDatabaseHistory.databaseUri) fileDatabaseHistory.databaseUri)
DatabaseFile( DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri), UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri), UriUtil.parse(fileDatabaseHistory.keyFileUri),
UriUtil.decode(fileDatabaseHistory.databaseUri), HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias), UriUtil.decode(fileDatabaseHistory.databaseUri),
fileDatabaseInfo.exists, fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.getLastModificationString(), fileDatabaseInfo.exists,
fileDatabaseInfo.getSizeString() fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
) )
} }
}, },
@@ -172,10 +180,11 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory) val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
if (returnValue > 0) { if (returnValue > 0) {
DatabaseFile( DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri), UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri), UriUtil.parse(fileDatabaseHistory.keyFileUri),
UriUtil.decode(fileDatabaseHistory.databaseUri), HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
databaseFileToDelete.databaseAlias UriUtil.decode(fileDatabaseHistory.databaseUri),
databaseFileToDelete.databaseAlias
) )
} else { } else {
null null

View File

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

View File

@@ -34,12 +34,10 @@ import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.CipherDecryptDatabase import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage import com.kunzisoft.keepass.model.CredentialStorage
@@ -398,7 +396,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} ?: deleteEncryptedDatabaseKey() } ?: deleteEncryptedDatabaseKey()
} }
} ?: throw IODatabaseException() } ?: throw UnknownDatabaseLocationException()
} ?: throw Exception("AdvancedUnlockManager not initialized") } ?: 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.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.database.element.MainCredential
open class AssignMainCredentialInDatabaseRunnable ( open class AssignMainCredentialInDatabaseRunnable (
context: Context, context: Context,
database: Database, database: Database,
protected val mDatabaseUri: Uri, protected val mDatabaseUri: Uri,
protected val mMainCredential: MainCredential) mainCredential: MainCredential,
: SaveDatabaseRunnable(context, database, true) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, true, mainCredential, challengeResponseRetriever) {
private var mBackupKey: ByteArray? = null private var mBackupKey: ByteArray? = null
@@ -40,10 +41,7 @@ open class AssignMainCredentialInDatabaseRunnable (
// Set key // Set key
try { try {
mBackupKey = ByteArray(database.masterKey.size) mBackupKey = ByteArray(database.masterKey.size)
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size) database.masterKey.copyInto(mBackupKey!!)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
} catch (e: Exception) { } catch (e: Exception) {
erase(mBackupKey) erase(mBackupKey)
setError(e) setError(e)

View File

@@ -24,7 +24,8 @@ import android.net.Uri
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
class CreateDatabaseRunnable(context: Context, class CreateDatabaseRunnable(context: Context,
@@ -33,9 +34,10 @@ class CreateDatabaseRunnable(context: Context,
private val databaseName: String, private val databaseName: String,
private val rootName: String, private val rootName: String,
private val templateGroupName: String?, private val templateGroupName: String?,
mainCredential: MainCredential, val mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private val createDatabaseResult: ((Result) -> Unit)?) private val createDatabaseResult: ((Result) -> Unit)?)
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { : AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential, challengeResponseRetriever) {
override fun onStartRun() { override fun onStartRun() {
try { try {
@@ -58,8 +60,11 @@ class CreateDatabaseRunnable(context: Context,
// Add database to recent files // Add database to recent files
if (PreferencesUtil.rememberDatabaseLocations(context)) { if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext) FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri, .addOrUpdateDatabaseUri(
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) 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 // 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.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.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.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.ProgressMessage
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK 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.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.viewmodels.ChallengeResponseViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
@@ -92,7 +97,6 @@ import java.util.*
class DatabaseTaskProvider { class DatabaseTaskProvider {
private var activity: FragmentActivity? = null private var activity: FragmentActivity? = null
private var service: Service? = null
private var context: Context private var context: Context
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
@@ -111,30 +115,80 @@ class DatabaseTaskProvider {
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
constructor(activity: FragmentActivity) { private var mChallengeResponseViewModel: ChallengeResponseViewModel? = null
constructor(activity: FragmentActivity,
challengeResponseViewModel: ChallengeResponseViewModel) {
this.activity = activity this.activity = activity
this.context = activity this.context = activity
this.intentDatabaseTask = Intent(activity.applicationContext, this.intentDatabaseTask = Intent(activity.applicationContext,
DatabaseTaskNotificationService::class.java) 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) { constructor(service: Service) {
this.service = service
this.context = service this.context = service
this.intentDatabaseTask = Intent(service.applicationContext, this.intentDatabaseTask = Intent(service.applicationContext,
DatabaseTaskNotificationService::class.java) 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 { private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) { override fun onStartAction(database: Database,
startDialog(titleId, messageId, warningId) progressMessage: ProgressMessage) {
startDialog(progressMessage)
} }
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) { override fun onUpdateAction(database: Database,
updateDialog(titleId, messageId, warningId) 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) onActionFinish?.invoke(database, actionTask, result)
// Remove the progress task // Remove the progress task
stopDialog() stopDialog()
@@ -181,9 +235,9 @@ class DatabaseTaskProvider {
} }
} }
private fun startDialog(titleId: Int? = null, private var requestChallengeListener: DatabaseTaskNotificationService.RequestChallengeListener? = null
messageId: Int? = null,
warningId: Int? = null) { private fun startDialog(progressMessage: ProgressMessage) {
activity?.let { activity -> activity?.let { activity ->
activity.lifecycleScope.launch { activity.lifecycleScope.launch {
if (progressTaskDialogFragment == null) { if (progressTaskDialogFragment == null) {
@@ -197,22 +251,17 @@ class DatabaseTaskProvider {
PROGRESS_TASK_DIALOG_TAG 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 { progressTaskDialogFragment?.apply {
titleId?.let { updateTitle(progressMessage.titleId)
updateTitle(it) updateMessage(progressMessage.messageId)
} updateWarning(progressMessage.warningId)
messageId?.let { setCancellable(progressMessage.cancelable)
updateMessage(it)
}
warningId?.let {
updateWarning(it)
}
} }
} }
@@ -226,25 +275,38 @@ class DatabaseTaskProvider {
serviceConnection = object : ServiceConnection { serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply { mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addDatabaseListener(databaseListener) addServiceListeners(this)
addDatabaseFileInfoListener(databaseInfoListener)
addActionTaskListener(actionTaskListener)
getService().checkDatabase() getService().checkDatabase()
getService().checkDatabaseInfo() getService().checkDatabaseInfo()
getService().checkAction() getService().checkAction()
} }
mChallengeResponseViewModel?.resendResponse()
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
mBinder?.removeActionTaskListener(actionTaskListener) removeServiceListeners(mBinder)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
mBinder = null 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() { private fun bindService() {
initServiceConnection() initServiceConnection()
serviceConnection?.let { serviceConnection?.let {
@@ -262,10 +324,6 @@ class DatabaseTaskProvider {
serviceConnection = null serviceConnection = null
} }
fun isBinded(): Boolean {
return mBinder != null
}
fun registerProgressTask() { fun registerProgressTask() {
stopDialog() stopDialog()
@@ -299,9 +357,7 @@ class DatabaseTaskProvider {
fun unregisterProgressTask() { fun unregisterProgressTask() {
stopDialog() stopDialog()
mBinder?.removeActionTaskListener(actionTaskListener) removeServiceListeners(mBinder)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
mBinder = null mBinder = null
unBindService() unBindService()
@@ -321,7 +377,7 @@ class DatabaseTaskProvider {
context.startService(intentDatabaseTask) context.startService(intentDatabaseTask)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e) 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, fun startDatabaseCreate(databaseUri: Uri,
mainCredential: MainCredential) { mainCredential: MainCredential
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
@@ -385,7 +442,8 @@ class DatabaseTaskProvider {
} }
fun startDatabaseAssignPassword(databaseUri: Uri, fun startDatabaseAssignPassword(databaseUri: Uri,
mainCredential: MainCredential) { mainCredential: MainCredential
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) 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.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.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.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -35,8 +36,9 @@ import com.kunzisoft.keepass.utils.UriUtil
class LoadDatabaseRunnable(private val context: Context, class LoadDatabaseRunnable(private val context: Context,
private val mDatabase: Database, private val mDatabase: Database,
private val mUri: Uri, private val mDatabaseUri: Uri,
private val mMainCredential: MainCredential, private val mMainCredential: MainCredential,
private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray,
private val mReadonly: Boolean, private val mReadonly: Boolean,
private val mCipherEncryptDatabase: CipherEncryptDatabase?, private val mCipherEncryptDatabase: CipherEncryptDatabase?,
private val mFixDuplicateUUID: Boolean, private val mFixDuplicateUUID: Boolean,
@@ -51,18 +53,21 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onActionRun() { override fun onActionRun() {
try { try {
mDatabase.loadData(mUri, mDatabase.loadData(
mMainCredential, context.contentResolver,
mReadonly, mDatabaseUri,
context.contentResolver, mMainCredential,
UriUtil.getBinaryDir(context), mChallengeResponseRetriever,
{ memoryWanted -> mReadonly,
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) UriUtil.getBinaryDir(context),
}, { memoryWanted ->
mFixDuplicateUUID, BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
progressTaskUpdater) },
mFixDuplicateUUID,
progressTaskUpdater
)
} }
catch (e: LoadDatabaseException) { catch (e: DatabaseInputException) {
setError(e) setError(e)
} }
@@ -70,8 +75,11 @@ class LoadDatabaseRunnable(private val context: Context,
// Save keyFile in app database // Save keyFile in app database
if (PreferencesUtil.rememberDatabaseLocations(context)) { if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context) FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri, .addOrUpdateDatabaseUri(
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null,
if (PreferencesUtil.rememberHardwareKey(context)) mMainCredential.hardwareKey else null,
)
} }
// Register the biometric // Register the biometric

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,13 +22,15 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.hardware.HardwareKey
abstract class ActionNodeDatabaseRunnable( abstract class ActionNodeDatabaseRunnable(
context: Context, context: Context,
database: Database, database: Database,
private val afterActionNodesFinish: AfterActionNodesFinish?, private val afterActionNodesFinish: AfterActionNodesFinish?,
save: Boolean) save: Boolean,
: SaveDatabaseRunnable(context, database, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, save, null, challengeResponseRetriever) {
/** /**
* Function do to a node action * 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.Entry
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class AddEntryRunnable constructor( class AddEntryRunnable constructor(
context: Context, context: Context,
@@ -31,8 +32,9 @@ class AddEntryRunnable constructor(
private val mNewEntry: Entry, private val mNewEntry: Entry,
private val mParent: Group, private val mParent: Group,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() { override fun nodeAction() {
mNewEntry.touch(modified = true, touchParents = true) 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.Database
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class AddGroupRunnable constructor( class AddGroupRunnable constructor(
context: Context, context: Context,
@@ -30,8 +31,9 @@ class AddGroupRunnable constructor(
private val mNewGroup: Group, private val mNewGroup: Group,
private val mParent: Group, private val mParent: Group,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() { override fun nodeAction() {
mNewGroup.touch(modified = true, touchParents = true) 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.content.Context
import android.util.Log 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.Node
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
class CopyNodesRunnable constructor( class CopyNodesRunnable constructor(
context: Context, context: Context,
@@ -33,8 +36,9 @@ class CopyNodesRunnable constructor(
private val mNodesToCopy: List<Node>, private val mNodesToCopy: List<Node>,
private val mNewParent: Group, private val mNewParent: Group,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mEntriesCopied = ArrayList<Entry>() private var mEntriesCopied = ArrayList<Entry>()

View File

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

View File

@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context import android.content.Context
import android.util.Log 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.Node
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
class MoveNodesRunnable constructor( class MoveNodesRunnable constructor(
context: Context, context: Context,
@@ -33,8 +36,9 @@ class MoveNodesRunnable constructor(
private val mNodesToMove: List<Node>, private val mNodesToMove: List<Node>,
private val mNewParent: Group, private val mNewParent: Group,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mOldParent: Group? = null 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.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateEntryRunnable constructor( class UpdateEntryRunnable constructor(
context: Context, context: Context,
@@ -31,8 +32,9 @@ class UpdateEntryRunnable constructor(
private val mOldEntry: Entry, private val mOldEntry: Entry,
private val mNewEntry: Entry, private val mNewEntry: Entry,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() { override fun nodeAction() {
if (mOldEntry.nodeId == mNewEntry.nodeId) { 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.Database
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateGroupRunnable constructor( class UpdateGroupRunnable constructor(
context: Context, context: Context,
@@ -30,8 +31,9 @@ class UpdateGroupRunnable constructor(
private val mOldGroup: Group, private val mOldGroup: Group,
private val mNewGroup: Group, private val mNewGroup: Group,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() { override fun nodeAction() {
if (mOldGroup.nodeId == mNewGroup.nodeId) { 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.merge.DatabaseKDBXMerger
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import java.io.* import java.io.*
import java.util.* import java.util.*
@@ -73,7 +71,7 @@ class Database {
var fileUri: Uri? = null var fileUri: Uri? = null
private set private set
private var mSearchHelper: SearchHelper? = null private var mSearchHelper: SearchHelper = SearchHelper()
var isReadOnly = false var isReadOnly = false
@@ -384,10 +382,14 @@ class Database {
set(masterKey) { set(masterKey) {
mDatabaseKDB?.masterKey = masterKey mDatabaseKDB?.masterKey = masterKey
mDatabaseKDBX?.masterKey = masterKey mDatabaseKDBX?.masterKey = masterKey
mDatabaseKDBX?.keyLastChanged = DateInstant()
mDatabaseKDBX?.settingsChanged = DateInstant() mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true dataModifiedSinceLastLoading = true
} }
val transformSeed: ByteArray?
get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed
var rootGroup: Group? var rootGroup: Group?
get() { get() {
mDatabaseKDB?.rootGroup?.let { mDatabaseKDB?.rootGroup?.let {
@@ -557,79 +559,28 @@ class Database {
this.dataModifiedSinceLastLoading = false this.dataModifiedSinceLastLoading = false
} }
@Throws(LoadDatabaseException::class) @Throws(DatabaseInputException::class)
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri, fun loadData(
openDatabaseKDB: (InputStream) -> DatabaseKDB, contentResolver: ContentResolver,
openDatabaseKDBX: (InputStream) -> DatabaseKDBX) { databaseUri: Uri,
var databaseInputStream: InputStream? = null mainCredential: MainCredential,
try { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
// Load Data, pass Uris as InputStreams readOnly: Boolean,
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri) cacheDirectory: File,
?: throw IOException("Database input stream cannot be retrieve") isRAMSufficient: (memoryWanted: Long) -> Boolean,
fixDuplicateUUID: Boolean,
databaseInputStream = BufferedInputStream(databaseStream) progressTaskUpdater: ProgressTaskUpdater?
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,
contentResolver: ContentResolver,
cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
// Save database URI // Save database URI
this.fileUri = uri this.fileUri = databaseUri
// Check if the file is writable // Check if the file is writable
this.isReadOnly = readOnly this.isReadOnly = readOnly
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
try { try {
// Get keyFile inputStream
mainCredential.keyFileUri?.let { keyFile ->
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
}
// Read database stream for the first time // Read database stream for the first time
readDatabaseStream(contentResolver, uri, readDatabaseStream(contentResolver, databaseUri,
{ databaseInputStream -> { databaseInputStream ->
val databaseKDB = DatabaseKDB().apply { val databaseKDB = DatabaseKDB().apply {
binaryCache.cacheDirectory = cacheDirectory binaryCache.cacheDirectory = cacheDirectory
@@ -639,12 +590,12 @@ class Database {
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
progressTaskUpdater progressTaskUpdater
) { ) {
databaseKDB.retrieveMasterKey( databaseKDB.deriveMasterKey(
mainCredential.masterPassword, contentResolver,
keyFileInputStream mainCredential
) )
} }
databaseKDB setDatabaseKDB(databaseKDB)
}, },
{ databaseInputStream -> { databaseInputStream ->
val databaseKDBX = DatabaseKDBX().apply { val databaseKDBX = DatabaseKDBX().apply {
@@ -655,23 +606,23 @@ class Database {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient) setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, openDatabase(databaseInputStream,
progressTaskUpdater) { progressTaskUpdater) {
databaseKDBX.retrieveMasterKey( databaseKDBX.deriveMasterKey(
mainCredential.masterPassword, contentResolver,
keyFileInputStream, mainCredential,
challengeResponseRetriever
) )
} }
} }
databaseKDBX setDatabaseKDBX(databaseKDBX)
} }
) )
} catch (e: FileNotFoundException) { loaded = true
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) { } catch (e: Exception) {
throw LoadDatabaseException(e) Log.e(TAG, "Unable to load the database")
if (e is DatabaseInputException)
throw e
throw DatabaseInputException(e)
} finally { } finally {
keyFileInputStream?.close()
dataModifiedSinceLastLoading = false dataModifiedSinceLastLoading = false
} }
} }
@@ -680,48 +631,44 @@ class Database {
return mDatabaseKDBX != null return mDatabaseKDBX != null
} }
@Throws(LoadDatabaseException::class) @Throws(DatabaseInputException::class)
fun mergeData(databaseToMergeUri: Uri?, fun mergeData(
databaseToMergeMainCredential: MainCredential?, contentResolver: ContentResolver,
contentResolver: ContentResolver, databaseToMergeUri: Uri?,
isRAMSufficient: (memoryWanted: Long) -> Boolean, databaseToMergeMainCredential: MainCredential?,
progressTaskUpdater: ProgressTaskUpdater?) { databaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?
) {
mDatabaseKDB?.let { mDatabaseKDB?.let {
throw IODatabaseException("Unable to merge from a database V1") throw MergeDatabaseKDBException()
} }
// New database instance to get new changes // New database instance to get new changes
val databaseToMerge = Database() val databaseToMerge = Database()
databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
try { try {
val databaseUri = databaseToMerge.fileUri val databaseUri = databaseToMerge.fileUri
if (databaseUri != null) { if (databaseUri != null) {
if (databaseToMergeMainCredential != null) { readDatabaseStream(contentResolver, databaseUri,
// Get keyFile inputStream
databaseToMergeMainCredential.keyFileUri?.let { keyFile ->
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
}
}
databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
{ databaseInputStream -> { databaseInputStream ->
val databaseToMergeKDB = DatabaseKDB() val databaseToMergeKDB = DatabaseKDB()
DatabaseInputKDB(databaseToMergeKDB) DatabaseInputKDB(databaseToMergeKDB)
.openDatabase(databaseInputStream, progressTaskUpdater) { .openDatabase(databaseInputStream, progressTaskUpdater) {
if (databaseToMergeMainCredential != null) { if (databaseToMergeMainCredential != null) {
databaseToMergeKDB.retrieveMasterKey( databaseToMergeKDB.deriveMasterKey(
databaseToMergeMainCredential.masterPassword, contentResolver,
keyFileInputStream, databaseToMergeMainCredential
) )
} else { } else {
databaseToMergeKDB.masterKey = masterKey this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
databaseToMergeKDB.copyMasterKeyFrom(thisDatabaseKDB)
}
} }
} }
databaseToMergeKDB databaseToMerge.setDatabaseKDB(databaseToMergeKDB)
}, },
{ databaseInputStream -> { databaseInputStream ->
val databaseToMergeKDBX = DatabaseKDBX() val databaseToMergeKDBX = DatabaseKDBX()
@@ -729,18 +676,22 @@ class Database {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient) setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, progressTaskUpdater) { openDatabase(databaseInputStream, progressTaskUpdater) {
if (databaseToMergeMainCredential != null) { if (databaseToMergeMainCredential != null) {
databaseToMergeKDBX.retrieveMasterKey( databaseToMergeKDBX.deriveMasterKey(
databaseToMergeMainCredential.masterPassword, contentResolver,
keyFileInputStream, databaseToMergeMainCredential,
databaseToMergeChallengeResponseRetriever
) )
} else { } else {
databaseToMergeKDBX.masterKey = masterKey this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
databaseToMergeKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
}
} }
} }
} }
databaseToMergeKDBX databaseToMerge.setDatabaseKDBX(databaseToMergeKDBX)
} }
) )
loaded = true
mDatabaseKDBX?.let { currentDatabaseKDBX -> mDatabaseKDBX?.let { currentDatabaseKDBX ->
val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply { val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply {
@@ -760,24 +711,24 @@ class Database {
} }
} }
} else { } 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) { } catch (e: Exception) {
throw LoadDatabaseException(e) Log.e(TAG, "Unable to merge the database")
if (e is DatabaseException)
throw e
throw DatabaseInputException(e)
} finally { } finally {
keyFileInputStream?.close()
databaseToMerge.clearAndClose() databaseToMerge.clearAndClose()
} }
} }
@Throws(LoadDatabaseException::class) @Throws(DatabaseInputException::class)
fun reloadData(contentResolver: ContentResolver, fun reloadData(
isRAMSufficient: (memoryWanted: Long) -> Boolean, contentResolver: ContentResolver,
progressTaskUpdater: ProgressTaskUpdater?) { isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?
) {
// Retrieve the stream from the old database URI // Retrieve the stream from the old database URI
try { try {
@@ -791,9 +742,11 @@ class Database {
} }
DatabaseInputKDB(databaseKDB) DatabaseInputKDB(databaseKDB)
.openDatabase(databaseInputStream, progressTaskUpdater) { .openDatabase(databaseInputStream, progressTaskUpdater) {
databaseKDB.masterKey = masterKey this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
databaseKDB.copyMasterKeyFrom(thisDatabaseKDB)
}
} }
databaseKDB setDatabaseKDB(databaseKDB)
}, },
{ databaseInputStream -> { databaseInputStream ->
val databaseKDBX = DatabaseKDBX() val databaseKDBX = DatabaseKDBX()
@@ -803,26 +756,144 @@ class Database {
DatabaseInputKDBX(databaseKDBX).apply { DatabaseInputKDBX(databaseKDBX).apply {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient) setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, progressTaskUpdater) { openDatabase(databaseInputStream, progressTaskUpdater) {
databaseKDBX.masterKey = masterKey this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
databaseKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
}
} }
} }
databaseKDBX setDatabaseKDBX(databaseKDBX)
} }
) )
loaded = true
} else { } 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) { } catch (e: Exception) {
throw LoadDatabaseException(e) Log.e(TAG, "Unable to reload the database")
if (e is DatabaseException)
throw e
throw DatabaseInputException(e)
} finally { } finally {
dataModifiedSinceLastLoading = false 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 { fun groupIsInRecycleBin(group: Group): Boolean {
val groupKDB = group.groupKDB val groupKDB = group.groupKDB
val groupKDBX = group.groupKDBX val groupKDBX = group.groupKDBX
@@ -845,13 +916,13 @@ class Database {
fun createVirtualGroupFromSearch(searchParameters: SearchParameters, fun createVirtualGroupFromSearch(searchParameters: SearchParameters,
fromGroup: NodeId<*>? = null, fromGroup: NodeId<*>? = null,
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper.createVirtualGroupWithSearchResult(this,
searchParameters, fromGroup, max) searchParameters, fromGroup, max)
} }
fun createVirtualGroupFromSearchInfo(searchInfoString: String, fun createVirtualGroupFromSearchInfo(searchInfoString: String,
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper.createVirtualGroupWithSearchResult(this,
SearchParameters().apply { SearchParameters().apply {
searchQuery = searchInfoString searchQuery = searchInfoString
searchInTitles = true searchInTitles = true
@@ -908,40 +979,6 @@ class Database {
dataModifiedSinceLastLoading = true 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) { fun clearIndexesAndBinaries(filesDirectory: File? = null) {
this.mDatabaseKDB?.clearIndexes() this.mDatabaseKDB?.clearIndexes()
this.mDatabaseKDBX?.clearIndexes() this.mDatabaseKDBX?.clearIndexes()
@@ -987,20 +1024,13 @@ class Database {
} }
fun validatePasswordEncoding(mainCredential: MainCredential): Boolean { fun validatePasswordEncoding(mainCredential: MainCredential): Boolean {
val password = mainCredential.masterPassword val password = mainCredential.password
val containsKeyFile = mainCredential.keyFileUri != null val containsKeyFile = mainCredential.keyFileUri != null
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile) return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile) ?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
?: false ?: false
} }
@Throws(IOException::class)
fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.keyLastChanged = DateInstant()
}
fun rootCanContainsEntry(): Boolean { fun rootCanContainsEntry(): Boolean {
return mDatabaseKDB?.rootCanContainsEntry() ?: mDatabaseKDBX?.rootCanContainsEntry() ?: false 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 package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.encrypt.aes.AESTransformer import com.kunzisoft.encrypt.aes.AESTransformer
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory 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.binary.BinaryData
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
@@ -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.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.nio.charset.Charset
import java.util.* import java.util.*
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() { class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
@@ -56,8 +60,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
KdfFactory.aesKdf KdfFactory.aesKdf
) )
override val passwordEncoding: String override val passwordEncoding: Charset
get() = "ISO-8859-1" get() = Charsets.ISO_8859_1
override var numberKeyEncryptionRounds = 300L override var numberKeyEncryptionRounds = 300L
@@ -116,20 +120,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return newId 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) @Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) { fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) {
// Encrypt the master key a few times to make brute-force key-search harder // Encrypt the master key a few times to make brute-force key-search harder
@@ -138,6 +128,41 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
finalKey = HashManager.hashSha256(masterSeed, transformedKey) 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 { override fun createGroup(): GroupKDB {
return GroupKDB() return GroupKDB()
} }

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import android.content.res.Resources import android.content.res.Resources
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
@@ -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.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.CustomData import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -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.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible 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_31
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.longTo8Bytes 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.IOException
import java.io.InputStream import java.nio.charset.Charset
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.util.* import java.util.*
import javax.crypto.Mac import javax.crypto.Mac
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import kotlin.collections.HashSet
import kotlin.math.min import kotlin.math.min
class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> { 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 var hmacKey: ByteArray? = null
private set 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 { fun getMinKdbxVersion(): UnsignedInt {
val entryHandler = EntryOperationHandler() val entryHandler = EntryOperationHandler()
val groupHandler = GroupOperationHandler() val groupHandler = GroupOperationHandler()
@@ -364,8 +432,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
kdfEngine.setParallelism(kdfParameters!!, parallelism) kdfEngine.setParallelism(kdfParameters!!, parallelism)
} }
override val passwordEncoding: String override val passwordEncoding: Charset
get() = "UTF-8" get() = Charsets.UTF_8
private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? { private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? {
if (groupUUID == UUID_ZERO) if (groupUUID == UUID_ZERO)
@@ -528,22 +596,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return mFieldReferenceEngine.compile(textReference, recursionLevel) 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) @Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray) { fun makeFinalKey(masterSeed: ByteArray) {
@@ -615,115 +667,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return ret 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 { override fun newGroupId(): NodeIdUUID {
var newId: NodeIdUUID var newId: NodeIdUUID
do { 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_ITEMS = 10 // -1 unlimited
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -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 const val BASE_64_FLAG = Base64.NO_WRAP
} }
} }

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import android.util.Log import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.binary.AttachmentPool 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.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException 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.InputStream
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.util.* import java.util.*
abstract class DatabaseVersioned< abstract class DatabaseVersioned<
@@ -46,7 +43,6 @@ abstract class DatabaseVersioned<
Entry : EntryVersioned<GroupId, EntryId, Group, Entry> Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
> { > {
// Algorithm used to encrypt the database // Algorithm used to encrypt the database
abstract var encryptionAlgorithm: EncryptionAlgorithm abstract var encryptionAlgorithm: EncryptionAlgorithm
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm> abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
@@ -55,11 +51,12 @@ abstract class DatabaseVersioned<
abstract val kdfAvailableList: List<KdfEngine> abstract val kdfAvailableList: List<KdfEngine>
abstract var numberKeyEncryptionRounds: Long abstract var numberKeyEncryptionRounds: Long
protected abstract val passwordEncoding: String abstract val passwordEncoding: Charset
var masterKey = ByteArray(32) var masterKey = ByteArray(32)
var finalKey: ByteArray? = null var finalKey: ByteArray? = null
protected set protected set
var transformSeed: ByteArray? = null
abstract val version: String abstract val version: String
abstract val defaultFileExtension: String abstract val defaultFileExtension: String
@@ -91,58 +88,6 @@ abstract class DatabaseVersioned<
return getGroupIndexes().filter { it != rootGroup } 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? { protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
return null return null
} }
@@ -158,20 +103,25 @@ abstract class DatabaseVersioned<
val bKey: ByteArray val bKey: ByteArray
try { try {
bKey = password.toByteArray(charset(encoding)) bKey = password.toByteArray(encoding)
} catch (e: UnsupportedEncodingException) { } catch (e: UnsupportedEncodingException) {
return false return false
} }
val reEncoded: String val reEncoded: String
try { try {
reEncoded = String(bKey, charset(encoding)) reEncoded = String(bKey, encoding)
} catch (e: UnsupportedEncodingException) { } catch (e: UnsupportedEncodingException) {
return false return false
} }
return password == reEncoded return password == reEncoded
} }
fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned<GroupId, EntryId, Group, Entry>) {
this.masterKey = databaseVersioned.masterKey
this.transformSeed = databaseVersioned.transformSeed
}
/* /*
* ------------------------------------- * -------------------------------------
* Node Creation * Node Creation

View File

@@ -24,128 +24,172 @@ import androidx.annotation.StringRes
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import java.io.PrintStream
import java.io.PrintWriter
abstract class DatabaseException : Exception { abstract class DatabaseException : Exception {
var innerMessage: String? = null
abstract var errorId: Int abstract var errorId: Int
var parameters: (Array<out String>)? = null var parameters: (Array<out String>)? = null
var mThrowable: Throwable? = null
constructor() : super() constructor() : super()
constructor(message: String) : super(message) constructor(message: String) : super(message)
constructor(message: String, throwable: Throwable) : super(message, throwable) constructor(message: String, throwable: Throwable) {
constructor(throwable: Throwable) : super(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 { fun getLocalizedMessage(resources: Resources): String {
parameters?.let { val throwable = mThrowable
return resources.getString(errorId, *it) if (throwable is DatabaseException)
} ?: return resources.getString(errorId) 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 { class FileNotFoundDatabaseException : DatabaseInputException() {
@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 {
@StringRes @StringRes
override var errorId: Int = R.string.file_not_found_content 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 @StringRes
override var errorId: Int = R.string.invalid_algorithm override var errorId: Int = R.string.invalid_algorithm
constructor() : super() constructor() : super()
constructor(exception: Throwable) : super(exception) constructor(exception: Throwable) : super(exception)
} }
class DuplicateUuidDatabaseException: LoadDatabaseException { class UnknownDatabaseLocationException : DatabaseException() {
@StringRes @StringRes
override var errorId: Int = R.string.invalid_db_same_uuid override var errorId: Int = R.string.error_location_unknown
constructor(type: Type, uuid: NodeId<*>) : super() {
parameters = arrayOf(type.name, uuid.toString())
}
constructor(exception: Throwable) : super(exception)
} }
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 @StringRes
override var errorId: Int = R.string.error_load_database override var errorId: Int = R.string.error_load_database
constructor() : super() constructor() : super()
constructor(string: String) : super(string) 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 { open class DatabaseOutputException : DatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.error_save_database 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 android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned 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 com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.InputStream 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. * 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, abstract fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)): D assignMasterKey: (() -> Unit)): D

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -96,6 +96,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.remember_keyfile_locations_default)) 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 { fun automaticallyFocusSearch(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.auto_focus_search_key), 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)) 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 { fun isAdvancedUnlockEnable(context: Context): Boolean {
return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context) 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.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.database.element.Database 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.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.showActionErrorIfNeeded

View File

@@ -24,15 +24,18 @@ import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.Button
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import java.lang.Exception import kotlinx.coroutines.launch
open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { open class ProgressTaskDialogFragment : DialogFragment() {
@StringRes @StringRes
private var title = UNDEFINED private var title = UNDEFINED
@@ -40,10 +43,12 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
private var message = UNDEFINED private var message = UNDEFINED
@StringRes @StringRes
private var warning = UNDEFINED private var warning = UNDEFINED
private var cancellable: (() -> Unit)? = null
private var titleView: TextView? = null private var titleView: TextView? = null
private var messageView: TextView? = null private var messageView: TextView? = null
private var warningView: TextView? = null private var warningView: TextView? = null
private var cancelButton: Button? = null
private var progressView: ProgressBar? = null private var progressView: ProgressBar? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -63,11 +68,13 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
titleView = root.findViewById(R.id.progress_dialog_title) titleView = root.findViewById(R.id.progress_dialog_title)
messageView = root.findViewById(R.id.progress_dialog_message) messageView = root.findViewById(R.id.progress_dialog_message)
warningView = root.findViewById(R.id.progress_dialog_warning) warningView = root.findViewById(R.id.progress_dialog_warning)
cancelButton = root.findViewById(R.id.progress_dialog_cancel)
progressView = root.findViewById(R.id.progress_dialog_bar) progressView = root.findViewById(R.id.progress_dialog_bar)
updateTitle(title) updateTitle(title)
updateMessage(message) updateMessage(message)
updateWarning(warning) updateWarning(warning)
setCancellable(cancellable)
isCancelable = false isCancelable = false
@@ -84,7 +91,7 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
} }
private fun updateView(textView: TextView?, @StringRes resId: Int) { private fun updateView(textView: TextView?, @StringRes resId: Int) {
activity?.runOnUiThread { activity?.lifecycleScope?.launch {
if (resId == UNDEFINED) { if (resId == UNDEFINED) {
textView?.visibility = View.GONE textView?.visibility = View.GONE
} else { } else {
@@ -94,21 +101,35 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
} }
} }
fun updateTitle(@StringRes resId: Int) { private fun updateCancelable() {
this.title = resId activity?.lifecycleScope?.launch {
cancelButton?.isVisible = cancellable != null
cancelButton?.setOnClickListener {
cancellable?.invoke()
}
}
}
fun updateTitle(@StringRes resId: Int?) {
this.title = resId ?: UNDEFINED
updateView(titleView, title) updateView(titleView, title)
} }
override fun updateMessage(@StringRes resId: Int) { fun updateMessage(@StringRes resId: Int?) {
this.message = resId this.message = resId ?: UNDEFINED
updateView(messageView, message) updateView(messageView, message)
} }
fun updateWarning(@StringRes resId: Int) { fun updateWarning(@StringRes resId: Int?) {
this.warning = resId this.warning = resId ?: UNDEFINED
updateView(warningView, warning) updateView(warningView, warning)
} }
fun setCancellable(cancellable: (() -> Unit)?) {
this.cancellable = cancellable
updateCancelable()
}
companion object { companion object {
private val TAG = ProgressTaskDialogFragment::class.java.simpleName private val TAG = ProgressTaskDialogFragment::class.java.simpleName
const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment" 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() = inline fun <reified T : Enum<T>> Parcel.readEnum() =
readString()?.let { enumValueOf<T>(it) } 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 { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
val clipData = intent.clipData val clipData = intent?.clipData
if (clipData != null) { if (clipData != null) {
if (clipData.description.label == key) { if (clipData.description.label == key) {
if (clipData.itemCount == 1) { if (clipData.itemCount == 1) {
@@ -242,7 +242,7 @@ object UriUtil {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
return intent.getParcelableExtra(key) return intent?.getParcelableExtra(key)
} }
return null return null
} }
@@ -269,11 +269,15 @@ object UriUtil {
fun contributingUser(context: Context): Boolean { fun contributingUser(context: Context): Boolean {
return (Education.isEducationScreenReclickedPerformed(context) 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 { try {
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES) context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
Education.setEducationScreenReclickedPerformed(context) Education.setEducationScreenReclickedPerformed(context)
@@ -295,10 +299,11 @@ object UriUtil {
} }
try { try {
if (launchIntent == null) { if (launchIntent == null) {
// TODO F-Droid
context.startActivity( context.startActivity(
Intent(Intent.ACTION_VIEW) Intent(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .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 { } else {
context.startActivity(launchIntent) 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.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.CredentialStorage 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, class MainCredentialView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0) defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) { : FrameLayout(context, attrs, defStyle) {
private var passwordTextView: EditText
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxPasswordView: CompoundButton private var checkboxPasswordView: CompoundButton
private var passwordTextView: EditText
private var checkboxKeyFileView: CompoundButton private var checkboxKeyFileView: CompoundButton
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxHardwareView: CompoundButton
private var hardwareKeySelectionView: HardwareKeySelectionView
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onValidateListener: (() -> Unit)? = null var onValidateListener: (() -> Unit)? = null
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD 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? val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_main_credentials, this) 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) 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 { val onEditorActionListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == EditorInfo.IME_ACTION_DONE) { if (actionId == EditorInfo.IME_ACTION_DONE) {
onValidateListener?.invoke() validateCredential()
return true return true
} }
return false return false
@@ -91,7 +98,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
if (keyEvent.action == KeyEvent.ACTION_DOWN if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER && keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
) { ) {
onValidateListener?.invoke() validateCredential()
handled = true handled = true
} }
handled handled
@@ -100,10 +107,30 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
checkboxPasswordView.setOnCheckedChangeListener { view, checked -> checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
onPasswordChecked?.onCheckedChanged(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)
}
hardwareKeySelectionView.selectionListener = { _ ->
checkboxHardwareView.isChecked = true
}
} }
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) { fun validateCredential() {
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper) onValidateListener?.invoke()
} }
fun populatePasswordTextView(text: String?) { 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()) { if (uri == null || uri.toString().isEmpty()) {
keyFileSelectionView.uri = null keyFileSelectionView.uri = null
if (checkboxKeyFileView.isChecked) 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 { fun isFill(): Boolean {
return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked return checkboxPasswordView.isChecked
|| (checkboxKeyFileView.isChecked && keyFileSelectionView.uri != null)
|| (checkboxHardwareView.isChecked && hardwareKeySelectionView.hardwareKey != null)
} }
fun getMainCredential(): MainCredential { fun getMainCredential(): MainCredential {
return MainCredential().apply { return MainCredential().apply {
this.masterPassword = if (checkboxPasswordView.isChecked) this.password = if (checkboxPasswordView.isChecked)
passwordTextView.text?.toString() else null passwordTextView.text?.toString() else null
this.keyFileUri = if (checkboxKeyFileView.isChecked) this.keyFileUri = if (checkboxKeyFileView.isChecked)
keyFileSelectionView.uri else null 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 // TODO HARDWARE_KEY
return when (mCredentialStorage) { return when (mCredentialStorage) {
CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked
CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked CredentialStorage.KEY_FILE -> false
CredentialStorage.HARDWARE_KEY -> false CredentialStorage.HARDWARE_KEY -> false
} }
} }

View File

@@ -68,7 +68,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE) setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener { label, value -> setCopyButtonClickListener { label, value ->
mOnCopyActionClickListener mOnCopyActionClickListener
?.invoke(Field(label, ProtectedString(false, value))) ?.invoke(Field(label, ProtectedString(true, value)))
} }
} else { } else {
setCopyButtonState(TextFieldView.ButtonState.GONE) 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.App
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.app.database.IOActionTask import com.kunzisoft.keepass.app.database.IOActionTask
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.DatabaseFile import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
@@ -72,8 +73,12 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
} }
} }
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?) { fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?, hardwareKey: HardwareKey?) {
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseUri, keyFileUri) { databaseFileAdded -> mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(
databaseUri,
keyFileUri,
hardwareKey
) { databaseFileAdded ->
databaseFileAdded?.let { _ -> databaseFileAdded?.let { _ ->
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply { databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
this.databaseFileAction = DatabaseFileAction.ADD this.databaseFileAction = DatabaseFileAction.ADD
@@ -96,6 +101,7 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
.find { it.databaseUri == databaseFileUpdated.databaseUri } .find { it.databaseUri == databaseFileUpdated.databaseUri }
?.apply { ?.apply {
keyFileUri = databaseFileUpdated.keyFileUri keyFileUri = databaseFileUpdated.keyFileUri
hardwareKey = databaseFileUpdated.hardwareKey
databaseAlias = databaseFileUpdated.databaseAlias databaseAlias = databaseFileUpdated.databaseAlias
databaseFileExists = databaseFileUpdated.databaseFileExists databaseFileExists = databaseFileUpdated.databaseFileExists
databaseLastModified = databaseFileUpdated.databaseLastModified 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_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar" app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -118,6 +118,11 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> 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 <TextView
android:id="@+id/activity_about_contribution_text" android:id="@+id/activity_about_contribution_text"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
@@ -179,4 +184,5 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> <include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,165 +17,174 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. 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:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:filterTouchesWhenObscured="true" android:filterTouchesWhenObscured="true"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/app_bar" android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_parallax_height" android:layout_height="0dp"
android:background="?attr/colorPrimary"> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.CollapsingToolbarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_layout" android:id="@+id/app_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="@dimen/toolbar_parallax_height"
app:contentScrim="?attr/colorPrimary" android:background="?attr/colorPrimary">
app:expandedTitleGravity="center_horizontal|bottom"
app:expandedTitleMarginStart="@dimen/default_margin"
app:expandedTitleMarginEnd="@dimen/default_margin"
app:expandedTitleMarginBottom="24dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<FrameLayout <com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/title_block" android:id="@+id/toolbar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_collapseMode="parallax" app:contentScrim="?attr/colorPrimary"
android:orientation="vertical" app:expandedTitleGravity="center_horizontal|bottom"
android:background="@drawable/background_repeat" app:expandedTitleMarginStart="@dimen/default_margin"
android:gravity="center" app:expandedTitleMarginEnd="@dimen/default_margin"
android:paddingBottom="12dp" app:expandedTitleMarginBottom="24dp"
style="@style/KeepassDXStyle.TextAppearance.Default"> app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_icon" <FrameLayout
android:id="@+id/title_block"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_collapseMode="parallax"
android:orientation="vertical"
android:background="@drawable/background_repeat"
android:gravity="center"
android:paddingBottom="12dp"
style="@style/KeepassDXStyle.TextAppearance.Default">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:src="@drawable/ic_blank_32dp"
style="@style/KeepassDXStyle.Icon"
android:layout_gravity="center"/>
</FrameLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="?attr/toolbarAppearance"
app:layout_collapseMode="pin"
tools:targetApi="lollipop">
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/entry_progress"
android:visibility="gone"
android:indeterminate="false"
app:indicatorColor="?attr/colorAccent"
android:progress="10"
android:max="30"
android:layout_gravity="bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/entry_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarStyle="insideOverlay"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/history_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
android:background="?attr/colorAccent"
android:padding="12dp"
android:textColor="?attr/colorOnAccentColor"
android:text="@string/entry_history"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/entry_tags_list_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="4dp" app:layout_constraintStart_toStartOf="parent"
android:src="@drawable/ic_blank_32dp" app:layout_constraintEnd_toEndOf="parent"
style="@style/KeepassDXStyle.Icon" android:paddingTop="12dp"
android:layout_gravity="center"/> android:paddingStart="5dp"
</FrameLayout> android:paddingLeft="5dp"
<androidx.appcompat.widget.Toolbar android:paddingEnd="5dp"
android:id="@+id/toolbar" android:paddingRight="5dp"
android:layout_width="match_parent" android:layout_gravity="center"
android:layout_height="?attr/actionBarSize" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:theme="?attr/toolbarAppearance" android:orientation="horizontal"
app:layout_collapseMode="pin" app:layout_constraintTop_toBottomOf="@+id/history_container"/>
tools:targetApi="lollipop">
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.progressindicator.LinearProgressIndicator <androidx.fragment.app.FragmentContainerView
android:id="@+id/entry_progress" android:id="@+id/entry_content"
android:visibility="gone" android:name="com.kunzisoft.keepass.activities.fragments.EntryFragment"
android:indeterminate="false" android:layout_width="0dp"
app:indicatorColor="?attr/colorAccent" android:layout_height="wrap_content"
android:progress="10" app:layout_constraintWidth_percent="@dimen/content_percent"
android:max="30" app:layout_constraintTop_toBottomOf="@+id/entry_tags_list_view"
android:layout_gravity="bottom" app:layout_constraintStart_toStartOf="parent"
android:layout_width="match_parent" app:layout_constraintEnd_toEndOf="parent"/>
android:layout_height="wrap_content" /> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout> </androidx.core.widget.NestedScrollView>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView <com.google.android.material.tabs.TabLayout
android:id="@+id/entry_scroll" android:id="@+id/entry_content_tab"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:scrollbarStyle="insideOverlay" android:minWidth="120dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> android:layout_gravity="bottom|center_horizontal"
android:background="?attr/cardBackgroundTransparentColor"
app:tabIconTint="?android:attr/textColor"
app:tabMode="fixed">
<androidx.constraintlayout.widget.ConstraintLayout <com.google.android.material.tabs.TabItem
android:layout_width="match_parent" android:id="@+id/entry_content_tab_main"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/history_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
android:background="?attr/colorAccent"
android:padding="12dp"
android:textColor="?attr/colorOnAccentColor"
android:text="@string/entry_history"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/entry_tags_list_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent" android:icon="@drawable/ic_view_list_white_24dp" />
app:layout_constraintEnd_toEndOf="parent"
android:paddingTop="12dp"
android:paddingStart="5dp"
android:paddingLeft="5dp"
android:paddingEnd="5dp"
android:paddingRight="5dp"
android:layout_gravity="center"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/history_container"/>
<androidx.fragment.app.FragmentContainerView <com.google.android.material.tabs.TabItem
android:id="@+id/entry_content" android:id="@+id/entry_content_tab_advanced"
android:name="com.kunzisoft.keepass.activities.fragments.EntryFragment" android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintWidth_percent="@dimen/content_percent" android:icon="@drawable/ic_time_white_24dp" />
app:layout_constraintTop_toBottomOf="@+id/entry_tags_list_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </com.google.android.material.tabs.TabLayout>
<com.google.android.material.tabs.TabLayout <FrameLayout
android:id="@+id/entry_content_tab" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="match_parent">
android:layout_height="wrap_content" <ProgressBar
android:minWidth="120dp" android:id="@+id/loading"
android:layout_gravity="bottom|center_horizontal" android:layout_width="wrap_content"
android:background="?attr/cardBackgroundTransparentColor" android:layout_height="wrap_content"
app:tabIconTint="?android:attr/textColor" android:layout_gravity="center"
app:tabMode="fixed"> android:indeterminate="true" />
</FrameLayout>
<com.google.android.material.tabs.TabItem <include
android:id="@+id/entry_content_tab_main" layout="@layout/view_button_lock"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:icon="@drawable/ic_view_list_white_24dp" /> android:layout_gravity="start|bottom" />
<com.google.android.material.tabs.TabItem </androidx.coordinatorlayout.widget.CoordinatorLayout>
android:id="@+id/entry_content_tab_advanced"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_time_white_24dp" />
</com.google.android.material.tabs.TabLayout> <include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
<include
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. 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:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -28,6 +28,7 @@
tools:targetApi="o"> tools:targetApi="o">
<com.kunzisoft.keepass.view.SpecialModeView <com.kunzisoft.keepass.view.SpecialModeView
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/special_mode_view" android:id="@+id/special_mode_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -36,11 +37,11 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/activity_password_coordinator_layout" android:id="@+id/activity_password_coordinator_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:background="@drawable/background_repeat" android:background="@drawable/background_repeat"
android:backgroundTint="?android:attr/textColor" android:backgroundTint="?android:attr/textColor"
android:layout_below="@+id/special_mode_view" app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
android:layout_above="@+id/activity_password_footer"> app:layout_constraintBottom_toTopOf="@+id/activity_password_footer">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar" android:id="@+id/app_bar"
@@ -58,7 +59,7 @@
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:minHeight="144dp" android:minHeight="106dp"
android:layout_marginTop="?attr/actionBarSize" android:layout_marginTop="?attr/actionBarSize"
android:background="?attr/colorPrimary"> android:background="?attr/colorPrimary">
<LinearLayout <LinearLayout
@@ -156,7 +157,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:layout_alignParentBottom="true"> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
<LinearLayout <LinearLayout
android:id="@+id/activity_password_info_container" android:id="@+id/activity_password_info_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -193,4 +194,5 @@
android:text="@string/menu_open" /> android:text="@string/menu_open" />
</LinearLayout> </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 You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. 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:android="http://schemas.android.com/apk/res/android"
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:filterTouchesWhenObscured="true" android:filterTouchesWhenObscured="true"
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
android:fitsSystemWindows="true"> 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 <LinearLayout
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -46,5 +53,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start|bottom" /> android:layout_gravity="start|bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</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" android:layout_marginEnd="20dp"
style="@style/KeepassDXStyle.TextAppearance.Warning"/> 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 <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_dialog_bar" android:id="@+id/progress_dialog_bar"
app:indicatorColor="?attr/colorAccent" app:indicatorColor="?attr/colorAccent"

View File

@@ -20,6 +20,7 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:filterTouchesWhenObscured="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <LinearLayout
@@ -115,7 +116,7 @@
android:orientation="vertical"> android:orientation="vertical">
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox" android:id="@+id/keyfile_checkbox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/entry_keyfile"/> android:text="@string/entry_keyfile"/>
@@ -126,9 +127,41 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="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" /> app:layout_constraintEnd_toEndOf="parent" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </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> </LinearLayout>
</ScrollView> </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"> android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox" android:id="@+id/keyfile_checkbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignTop="@+id/keyfile_selection" android:layout_alignTop="@+id/keyfile_selection"
@@ -76,9 +76,35 @@
android:id="@+id/keyfile_selection" android:id="@+id/keyfile_selection"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="48dp" android:layout_toEndOf="@+id/keyfile_checkbox"
android:layout_toRightOf="@+id/keyfile_checkox" android:layout_toRightOf="@+id/keyfile_checkbox"
android:layout_toEndOf="@+id/keyfile_checkox" 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:importantForAccessibility="no"
android:importantForAutofill="no" /> android:importantForAutofill="no" />
</RelativeLayout> </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="select_to_copy">اختر لنسخ %1$s إلى الحافظة</string>
<string name="retrieving_db_key">يجلب مفتاح قاعدة البيانات…</string> <string name="retrieving_db_key">يجلب مفتاح قاعدة البيانات…</string>
<string name="default_checkbox">استخدامها كقاعدة بيانات افتراضية</string> <string name="default_checkbox">استخدامها كقاعدة بيانات افتراضية</string>
<string name="html_about_licence">KeePassDX © %1$d كونزيسوفت <strong>مفتوح المصدر</strong> و <strong>بدون اعلانات</strong>. <string name="html_about_licence">KeePassDX © %1$d كونزيسوفت &lt;strong&gt;مفتوح المصدر&lt;/strong&gt; و &lt;strong&gt;بدون اعلانات&lt;/strong&gt;.
\n يوزع كما هو، بدون ضمان, تحت ترخيص <strong>GPLv3</strong>.</string> \n يوزع كما هو، بدون ضمان, تحت ترخيص &lt;strong&gt;GPLv3&lt;/strong&gt;.</string>
<string name="entry_accessed">نُفذ إليه</string> <string name="entry_accessed">نُفذ إليه</string>
<string name="entry_expires">تنتهي صلاحيته في</string> <string name="entry_expires">تنتهي صلاحيته في</string>
<string name="entry_keyfile">ملف المفتاح</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_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_summary">Ajudeu a augmentar lestabilitat i la seguretat i a crear més funcionalitats.</string>
<string name="education_donation_title">Participació</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_buy_pro">En comprar la versió &lt;strong&gt;professional&lt;/strong&gt;,</string>
<string name="html_text_dev_feature_contibute">En <strong>col·laborar-hi</strong>,</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_keyfile_checkbox">Casella del fitxer de la clau</string>
<string name="content_description_password_checkbox">Casella de la contrasenya</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_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_credentials_information">Informació de les dades daccés</string>
<string name="content_description_add_item">Afegeix un element</string> <string name="content_description_add_item">Afegeix un element</string>
<string name="education_lock_title">Bloca la base de dades</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="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="icon_section_standard">Estàndard</string>
<string name="show_uuid_title">Mostra lUUID</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_sort_summary">Vyberte řazení položek a skupin.</string>
<string name="education_donation_title">Zapojit se</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="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_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 <strong>vizuálnímu stylu</strong> a hlavně pomůžete <strong>uskutečnění komunitních projektů.</strong></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_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_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">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_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_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_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> <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_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="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="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>. <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í <strong>GPLv3</strong>, bez jakékoli záruky.</string> \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 <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_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="error_create_database">Nepodařilo se vytvořit soubor databáze.</string>
<string name="entry_add_attachment">Přidat přílohu</string> <string name="entry_add_attachment">Přidat přílohu</string>
<string name="discard">Zahodit</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_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_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_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">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_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> <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-array>
<string name="warning_empty_password">Soll das Entsperren ohne Passwort wirklich möglich sein\?</string> <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="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_title">Generierte Passwortlänge</string>
<string name="password_size_summary">Legt die Standardlänge des generierten Passworts fest</string> <string name="password_size_summary">Legt die Standardlänge des generierten Passworts fest</string>
<string name="clipboard_notifications_title">Zwischenablage-Benachrichtigung</string> <string name="clipboard_notifications_title">Zwischenablage-Benachrichtigung</string>
@@ -162,7 +162,7 @@
<string name="path">Pfad</string> <string name="path">Pfad</string>
<string name="file_name">Dateiname</string> <string name="file_name">Dateiname</string>
<string name="unavailable_feature_text">Dieses Feature konnte nicht gestartet werden.</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="advanced_unlock">Moderne Entsperrung</string>
<string name="biometric_unlock_enable_title">Biometrische Entsperrung</string> <string name="biometric_unlock_enable_title">Biometrische Entsperrung</string>
<string name="lock">Sperren</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="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_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_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_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">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> <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_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_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_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="download">Download</string>
<string name="contribute">Unterstützen</string> <string name="contribute">Unterstützen</string>
<string name="icon_pack_choose_title">Symbolpaket</string> <string name="icon_pack_choose_title">Symbolpaket</string>
@@ -285,7 +285,7 @@
\n„Schreibgeschützt“ verhindert unbeabsichtigte Änderungen an der Datenbank. \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> \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="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="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_title">Benutzernamen anzeigen</string>
<string name="list_entries_show_username_summary">Benutzernamen in Eintragslisten 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_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_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_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_theme_title">Tastaturdesign</string>
<string name="keyboard_keys_category">Tasten</string> <string name="keyboard_keys_category">Tasten</string>
<string name="keyboard_key_vibrate_title">Vibrierende Tastendrücke</string> <string name="keyboard_key_vibrate_title">Vibrierende Tastendrücke</string>
<string name="keyboard_key_sound_title">Hörbare Tastendrücke</string> <string name="keyboard_key_sound_title">Hörbare Tastendrücke</string>
<string name="selection_mode">Auswahlmodus</string> <string name="selection_mode">Auswahlmodus</string>
<string name="remember_database_locations_title">Datenbank-Speicherorte merken</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_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_title">Zuletzt verwendete Dateien anzeigen</string>
<string name="show_recent_files_summary">Speicherort zuletzt verwendeter Datenbanken 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> <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_background">Hintergrund</string>
<string name="content_description_update_from_list">Aktualisieren</string> <string name="content_description_update_from_list">Aktualisieren</string>
<string name="content_description_keyboard_close_fields">Felder schließen</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="menu_advanced_unlock_settings">Modernes Entsperren</string>
<string name="biometric">Biometrisch</string> <string name="biometric">Biometrisch</string>
<string name="enable">Aktivieren</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="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="database_opened">Datenbank geöffnet</string>
<string name="clipboard_explanation_summary">Eintragsfelder mithilfe der Zwischenablage des Geräts kopieren</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_title">Datenkompression</string>
<string name="database_data_compression_summary">Datenkompression reduziert die Datenbankgröße</string> <string name="database_data_compression_summary">Datenkompression reduziert die Datenbankgröße</string>
<string name="max_history_items_title">Maximale Anzahl</string> <string name="max_history_items_title">Maximale Anzahl</string>
@@ -416,8 +416,8 @@
<string name="entry_attachments">Anhänge</string> <string name="entry_attachments">Anhänge</string>
<string name="menu_restore_entry_history">Historie wiederherstellen</string> <string name="menu_restore_entry_history">Historie wiederherstellen</string>
<string name="menu_delete_entry_history">Historie löschen</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_title">Automatische Tastenaktion</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_summary">Nach dem Drücken einer Feldtaste automatisch die Eingabetaste ausführen</string>
<string name="download_attachment">%1$s herunterladen</string> <string name="download_attachment">%1$s herunterladen</string>
<string name="download_initialization">Initialisieren </string> <string name="download_initialization">Initialisieren </string>
<string name="download_progression">Fortschritt: %1$d%%</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="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_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="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="entry_add_attachment">Anhang hinzufügen</string>
<string name="discard">Verwerfen</string> <string name="discard">Verwerfen</string>
<string name="discard_changes">Änderungen 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="content_description_add_item">Element hinzufügen</string>
<string name="filter">Filter</string> <string name="filter">Filter</string>
<string name="keyboard_change">Tastatur wechseln</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_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_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> <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_unlock_enable_title">Geräteanmeldedaten entsperren</string>
<string name="device_credential">Geräteanmeldedaten</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="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_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_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> <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="character_count">Anzahl der Zeichen: %1$d</string>
<string name="exclude_ambiguous_chars">Mehrdeutige Zeichen ausschließen</string> <string name="exclude_ambiguous_chars">Mehrdeutige Zeichen ausschließen</string>
<string name="title_case">Groß-/Kleinschreibung des Titels</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> </resources>

View File

@@ -267,7 +267,7 @@
\nΤο \"Προστατευμένο από εγγραφή\" αποτρέπει τυχόν μη επιθυμητές αλλαγές στη βάση δεδομένων. \nΤο \"Προστατευμένο από εγγραφή\" αποτρέπει τυχόν μη επιθυμητές αλλαγές στη βάση δεδομένων.
\nΤο \"Τροποποιητικό\" σάς επιτρέπει να προσθέσετε, να διαγράψετε ή να τροποποιήσετε όλα τα στοιχεία όπως επιθυμείτε.</string> \nΤο \"Τροποποιητικό\" σάς επιτρέπει να προσθέσετε, να διαγράψετε ή να τροποποιήσετε όλα τα στοιχεία όπως επιθυμείτε.</string>
<string name="edit_entry">Επεξεργασία καταχώρησης</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="error_load_database_KDF_memory">Δεν ήταν δυνατή η φόρτωση του κλειδιού. Προσπαθήστε να μειώσετε την KDF \"Χρήση μνήμης\".</string>
<string name="list_entries_show_username_title">Εμφάνιση ονομάτων χρηστών</string> <string name="list_entries_show_username_title">Εμφάνιση ονομάτων χρηστών</string>
<string name="list_entries_show_username_summary">Εμφάνιση ονομάτων χρηστών σε λίστες καταχώρησης</string> <string name="list_entries_show_username_summary">Εμφάνιση ονομάτων χρηστών σε λίστες καταχώρησης</string>
@@ -645,4 +645,26 @@
<string name="upper_case">ΚΕΦΑΛΑΙΑ ΓΡΑΜΜΑΤΑ</string> <string name="upper_case">ΚΕΦΑΛΑΙΑ ΓΡΑΜΜΑΤΑ</string>
<string name="word_separator">Διαχωριστής</string> <string name="word_separator">Διαχωριστής</string>
<string name="character_count">Αριθμός χαρακτήρων: %1$d</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> </resources>

View File

@@ -20,5 +20,34 @@
\n \n
\nGroups (~folders) organise entries in your database.</string> \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="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> </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_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_title">Participar</string>
<string name="education_donation_summary">Participe para aumentar la estabilidad, la seguridad y agregar más funciones.</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_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_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">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> <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_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_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_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_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_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> <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_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_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_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="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_summary">Muestra el UUID vinculado a una entrada o a un grupo</string>
<string name="show_uuid_title">Mostrar UUID</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="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_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_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_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="show_entry_colors_title">Colores de entrada</string>
<string name="menu_merge_database">Fusionar datos</string> <string name="menu_merge_database">Fusionar datos</string>
@@ -647,4 +649,5 @@
<string name="upper_case">MAYÚSCULAS</string> <string name="upper_case">MAYÚSCULAS</string>
<string name="title_case">Tipo Titular</string> <string name="title_case">Tipo Titular</string>
<string name="character_count">Conteo de caracteres: %1$d</string> <string name="character_count">Conteo de caracteres: %1$d</string>
</resources> <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