mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
331 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
128ce5657e | ||
|
|
861911ad63 | ||
|
|
9093c65235 | ||
|
|
e6ab8f82ff | ||
|
|
77ff1850f3 | ||
|
|
e985bd2a20 | ||
|
|
a6cd02d146 | ||
|
|
eb51f6712b | ||
|
|
0a19ecb715 | ||
|
|
d7e7020244 | ||
|
|
ea3349eea4 | ||
|
|
9cec64ded4 | ||
|
|
f9dc456032 | ||
|
|
6416aad823 | ||
|
|
10b5c9c261 | ||
|
|
bc0e364164 | ||
|
|
83ab5223a8 | ||
|
|
94c6710f22 | ||
|
|
a1e5266161 | ||
|
|
adbdb9a642 | ||
|
|
527f734fcf | ||
|
|
f1a29af0c6 | ||
|
|
b69a277769 | ||
|
|
d049ed39e8 | ||
|
|
ecfe767068 | ||
|
|
8d827eb562 | ||
|
|
9048f618c5 | ||
|
|
66377ded62 | ||
|
|
04c4c82953 | ||
|
|
ccd8467cba | ||
|
|
4eee9e95c4 | ||
|
|
c64b4b8d62 | ||
|
|
72ef0f2f3f | ||
|
|
9b5a95a10f | ||
|
|
1a3baa6523 | ||
|
|
a745ed1c28 | ||
|
|
39ff112b42 | ||
|
|
b3aab27c9b | ||
|
|
9b3c751a49 | ||
|
|
d29f4e0097 | ||
|
|
0645fbe938 | ||
|
|
ee6c8fc041 | ||
|
|
d93e3a1c2d | ||
|
|
d4e0c008b8 | ||
|
|
0ca392f312 | ||
|
|
c6c14c2354 | ||
|
|
652616226b | ||
|
|
d2101fd3e5 | ||
|
|
e22d9f6bdf | ||
|
|
affcc28f13 | ||
|
|
7a151bc2fe | ||
|
|
5349c4783e | ||
|
|
96a72d9842 | ||
|
|
67afa55f1d | ||
|
|
efa8fd9f17 | ||
|
|
64128991a6 | ||
|
|
3fa38c29f6 | ||
|
|
86a335768c | ||
|
|
2d29092c52 | ||
|
|
2d7e76a279 | ||
|
|
1514dbb1de | ||
|
|
1707d3c3ba | ||
|
|
3fee162c4d | ||
|
|
78fd9d616b | ||
|
|
46828c3317 | ||
|
|
5126bd4fb6 | ||
|
|
6440e5e054 | ||
|
|
15b84739e7 | ||
|
|
4bfe296e1a | ||
|
|
3fcc0db4f8 | ||
|
|
a8238565f3 | ||
|
|
53114462b3 | ||
|
|
094da79cea | ||
|
|
f093206a1c | ||
|
|
5b4940d017 | ||
|
|
aee36eeec6 | ||
|
|
cc783f8be1 | ||
|
|
a20f491cf1 | ||
|
|
f5ff9bf263 | ||
|
|
dcba54b499 | ||
|
|
d1e103c1d7 | ||
|
|
542daba206 | ||
|
|
94b61b1bbd | ||
|
|
718e590bfd | ||
|
|
9ce5e184c0 | ||
|
|
dbc477765f | ||
|
|
92fcadf3f3 | ||
|
|
c437fd96a8 | ||
|
|
10d33ecb82 | ||
|
|
d506c0cb27 | ||
|
|
acac7f7540 | ||
|
|
c853bd282a | ||
|
|
ecc4550261 | ||
|
|
c26ece7166 | ||
|
|
88b50c7902 | ||
|
|
7ba2cdd6ff | ||
|
|
589d9a2f1d | ||
|
|
a92411b95b | ||
|
|
472051bd24 | ||
|
|
e5109a1f43 | ||
|
|
f7ae9e3574 | ||
|
|
170ec3c636 | ||
|
|
1ab3fa8b3b | ||
|
|
8b046512e3 | ||
|
|
228a10c8e0 | ||
|
|
9c53bea190 | ||
|
|
11cf991498 | ||
|
|
a88c3721b2 | ||
|
|
0b4b6d4d91 | ||
|
|
941f9bcd48 | ||
|
|
09988a858d | ||
|
|
f1bf9fb25c | ||
|
|
1751fa49c0 | ||
|
|
6b4fc9a4fa | ||
|
|
7c8d85e428 | ||
|
|
e335140f23 | ||
|
|
d85f398b5f | ||
|
|
a16082a59d | ||
|
|
456269a343 | ||
|
|
eb8e1e20eb | ||
|
|
ed3c84fec0 | ||
|
|
be40416a2d | ||
|
|
5b5476a513 | ||
|
|
dc64dd6400 | ||
|
|
eca02d3bde | ||
|
|
176b6c2936 | ||
|
|
5b22350bdf | ||
|
|
6e1e011234 | ||
|
|
ac65ef6a5c | ||
|
|
fc198dde74 | ||
|
|
15ac51b2fc | ||
|
|
34214432e1 | ||
|
|
361ca92493 | ||
|
|
e367051b80 | ||
|
|
a2a4a50c5e | ||
|
|
afc74b2f2a | ||
|
|
fc756d1eaf | ||
|
|
eb8a4b1e49 | ||
|
|
8d258b3538 | ||
|
|
a59cfa3477 | ||
|
|
f1e513006e | ||
|
|
9df5c8f439 | ||
|
|
3ae099accf | ||
|
|
bb3e9396f2 | ||
|
|
1628749bde | ||
|
|
f2006b5e42 | ||
|
|
80d387d9e7 | ||
|
|
4452b4d599 | ||
|
|
dfeaeb9888 | ||
|
|
7e45a20ee7 | ||
|
|
f3fe92e4de | ||
|
|
b606909c65 | ||
|
|
2882bb30d7 | ||
|
|
5b62227e3f | ||
|
|
8b6af6fd8a | ||
|
|
99e9a92953 | ||
|
|
9f626309c3 | ||
|
|
3fe7cf2bfd | ||
|
|
9b5c274b49 | ||
|
|
46b350e7ac | ||
|
|
22a4aeb108 | ||
|
|
332e116ba7 | ||
|
|
8b594a1a1f | ||
|
|
ab23ec6d4d | ||
|
|
0ef574d675 | ||
|
|
6d15a2462d | ||
|
|
24fcdeb7aa | ||
|
|
13905db732 | ||
|
|
6e1c8e5bec | ||
|
|
9aa1d11b94 | ||
|
|
6c9f359fae | ||
|
|
531ebcae85 | ||
|
|
fe9601b510 | ||
|
|
bdf7cc6ea0 | ||
|
|
1cfe02af6f | ||
|
|
647e3f9383 | ||
|
|
597f52799d | ||
|
|
a59e052ed8 | ||
|
|
11da0a4500 | ||
|
|
fd736bd1c2 | ||
|
|
ca6a4bfeef | ||
|
|
02e9debc42 | ||
|
|
9bc9bd8b95 | ||
|
|
d810f79b7a | ||
|
|
f3468951f1 | ||
|
|
7ec5badabb | ||
|
|
1ff2f501ca | ||
|
|
cfcb49e233 | ||
|
|
467df2020e | ||
|
|
a961b41de0 | ||
|
|
40e8d5225e | ||
|
|
bc755ae1df | ||
|
|
b1cb0c3786 | ||
|
|
090d0fa2db | ||
|
|
27918a12b0 | ||
|
|
ba1498b0b2 | ||
|
|
cbde96dd82 | ||
|
|
344118a755 | ||
|
|
259c8a4bd9 | ||
|
|
fe92e41e91 | ||
|
|
e58c2f2a99 | ||
|
|
f4d5bd1bea | ||
|
|
20b352cabe | ||
|
|
20e35f1a69 | ||
|
|
d963f56d0f | ||
|
|
aecfbc7728 | ||
|
|
5734df89f0 | ||
|
|
bdf9b864d4 | ||
|
|
1c0f1a036b | ||
|
|
327c9de464 | ||
|
|
8b2f994769 | ||
|
|
a5e53d872b | ||
|
|
1868d90693 | ||
|
|
da0c19e068 | ||
|
|
d1103d8db4 | ||
|
|
b2e92646a1 | ||
|
|
19bc2444bc | ||
|
|
831b649cbb | ||
|
|
ded3c204b9 | ||
|
|
23eec5f066 | ||
|
|
6c167090e1 | ||
|
|
7d9eca0d46 | ||
|
|
c551aff474 | ||
|
|
e627745358 | ||
|
|
5a30d9d2b5 | ||
|
|
0a46817bbc | ||
|
|
a4134fa8c8 | ||
|
|
683535a5a6 | ||
|
|
edb53112c2 | ||
|
|
83a77af520 | ||
|
|
df3ae17c7b | ||
|
|
4a1624a443 | ||
|
|
a8de9f9f9f | ||
|
|
3aa5b40acd | ||
|
|
8400f3e874 | ||
|
|
b40bca1913 | ||
|
|
7100257f31 | ||
|
|
17df1a4d8a | ||
|
|
d7a5209c68 | ||
|
|
076220eacd | ||
|
|
99a50f271a | ||
|
|
63d265da06 | ||
|
|
30e3624eb1 | ||
|
|
88f3713e28 | ||
|
|
90f0c22545 | ||
|
|
8deed8468d | ||
|
|
923ad26b1b | ||
|
|
3bc858e4c2 | ||
|
|
f5a7fa41a7 | ||
|
|
bf71d5508b | ||
|
|
b44c9cfc51 | ||
|
|
5b4338abae | ||
|
|
aa5adc28cb | ||
|
|
2dad013cc0 | ||
|
|
7ade66f3ac | ||
|
|
ed75a64b46 | ||
|
|
e156b80d91 | ||
|
|
e8f79ae467 | ||
|
|
90e4862280 | ||
|
|
438080d3d6 | ||
|
|
3c17605764 | ||
|
|
3f68bc0eda | ||
|
|
ecbee73eae | ||
|
|
3e4452da00 | ||
|
|
549c690b56 | ||
|
|
aabe06f29b | ||
|
|
82693c5cd3 | ||
|
|
37a4f26d2f | ||
|
|
ca94063c7b | ||
|
|
eadc4bf6c2 | ||
|
|
b1c307c86b | ||
|
|
1874a0056d | ||
|
|
48331f9552 | ||
|
|
f907aa578a | ||
|
|
41e2620cc1 | ||
|
|
e7a82b167a | ||
|
|
088c556b00 | ||
|
|
c80343b6d4 | ||
|
|
4e52a8cf60 | ||
|
|
1ed1d4233f | ||
|
|
6e4626bc02 | ||
|
|
2608ae247f | ||
|
|
785586bfe9 | ||
|
|
bdcbb177ae | ||
|
|
15ac365d79 | ||
|
|
debbcb753b | ||
|
|
69d73aeaa4 | ||
|
|
dffe53370f | ||
|
|
4334e6dcdf | ||
|
|
c2c6c093d5 | ||
|
|
77e539eec2 | ||
|
|
a57970210e | ||
|
|
1b31a46fb7 | ||
|
|
87f19c74fc | ||
|
|
bd157a9724 | ||
|
|
5a327eb0db | ||
|
|
4b9c0b0109 | ||
|
|
df6b75cdbb | ||
|
|
0b4f8c122b | ||
|
|
2a87eaf3e5 | ||
|
|
c52266f5cf | ||
|
|
3b21f8add2 | ||
|
|
6574bd10a0 | ||
|
|
23f3335988 | ||
|
|
a5d7f33c82 | ||
|
|
3782c4dac0 | ||
|
|
1fc02fd2fe | ||
|
|
cc347c1dbe | ||
|
|
79ff20eb18 | ||
|
|
e6e8a447da | ||
|
|
233f0c5bdb | ||
|
|
9ed4271a14 | ||
|
|
470c0b6b43 | ||
|
|
afa8ae42b9 | ||
|
|
63d426503f | ||
|
|
ffb7f80b26 | ||
|
|
63f8826fd8 | ||
|
|
ef836e8b84 | ||
|
|
abc1c43a51 | ||
|
|
a4fe92562f | ||
|
|
b9bd1d9d4b | ||
|
|
3b6c28488a | ||
|
|
875eb3500d | ||
|
|
3a88a2451c | ||
|
|
6800b73a4f | ||
|
|
983404e6d8 | ||
|
|
b95c0a18a7 | ||
|
|
36b317cad8 | ||
|
|
35d74888fb | ||
|
|
279bd16b74 | ||
|
|
2e0081b66c |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
#github: [J-Jamet] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
#patreon: # Replace with a single Patreon username
|
||||
#open_collective: # Replace with a single Open Collective username
|
||||
#ko_fi: # Replace with a single Ko-fi username
|
||||
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: Kunzisoft # Replace with a single Liberapay username
|
||||
issuehunt: Kunzisoft/KeePassDX # Replace with a single IssueHunt username
|
||||
#otechie: # Replace with a single Otechie username
|
||||
#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: ['https://www.keepassdx.com/#donation'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -80,6 +80,9 @@ art/screen*.png
|
||||
art/logo_512.png
|
||||
art/store_screens/
|
||||
|
||||
# Release
|
||||
releases/*
|
||||
|
||||
# Dir linux
|
||||
.directory
|
||||
*/.directory
|
||||
|
||||
39
CHANGELOG
39
CHANGELOG
@@ -1,3 +1,42 @@
|
||||
KeePassDX(3.5.0)
|
||||
* Support YubiKey challenge-response #8 #137
|
||||
* Better exception management during database save #1346
|
||||
* Add "Screenshot mode" setting #459 #1377 #1354 (Thx @GianpaMX)
|
||||
* Hide clipboard sensitive text when copy entry field #1386
|
||||
* Fix attachment download button #1401
|
||||
* Add monochrome icon #1403 #1404 (Thx @Sandelinos)
|
||||
* Fix lock with back button #1412 #1414 (Thx @ryg-git)
|
||||
* Vanadium compatibility #1447 (Thx @flawedworld)
|
||||
|
||||
KeePassDX(3.4.5)
|
||||
* Fix custom data in group (fix KeeShare) #1335
|
||||
* Fix device credential unlocking #1344
|
||||
* New clipboard manager #1343
|
||||
* Keep screen on by default when viewing an entry
|
||||
* Change the order of the search filters
|
||||
* Fix searchable selection
|
||||
|
||||
KeePassDX(3.4.4)
|
||||
* Fix crash in New Android 13 #1321
|
||||
* Better backstack management for selection mode
|
||||
* Prevent Tapjacking #1318
|
||||
* Small changes #1298
|
||||
|
||||
KeePassDX(3.4.3)
|
||||
* Remove "Select share info" setting for Magikeyboard #1304
|
||||
* Fix quick search and better loadGroup implementation #1302
|
||||
* Fix small bugs
|
||||
|
||||
KeePassDX(3.4.2)
|
||||
* Fix service parameter and workflow to remove notification when service is killed
|
||||
* Fix color
|
||||
|
||||
KeePassDX(3.4.1)
|
||||
* Fix search mode with Magikeyboard #1292
|
||||
* Fix select another entry with Magikeyboard #1293
|
||||
* Fix unexpected lock with Magikeyboard #1294
|
||||
* Small UI changes
|
||||
|
||||
KeePassDX(3.4.0)
|
||||
* Passphrase implementation #218
|
||||
* Show visual password strength indicator with entropy #631 #869 #454 #1270
|
||||
|
||||
10
Gemfile
Normal file
10
Gemfile
Normal file
@@ -0,0 +1,10 @@
|
||||
# Autogenerated by fastlane
|
||||
#
|
||||
# Ensure this file is checked in to source control!
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem 'fastlane'
|
||||
|
||||
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||
220
Gemfile.lock
Normal file
220
Gemfile.lock
Normal file
@@ -0,0 +1,220 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
addressable (2.8.1)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.646.0)
|
||||
aws-sdk-core (3.160.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.58.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.93.0)
|
||||
faraday (1.10.2)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.210.1)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (~> 2.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
fastlane-plugin-versioning_android (0.1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.29.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-apis-core (0.9.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.15.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.11.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-apis-storage_v1 (0.19.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.3.0)
|
||||
google-cloud-storage (1.43.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.19.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.2.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.2)
|
||||
jwt (2.5.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
optparse (0.1.1)
|
||||
os (1.1.4)
|
||||
plist (3.6.0)
|
||||
public_suffix (5.0.0)
|
||||
rake (13.0.6)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
signet (0.17.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.1)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.22.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
fastlane-plugin-versioning_android
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
10
README.md
10
README.md
@@ -1,6 +1,6 @@
|
||||
# Android KeePassDX
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeePassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
- Create database files / entries and groups.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …).
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||
- Allows opening and **copying URI / URL fields quickly**.
|
||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||
@@ -53,10 +53,12 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
||||
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||
alt="Get it on Google Play"
|
||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||
[<img src="https://raw.githubusercontent.com/Kunzisoft/Github-badge/main/get-it-on-github.png"
|
||||
alt="Get it on Github"
|
||||
height="80">](https://github.com/Kunzisoft/KeePassDX/releases)
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
@@ -72,7 +74,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
Copyright © 2023 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "31.0.0"
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion "32.0.0"
|
||||
ndkVersion "21.4.7075529"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 31
|
||||
versionCode = 108
|
||||
versionName = "3.4.0"
|
||||
targetSdkVersion 32
|
||||
versionCode = 118
|
||||
versionName = "3.5.0"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -93,7 +93,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
def room_version = "2.4.2"
|
||||
def room_version = "2.4.3"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
@@ -101,14 +101,14 @@ dependencies {
|
||||
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
implementation 'androidx.media:media:1.5.0'
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||
implementation "androidx.core:core-ktx:$android_core_version"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.2'
|
||||
implementation "com.google.android.material:material:$android_material_version"
|
||||
// Token auto complete
|
||||
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
|
||||
|
||||
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
15
app/src/free/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
15
app/src/free/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="120"
|
||||
android:viewportHeight="120">
|
||||
<group
|
||||
android:translateX="6"
|
||||
android:translateY="6">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
15
app/src/libre/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
15
app/src/libre/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="120"
|
||||
android:viewportHeight="120">
|
||||
<group
|
||||
android:translateX="6"
|
||||
android:translateY="6">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -150,15 +150,19 @@
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden" />
|
||||
android:configChanges="keyboardHidden"
|
||||
android:excludeFromRecents="true"/>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
||||
android:theme="@style/Theme.Transparent" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.core.text.HtmlCompat
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class AboutActivity : StylishActivity() {
|
||||
@@ -45,6 +46,12 @@ class AboutActivity : StylishActivity() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
val appName = if (UriUtil.contributingUser(this))
|
||||
getString(R.string.app_name) + " " + getString(R.string.app_name_part3)
|
||||
else
|
||||
getString(R.string.app_name)
|
||||
findViewById<TextView>(R.id.activity_about_app_name).text = appName
|
||||
|
||||
var version: String
|
||||
var build: String
|
||||
try {
|
||||
@@ -70,6 +77,12 @@ class AboutActivity : StylishActivity() {
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
|
||||
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_privacy),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
|
||||
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
|
||||
|
||||
@@ -60,6 +60,7 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
@@ -434,9 +435,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
|
||||
private fun entryValidatedForKeyboardSelection(database: Database, entry: Entry) {
|
||||
// Populate Magikeyboard with entry
|
||||
populateKeyboardAndMoveAppToBackground(this,
|
||||
entry.getEntryInfo(database),
|
||||
intent)
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entry.getEntryInfo(database)
|
||||
)
|
||||
onValidateSpecialMode()
|
||||
// Don't keep activity history for entry edition
|
||||
finishForEntryResult(entry)
|
||||
@@ -477,6 +479,11 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the screen on
|
||||
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
|
||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
||||
@@ -19,23 +19,18 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
/**
|
||||
* Activity to search or select entry in database,
|
||||
@@ -48,7 +43,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
@@ -61,7 +56,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
keySelectionBundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
|
||||
searchInfo = mSearchInfo
|
||||
}
|
||||
launch(database, searchInfo, true)
|
||||
launch(database, searchInfo)
|
||||
} else {
|
||||
// To manage share
|
||||
var sharedWebDomain: String? = null
|
||||
@@ -78,6 +73,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
sharedWebDomain = Uri.parse(extra).host
|
||||
}
|
||||
}
|
||||
launchSelection(database, sharedWebDomain, otpString)
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
// Retrieve OTP
|
||||
@@ -85,29 +81,40 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
}
|
||||
launchSelection(database, sharedWebDomain, otpString)
|
||||
}
|
||||
else -> {
|
||||
if (database != null) {
|
||||
GroupActivity.launch(this, database)
|
||||
} else {
|
||||
FileDatabaseSelectActivity.launch(this)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
// Build domain search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
this.webDomain = sharedWebDomain
|
||||
this.otpString = otpString
|
||||
}
|
||||
private fun launchSelection(database: Database?,
|
||||
sharedWebDomain: String?,
|
||||
otpString: String?) {
|
||||
// Build domain search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
this.webDomain = sharedWebDomain
|
||||
this.otpString = otpString
|
||||
}
|
||||
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launch(database, searchInfo)
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launch(database, searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launch(database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
forceSelection: Boolean = false) {
|
||||
searchInfo: SearchInfo) {
|
||||
|
||||
// Setting to integrate Magikeyboard
|
||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||
val searchShareForMagikeyboard = MagikeyboardService.activatedInSettings(this)
|
||||
|
||||
// If database is open
|
||||
val readOnly = database?.isReadOnly != false
|
||||
@@ -130,21 +137,22 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
.show()
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
if (items.size == 1 && !forceSelection) {
|
||||
// Automatically populate keyboard
|
||||
val entryPopulate = items[0]
|
||||
populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entryPopulate,
|
||||
intent)
|
||||
Log.e("TEST", "One item activity")
|
||||
} else {
|
||||
// Select the one we want
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
// Automatically populate keyboard
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entryInfo
|
||||
)
|
||||
},
|
||||
{ autoSearch ->
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
autoSearch)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
openedDatabase,
|
||||
@@ -166,13 +174,13 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (readOnly || searchShareForMagikeyboard) {
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
@@ -192,7 +200,6 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
}
|
||||
)
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -214,14 +221,3 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||
entry: EntryInfo,
|
||||
intent: Intent,
|
||||
toast: Boolean = true) {
|
||||
// Populate Magikeyboard with entry
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||
// Consume the selection mode
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
activity.moveTaskToBack(true)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
@@ -66,6 +67,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
@@ -155,8 +157,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||
launchPasswordActivity(
|
||||
databaseFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri
|
||||
databaseFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -250,7 +253,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
?: MainCredential()
|
||||
databaseFilesViewModel.addDatabaseFile(
|
||||
databaseUri,
|
||||
mainCredential.keyFileUri
|
||||
mainCredential.keyFileUri,
|
||||
mainCredential.hardwareKey
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -268,18 +272,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var resultError = ""
|
||||
val resultMessage = result.message
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
}
|
||||
Log.e(TAG, resultError)
|
||||
Snackbar.make(coordinatorLayout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,10 +291,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
|
||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||
MainCredentialActivity.launch(this,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
{ exception ->
|
||||
fileNoFoundAction(exception)
|
||||
},
|
||||
@@ -320,18 +315,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
super.onValidateSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
super.onCancelSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
launchPasswordActivity(databaseUri, null)
|
||||
launchPasswordActivity(databaseUri, null, null)
|
||||
// Delete flickering for kitkat <=
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
overridePendingTransition(0, 0)
|
||||
|
||||
@@ -67,8 +67,9 @@ import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
@@ -123,8 +124,6 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
|
||||
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null
|
||||
|
||||
private var mSearchMenuItem: MenuItem? = null
|
||||
|
||||
private var mGroupFragment: GroupFragment? = null
|
||||
private var mRecyclingBinEnabled = false
|
||||
private var mRecyclingBinIsCurrentGroup = false
|
||||
@@ -408,20 +407,14 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
val currentGroup = it.group
|
||||
mCurrentGroup = currentGroup
|
||||
if (currentGroup.isVirtual) {
|
||||
val searchParameters = it.searchParameters
|
||||
mSearchState = SearchState(searchParameters, it.showFromPosition)
|
||||
mSearchState = SearchState(
|
||||
it.searchParameters,
|
||||
it.showFromPosition
|
||||
)
|
||||
}
|
||||
// Main and search groups in activity are managed with another variables
|
||||
// to keep values during orientation
|
||||
|
||||
// Expand the search view if defined in settings
|
||||
if (mRequestStartupSearch
|
||||
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
|
||||
// To request search only one time
|
||||
mRequestStartupSearch = false
|
||||
mSearchMenuItem?.expandActionView()
|
||||
}
|
||||
|
||||
loadingView?.hideByFading()
|
||||
}
|
||||
|
||||
@@ -728,8 +721,13 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
mSearchState = SearchState(PreferencesUtil.getDefaultSearchParameters(this).apply {
|
||||
searchQuery = stringQuery
|
||||
}, mSearchState?.firstVisibleItem ?: 0)
|
||||
} else if (mRequestStartupSearch
|
||||
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
|
||||
// Expand the search view if defined in settings
|
||||
// To request search only one time
|
||||
mRequestStartupSearch = false
|
||||
addSearch()
|
||||
}
|
||||
loadGroup()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,10 +888,9 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
private fun entrySelectedForKeyboardSelection(database: Database, entry: Entry) {
|
||||
reloadCurrentGroup()
|
||||
// Populate Magikeyboard with entry
|
||||
populateKeyboardAndMoveAppToBackground(
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entry.getEntryInfo(database),
|
||||
intent
|
||||
entry.getEntryInfo(database)
|
||||
)
|
||||
onValidateSpecialMode()
|
||||
}
|
||||
@@ -1180,7 +1177,6 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
// Get the SearchView and set the searchable configuration
|
||||
menu.findItem(R.id.menu_search)?.let {
|
||||
mLockSearchListeners = true
|
||||
mSearchMenuItem = it
|
||||
it.setOnActionExpandListener(mOnSearchActionExpandListener)
|
||||
searchView = it.actionView as SearchView?
|
||||
searchView?.apply {
|
||||
@@ -1372,7 +1368,6 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||
lockAndExit()
|
||||
super.onRegularBackPressed()
|
||||
} else {
|
||||
backToTheAppCaller()
|
||||
}
|
||||
@@ -1612,50 +1607,31 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
GroupActivity.launch(
|
||||
// Default action
|
||||
launch(
|
||||
activity,
|
||||
database,
|
||||
true
|
||||
)
|
||||
},
|
||||
{ searchInfo ->
|
||||
SearchHelper.checkAutoSearchInfo(activity,
|
||||
// Search action
|
||||
if (database.loaded) {
|
||||
launchForSearchResult(activity,
|
||||
database,
|
||||
searchInfo,
|
||||
{ _, _ ->
|
||||
// Response is build
|
||||
GroupActivity.launchForSearchResult(activity,
|
||||
database,
|
||||
searchInfo,
|
||||
true)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Here no search info found
|
||||
if (database.isReadOnly) {
|
||||
GroupActivity.launchForSearchResult(activity,
|
||||
database,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
GroupActivity.launchForSaveResult(activity,
|
||||
database,
|
||||
searchInfo,
|
||||
false)
|
||||
}
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Simply close if database not opened, normally not happened
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
)
|
||||
true)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
// Simply close if database not opened
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ searchInfo ->
|
||||
// Save info used with OTP
|
||||
// Save info
|
||||
if (database.loaded) {
|
||||
if (!database.isReadOnly) {
|
||||
GroupActivity.launchForSaveResult(
|
||||
launchForSaveResult(
|
||||
activity,
|
||||
database,
|
||||
searchInfo,
|
||||
@@ -1674,28 +1650,33 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
},
|
||||
{ searchInfo ->
|
||||
// Keyboard selection
|
||||
SearchHelper.checkAutoSearchInfo(activity,
|
||||
database,
|
||||
searchInfo,
|
||||
{ _, items ->
|
||||
// Response is build
|
||||
if (items.size == 1) {
|
||||
populateKeyboardAndMoveAppToBackground(activity,
|
||||
items[0],
|
||||
activity.intent)
|
||||
onValidateSpecialMode()
|
||||
} else {
|
||||
// Select the one we want
|
||||
GroupActivity.launchForKeyboardSelectionResult(activity,
|
||||
database,
|
||||
searchInfo,
|
||||
true)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
// Keyboard populated
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
activity,
|
||||
entryInfo
|
||||
)
|
||||
onValidateSpecialMode()
|
||||
},
|
||||
{ autoSearch ->
|
||||
launchForKeyboardSelectionResult(activity,
|
||||
database,
|
||||
searchInfo,
|
||||
autoSearch)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
GroupActivity.launchForKeyboardSelectionResult(activity,
|
||||
launchForKeyboardSelectionResult(activity,
|
||||
database,
|
||||
searchInfo,
|
||||
false)
|
||||
@@ -1708,6 +1689,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
)
|
||||
},
|
||||
{ searchInfo, autofillComponent ->
|
||||
// Autofill selection
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
SearchHelper.checkAutoSearchInfo(activity,
|
||||
database,
|
||||
@@ -1719,7 +1701,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
GroupActivity.launchForAutofillResult(activity,
|
||||
launchForAutofillResult(activity,
|
||||
database,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
@@ -1737,20 +1719,21 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
},
|
||||
{ registerInfo ->
|
||||
// Autofill registration
|
||||
if (!database.isReadOnly) {
|
||||
SearchHelper.checkAutoSearchInfo(activity,
|
||||
database,
|
||||
registerInfo?.searchInfo,
|
||||
{ _, _ ->
|
||||
// No auto search, it's a registration
|
||||
GroupActivity.launchForRegistration(activity,
|
||||
launchForRegistration(activity,
|
||||
database,
|
||||
registerInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
GroupActivity.launchForRegistration(activity,
|
||||
launchForRegistration(activity,
|
||||
database,
|
||||
registerInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
|
||||
@@ -56,9 +56,11 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
||||
@@ -73,6 +75,7 @@ import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.MainCredentialView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||
import java.io.FileNotFoundException
|
||||
@@ -101,6 +104,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mRememberHardwareKey: Boolean = false
|
||||
|
||||
private var mReadOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
|
||||
@@ -133,11 +138,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
PreferencesUtil.enableReadOnlyDatabase(this)
|
||||
}
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
|
||||
// Build elements to manage keyfile selection
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mainCredentialView?.populateKeyFileTextView(uri)
|
||||
mainCredentialView?.populateKeyFileView(uri)
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
@@ -171,6 +178,16 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableConfirmationButton()
|
||||
}
|
||||
mainCredentialView?.onKeyFileChecked =
|
||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableConfirmationButton()
|
||||
}
|
||||
mainCredentialView?.onHardwareKeyChecked =
|
||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableConfirmationButton()
|
||||
}
|
||||
|
||||
// Observe if default database
|
||||
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||
@@ -204,10 +221,19 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
databaseKeyFileUri
|
||||
}
|
||||
|
||||
val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey
|
||||
val hardwareKey =
|
||||
if (mRememberHardwareKey
|
||||
&& databaseHardwareKey == null) {
|
||||
databaseFile?.hardwareKey
|
||||
} else {
|
||||
databaseHardwareKey
|
||||
}
|
||||
|
||||
// Define title
|
||||
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||
|
||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +241,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
super.onResume()
|
||||
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
|
||||
|
||||
// Back to previous keyboard is setting activated
|
||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
|
||||
@@ -266,90 +293,84 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
launchGroupActivityIfLoaded(database)
|
||||
} else {
|
||||
mainCredentialView?.requestPasswordFocus()
|
||||
// Manage special exceptions
|
||||
when (result.exception) {
|
||||
is DuplicateUuidDatabaseException -> {
|
||||
// Relaunch loading if we need to fix UUID
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
|
||||
var resultError = ""
|
||||
val resultException = result.exception
|
||||
val resultMessage = result.message
|
||||
var databaseUri: Uri? = null
|
||||
var mainCredential = MainCredential()
|
||||
var readOnly = true
|
||||
var cipherEncryptDatabase: CipherEncryptDatabase? = null
|
||||
|
||||
if (resultException != null) {
|
||||
resultError = resultException.getLocalizedMessage(resources)
|
||||
|
||||
when (resultException) {
|
||||
is DuplicateUuidDatabaseException -> {
|
||||
// Relaunch loading if we need to fix UUID
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
var mainCredential = MainCredential()
|
||||
var readOnly = true
|
||||
var cipherEncryptDatabase: CipherEncryptDatabase? = null
|
||||
|
||||
result.data?.let { resultData ->
|
||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||
mainCredential =
|
||||
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
|
||||
?: mainCredential
|
||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||
cipherEncryptDatabase =
|
||||
resultData.getParcelable(CIPHER_DATABASE_KEY)
|
||||
}
|
||||
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseFileUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
true
|
||||
)
|
||||
}
|
||||
result.data?.let { resultData ->
|
||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||
mainCredential =
|
||||
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
|
||||
?: mainCredential
|
||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||
cipherEncryptDatabase =
|
||||
resultData.getParcelable(CIPHER_DATABASE_KEY)
|
||||
}
|
||||
}
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseFileUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
}
|
||||
Log.e(TAG, resultError)
|
||||
Snackbar.make(
|
||||
coordinatorLayout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG
|
||||
).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
private fun getUriFromIntent(intent: Intent?) {
|
||||
// If is a view intent
|
||||
val action = intent?.action
|
||||
if (action != null
|
||||
&& action == VIEW_INTENT) {
|
||||
mDatabaseFileUri = intent.data
|
||||
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
|
||||
if (action == VIEW_INTENT) {
|
||||
fillCredentials(
|
||||
intent.data,
|
||||
UriUtil.getUriFromIntent(intent, KEY_KEYFILE),
|
||||
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
|
||||
)
|
||||
} else {
|
||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
|
||||
mainCredentialView?.populateKeyFileTextView(it)
|
||||
}
|
||||
fillCredentials(
|
||||
intent?.getParcelableExtra(KEY_FILENAME),
|
||||
intent?.getParcelableExtra(KEY_KEYFILE),
|
||||
HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
|
||||
)
|
||||
}
|
||||
try {
|
||||
intent?.removeExtra(KEY_KEYFILE)
|
||||
intent?.removeExtra(KEY_HARDWARE_KEY)
|
||||
} catch (e: Exception) {}
|
||||
mDatabaseFileUri?.let {
|
||||
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillCredentials(databaseUri: Uri?,
|
||||
keyFileUri: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
mDatabaseFileUri = databaseUri
|
||||
mainCredentialView?.populateKeyFileView(keyFileUri)
|
||||
mainCredentialView?.populateHardwareKeyView(hardwareKey)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
getUriFromIntent(intent)
|
||||
@@ -358,7 +379,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||
// Check if database really loaded
|
||||
if (database.loaded) {
|
||||
clearCredentialsViews(true)
|
||||
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
||||
GroupActivity.launch(this,
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
@@ -369,16 +390,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
super.onValidateSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
super.onCancelSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun retrieveCredentialForEncryption(): ByteArray {
|
||||
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||
?: byteArrayOf()
|
||||
@@ -418,7 +429,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||
when (cipherDecryptDatabase.credentialStorage) {
|
||||
CredentialStorage.PASSWORD -> {
|
||||
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
|
||||
mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
|
||||
}
|
||||
CredentialStorage.KEY_FILE -> {
|
||||
// TODO advanced unlock key file
|
||||
@@ -433,14 +444,23 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?,
|
||||
keyFileUri: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
// Define Key File text
|
||||
if (mRememberKeyFile) {
|
||||
mainCredentialView?.populateKeyFileTextView(keyFileUri)
|
||||
mainCredentialView?.populateKeyFileView(keyFileUri)
|
||||
}
|
||||
|
||||
// Define hardware key
|
||||
if (mRememberHardwareKey) {
|
||||
mainCredentialView?.populateHardwareKeyView(hardwareKey)
|
||||
}
|
||||
|
||||
// Define listener for validate button
|
||||
confirmButtonView?.setOnClickListener { loadDatabase() }
|
||||
confirmButtonView?.setOnClickListener {
|
||||
mainCredentialView?.validateCredential()
|
||||
}
|
||||
|
||||
// If Activity is launch with a password and want to open directly
|
||||
val intent = intent
|
||||
@@ -472,10 +492,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile,
|
||||
clearHardwareKey: Boolean = !mRememberHardwareKey) {
|
||||
mainCredentialView?.populatePasswordTextView(null)
|
||||
if (clearKeyFile) {
|
||||
mainCredentialView?.populateKeyFileTextView(null)
|
||||
mainCredentialView?.populateKeyFileView(null)
|
||||
}
|
||||
if (clearHardwareKey) {
|
||||
mainCredentialView?.populateHardwareKeyView(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,18 +690,24 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
|
||||
private const val KEY_FILENAME = "fileName"
|
||||
private const val KEY_KEYFILE = "keyFile"
|
||||
private const val KEY_HARDWARE_KEY = "hardwareKey"
|
||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||
|
||||
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||
private fun buildAndLaunchIntent(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||
if (keyFile != null)
|
||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||
if (hardwareKey != null)
|
||||
intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString())
|
||||
intentBuildLauncher.invoke(intent)
|
||||
}
|
||||
|
||||
@@ -690,8 +720,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -706,8 +737,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForSearchResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -725,8 +757,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForSaveResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -744,8 +777,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForKeyboardResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -764,10 +798,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -785,8 +820,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForRegistration(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
registerInfo: RegisterInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -802,6 +838,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launch(activity: AppCompatActivity,
|
||||
databaseUri: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit,
|
||||
@@ -810,43 +847,67 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
MainCredentialActivity.launch(activity,
|
||||
databaseUri, keyFile)
|
||||
launch(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey
|
||||
)
|
||||
},
|
||||
{ searchInfo -> // Search Action
|
||||
MainCredentialActivity.launchForSearchResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
launchForSearchResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Save Action
|
||||
MainCredentialActivity.launchForSaveResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
launchForSaveResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Keyboard Selection Action
|
||||
MainCredentialActivity.launchForKeyboardResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
launchForKeyboardResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
MainCredentialActivity.launchForAutofillResult(activity,
|
||||
databaseUri, keyFile,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
launchForAutofillResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ registerInfo -> // Registration Action
|
||||
MainCredentialActivity.launchForRegistration(activity,
|
||||
databaseUri, keyFile,
|
||||
registerInfo)
|
||||
launchForRegistration(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
registerInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.MainCredentialView
|
||||
|
||||
@@ -95,7 +95,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mainCredentialView?.populateKeyFileTextView(uri)
|
||||
mainCredentialView?.populateKeyFileView(uri)
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
|
||||
@@ -26,7 +26,7 @@ import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
|
||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
|
||||
|
||||
@@ -45,13 +45,16 @@ class ProFeatureDialogFragment : DialogFragment() {
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
||||
UriUtil.gotoUrl(activity,
|
||||
activity.getString(R.string.play_store_url,
|
||||
activity.getString(R.string.keepro_app_id))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||
UriUtil.gotoUrl(activity, R.string.contribution_url)
|
||||
}
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
|
||||
@@ -35,9 +35,12 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.HardwareKeySelectionView
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
import com.kunzisoft.keepass.view.PassKeyView
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
@@ -45,18 +48,21 @@ import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mMasterPassword: String? = null
|
||||
private var mKeyFile: Uri? = null
|
||||
private var mKeyFileUri: Uri? = null
|
||||
private var mHardwareKey: HardwareKey? = null
|
||||
|
||||
private var rootView: View? = null
|
||||
private lateinit var rootView: View
|
||||
|
||||
private var passwordCheckBox: CompoundButton? = null
|
||||
private lateinit var passwordCheckBox: CompoundButton
|
||||
private lateinit var passwordView: PassKeyView
|
||||
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
|
||||
private lateinit var passwordRepeatView: TextView
|
||||
|
||||
private var passKeyView: PassKeyView? = null
|
||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||
private var passwordRepeatView: TextView? = null
|
||||
private lateinit var keyFileCheckBox: CompoundButton
|
||||
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
||||
|
||||
private var keyFileCheckBox: CompoundButton? = null
|
||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||
private lateinit var hardwareKeyCheckBox: CompoundButton
|
||||
private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
|
||||
|
||||
private var mListener: AssignMainCredentialDialogListener? = null
|
||||
|
||||
@@ -67,13 +73,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||
|
||||
private var mAllowNoMasterKey: Boolean = false
|
||||
|
||||
private val passwordTextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
passwordCheckBox?.isChecked = true
|
||||
passwordCheckBox.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +121,9 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
var allowNoMasterKey = false
|
||||
arguments?.apply {
|
||||
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
||||
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||
mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
@@ -128,63 +135,63 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||
rootView.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||
}
|
||||
|
||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||
passKeyView = rootView?.findViewById(R.id.password_view)
|
||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation)
|
||||
passwordRepeatView?.applyFontVisibility()
|
||||
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
|
||||
passwordView = rootView.findViewById(R.id.password_view)
|
||||
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
|
||||
passwordRepeatView.applyFontVisibility()
|
||||
|
||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
|
||||
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
|
||||
|
||||
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
|
||||
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let { pathUri ->
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
keyFileSelectionView.error = null
|
||||
keyFileCheckBox.isChecked = true
|
||||
keyFileSelectionView.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
hardwareKeySelectionView.selectionListener = { hardwareKey ->
|
||||
hardwareKeyCheckBox.isChecked = true
|
||||
hardwareKeySelectionView.error =
|
||||
if (!HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
|
||||
// show hardware driver dialog if required
|
||||
getString(R.string.error_driver_required, hardwareKey.toString())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.setOnShowListener { dialog1 ->
|
||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
positiveButton.setOnClickListener {
|
||||
|
||||
if (passwordCheckBox != null && keyFileCheckBox!= null) {
|
||||
dialog.setOnShowListener { dialog1 ->
|
||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
positiveButton.setOnClickListener {
|
||||
mMasterPassword = ""
|
||||
mKeyFileUri = null
|
||||
mHardwareKey = null
|
||||
|
||||
mMasterPassword = ""
|
||||
mKeyFile = null
|
||||
|
||||
var error = verifyPassword() || verifyKeyFile()
|
||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||
error = true
|
||||
if (allowNoMasterKey)
|
||||
showNoKeyConfirmationDialog()
|
||||
else {
|
||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
||||
}
|
||||
}
|
||||
if (!error) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
negativeButton.setOnClickListener {
|
||||
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
approveMainCredential()
|
||||
}
|
||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
negativeButton.setOnClickListener {
|
||||
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,67 +201,113 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun approveMainCredential() {
|
||||
val errorPassword = verifyPassword()
|
||||
val errorKeyFile = verifyKeyFile()
|
||||
val errorHardwareKey = verifyHardwareKey()
|
||||
// Check all to fill error
|
||||
var error = errorPassword || errorKeyFile || errorHardwareKey
|
||||
val hardwareKey = hardwareKeySelectionView.hardwareKey
|
||||
if (!error
|
||||
&& (!passwordCheckBox.isChecked)
|
||||
&& (!keyFileCheckBox.isChecked)
|
||||
&& (!hardwareKeyCheckBox.isChecked)
|
||||
) {
|
||||
error = true
|
||||
if (mAllowNoMasterKey) {
|
||||
// show no key dialog if required
|
||||
showNoKeyConfirmationDialog()
|
||||
} else {
|
||||
passwordRepeatTextInputLayout.error =
|
||||
getString(R.string.error_disallow_no_credentials)
|
||||
}
|
||||
} else if (!error
|
||||
&& mMasterPassword.isNullOrEmpty()
|
||||
&& !keyFileCheckBox.isChecked
|
||||
&& !hardwareKeyCheckBox.isChecked
|
||||
) {
|
||||
// show empty password dialog if required
|
||||
error = true
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
} else if (!error
|
||||
&& hardwareKey != null
|
||||
&& !HardwareKeyActivity.isHardwareKeyAvailable(
|
||||
requireActivity(), hardwareKey, false)
|
||||
) {
|
||||
// show hardware driver dialog if required
|
||||
error = true
|
||||
hardwareKeySelectionView.error =
|
||||
getString(R.string.error_driver_required, hardwareKey.toString())
|
||||
}
|
||||
if (!error) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyPassword(): Boolean {
|
||||
var error = false
|
||||
passwordRepeatTextInputLayout.error = null
|
||||
if (passwordCheckBox.isChecked) {
|
||||
mMasterPassword = passwordView.passwordString
|
||||
val confPassword = passwordRepeatView.text.toString()
|
||||
|
||||
// Verify that passwords match
|
||||
if (mMasterPassword != confPassword) {
|
||||
error = true
|
||||
// Passwords do not match
|
||||
passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match)
|
||||
}
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
private fun verifyKeyFile(): Boolean {
|
||||
var error = false
|
||||
keyFileSelectionView.error = null
|
||||
if (keyFileCheckBox.isChecked) {
|
||||
keyFileSelectionView.uri?.let { uri ->
|
||||
mKeyFileUri = uri
|
||||
} ?: run {
|
||||
error = true
|
||||
keyFileSelectionView.error = getString(R.string.error_nokeyfile)
|
||||
}
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
private fun verifyHardwareKey(): Boolean {
|
||||
var error = false
|
||||
hardwareKeySelectionView.error = null
|
||||
if (hardwareKeyCheckBox.isChecked) {
|
||||
hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
|
||||
mHardwareKey = hardwareKey
|
||||
} ?: run {
|
||||
error = true
|
||||
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
|
||||
}
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
private fun retrieveMainCredential(): MainCredential {
|
||||
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
|
||||
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
|
||||
return MainCredential(masterPassword, keyFile)
|
||||
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
|
||||
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
|
||||
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
|
||||
return MainCredential(masterPassword, keyFileUri, hardwareKey)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// To check checkboxes if a text is present
|
||||
passKeyView?.addTextChangedListener(passwordTextWatcher)
|
||||
passwordView.addTextChangedListener(passwordTextWatcher)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
passKeyView?.removeTextChangedListener(passwordTextWatcher)
|
||||
}
|
||||
|
||||
private fun verifyPassword(): Boolean {
|
||||
var error = false
|
||||
if (passwordCheckBox != null
|
||||
&& passwordCheckBox!!.isChecked
|
||||
&& passKeyView != null
|
||||
&& passwordRepeatView != null) {
|
||||
mMasterPassword = passKeyView!!.passwordString
|
||||
val confPassword = passwordRepeatView!!.text.toString()
|
||||
|
||||
// Verify that passwords match
|
||||
if (mMasterPassword != confPassword) {
|
||||
error = true
|
||||
// Passwords do not match
|
||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
||||
}
|
||||
|
||||
if ((mMasterPassword == null
|
||||
|| mMasterPassword!!.isEmpty())
|
||||
&& (keyFileCheckBox == null
|
||||
|| !keyFileCheckBox!!.isChecked
|
||||
|| keyFileSelectionView?.uri == null)) {
|
||||
error = true
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
}
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
private fun verifyKeyFile(): Boolean {
|
||||
var error = false
|
||||
if (keyFileCheckBox != null
|
||||
&& keyFileCheckBox!!.isChecked) {
|
||||
|
||||
keyFileSelectionView?.uri?.let { uri ->
|
||||
mKeyFile = uri
|
||||
} ?: run {
|
||||
error = true
|
||||
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
|
||||
}
|
||||
}
|
||||
return error
|
||||
passwordView.removeTextChangedListener(passwordTextWatcher)
|
||||
}
|
||||
|
||||
private fun showEmptyPasswordConfirmationDialog() {
|
||||
@@ -262,10 +315,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_empty_password)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!verifyKeyFile()) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@SetMainCredentialDialogFragment.dismiss()
|
||||
}
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@SetMainCredentialDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
mEmptyPasswordConfirmationDialog = builder.create()
|
||||
@@ -299,8 +350,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
keyFileCheckBox?.isChecked = false
|
||||
keyFileSelectionView?.uri = null
|
||||
keyFileCheckBox.isChecked = false
|
||||
keyFileSelectionView.uri = null
|
||||
}
|
||||
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||
mEmptyKeyFileConfirmationDialog?.show()
|
||||
|
||||
@@ -39,6 +39,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
/*
|
||||
if (UriUtil.contributingUser(activity)) {
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
@@ -46,14 +47,14 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
||||
} else {
|
||||
*/
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
}
|
||||
//}
|
||||
builder.setMessage(stringBuilder)
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
|
||||
@@ -27,7 +27,6 @@ import com.kunzisoft.keepass.view.TemplateView
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.*
|
||||
|
||||
class EntryFragment: DatabaseFragment() {
|
||||
|
||||
@@ -158,11 +157,9 @@ class EntryFragment: DatabaseFragment() {
|
||||
|
||||
setOnCopyActionClickListener { field ->
|
||||
mClipboardHelper?.timeoutCopyToClipboard(
|
||||
TemplateField.getLocalizedName(context, field.name),
|
||||
field.protectedValue.stringValue,
|
||||
getString(
|
||||
R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, field.name)
|
||||
)
|
||||
field.protectedValue.isProtected
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -251,8 +248,7 @@ class EntryFragment: DatabaseFragment() {
|
||||
|
||||
fun launchEntryCopyEducationAction() {
|
||||
val appNameString = getString(R.string.app_name)
|
||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString, appNameString)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -78,9 +78,11 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(context)
|
||||
passphraseCopyView?.setOnClickListener {
|
||||
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
clipboardHelper.timeoutCopyToClipboard(
|
||||
getString(R.string.passphrase),
|
||||
passKeyView.passwordString,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
wordCaseAdapter = ArrayAdapter(context,
|
||||
|
||||
@@ -99,9 +99,11 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(context)
|
||||
passwordCopyView?.setOnClickListener {
|
||||
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
clipboardHelper.timeoutCopyToClipboard(
|
||||
getString(R.string.password),
|
||||
passKeyView.passwordString,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ object EntrySelectionHelper {
|
||||
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||
}
|
||||
|
||||
fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||
registerInfo?.let {
|
||||
intent.putExtra(KEY_REGISTER_INFO, it)
|
||||
}
|
||||
@@ -113,7 +113,7 @@ object EntrySelectionHelper {
|
||||
?: SpecialMode.DEFAULT
|
||||
}
|
||||
|
||||
fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class ExternalFileHelper {
|
||||
|
||||
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
|
||||
|
||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||
val resultCallback = ActivityResultCallback<Uri?> { result ->
|
||||
result?.let { uri ->
|
||||
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
||||
onFileSelected?.invoke(uri)
|
||||
@@ -91,7 +91,7 @@ class ExternalFileHelper {
|
||||
fun buildCreateDocument(typeString: String = "application/octet-stream",
|
||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||
|
||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||
val resultCallback = ActivityResultCallback<Uri?> { result ->
|
||||
onFileCreated.invoke(result)
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ class ExternalFileHelper {
|
||||
|
||||
class OpenDocument : ActivityResultContracts.OpenDocument() {
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun createIntent(context: Context, input: Array<out String>): Intent {
|
||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
@@ -178,11 +178,10 @@ class ExternalFileHelper {
|
||||
}
|
||||
}
|
||||
|
||||
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
|
||||
class CreateDocument(typeString: String) : ActivityResultContracts.CreateDocument(typeString) {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import androidx.activity.viewModels
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
|
||||
@@ -20,7 +20,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
|
||||
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
val databaseWasReloaded = database?.wasReloaded == true
|
||||
@@ -36,6 +36,17 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun showDatabaseDialog(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mDatabaseTaskProvider?.destroy()
|
||||
mDatabaseTaskProvider = null
|
||||
mDatabase = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
mDatabase = database
|
||||
mDatabaseViewModel.defineDatabase(database)
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -42,7 +43,7 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
@@ -89,8 +90,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.mergeDatabase.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||
@@ -226,6 +227,9 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
// Reload the current activity
|
||||
if (result.isSuccess) {
|
||||
reloadActivity()
|
||||
if (actionTask == DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK) {
|
||||
Toast.makeText(this, R.string.merge_success, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
this.showActionErrorIfNeeded(result)
|
||||
finish()
|
||||
@@ -269,11 +273,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
fun mergeDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential)
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
|
||||
}
|
||||
|
||||
fun reloadDatabase() {
|
||||
@@ -433,9 +437,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
|
||||
protected fun lockAndExit() {
|
||||
// Ask confirmation if modification not saved
|
||||
if (mDatabase?.isReadOnly == false
|
||||
&& mDatabase?.dataModifiedSinceLastLoading == true
|
||||
&& !PreferencesUtil.isAutoSaveDatabaseEnabled(this)) {
|
||||
if (mDatabase?.dataModifiedSinceLastLoading == true) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.discard_changes)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
@@ -480,25 +482,33 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
||||
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
||||
setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
||||
databaseLoaded ?: false)
|
||||
try {
|
||||
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
||||
setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
|
||||
context,
|
||||
databaseLoaded ?: false
|
||||
)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
setOnFocusChangeListener { _, _ ->
|
||||
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
|
||||
context,
|
||||
databaseLoaded ?: false
|
||||
)
|
||||
}
|
||||
if (this is ViewGroup) {
|
||||
for (i in 0..childCount) {
|
||||
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
setOnFocusChangeListener { _, _ ->
|
||||
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
||||
databaseLoaded ?: false)
|
||||
}
|
||||
if (this is ViewGroup) {
|
||||
for (i in 0..childCount) {
|
||||
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppTimeout", "Unable to reset app timeout", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.kunzisoft.keepass.activities.legacy
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -96,9 +93,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
|
||||
private fun backToTheMainAppAndFinish() {
|
||||
// To move the app in background and return to the main app
|
||||
// Not visible as opened with FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||
moveTaskToBack(true)
|
||||
// Not finish() to prevent service kill
|
||||
// Not using FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or finish() because kills the service
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -21,14 +21,21 @@ package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
/**
|
||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||
@@ -81,8 +88,24 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
setTheme(themeId)
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.registerOnSharedPreferenceChangeListener(onScreenshotModePrefListener)
|
||||
}
|
||||
private val onScreenshotModePrefListener = OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key != getString(R.string.enable_screenshot_mode_key)) return@OnSharedPreferenceChangeListener
|
||||
|
||||
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
|
||||
}
|
||||
|
||||
private fun setScreenshotMode(isEnabled: Boolean) {
|
||||
findViewById<View>(R.id.screenshot_mode_banner)?.visibility = if (isEnabled) VISIBLE else GONE
|
||||
|
||||
// Several gingerbread devices have problems with FLAG_SECURE
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
if (isEnabled) {
|
||||
window.clearFlags(FLAG_SECURE)
|
||||
} else {
|
||||
window.setFlags(FLAG_SECURE, FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -94,6 +117,7 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||
recreateActivity()
|
||||
}
|
||||
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
|
||||
}
|
||||
|
||||
private fun recreateActivity() {
|
||||
|
||||
@@ -21,13 +21,13 @@ package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
@@ -521,13 +521,14 @@ class NodesAdapter (private val context: Context,
|
||||
}
|
||||
holder?.otpContainer?.setOnClickListener {
|
||||
otpElement?.token?.let { token ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
mClipboardHelper.copyToClipboard(token)
|
||||
try {
|
||||
mClipboardHelper.copyToClipboard(
|
||||
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN),
|
||||
token
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to copy the OTP token", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,15 @@ import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import android.content.Context
|
||||
import androidx.room.AutoMigration
|
||||
|
||||
@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class])
|
||||
@Database(
|
||||
version = 2,
|
||||
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
|
||||
autoMigrations = [
|
||||
AutoMigration (from = 1, to = 2)
|
||||
]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app.database
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.DatabaseFile
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||
@@ -44,6 +45,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
DatabaseFile(
|
||||
databaseUri,
|
||||
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
|
||||
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
|
||||
fileDatabaseInfo.exists,
|
||||
@@ -85,13 +87,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
|| !hideBrokenLocations) {
|
||||
databaseFileListLoaded.add(
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
|
||||
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -107,11 +110,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
|
||||
fun addOrUpdateDatabaseUri(databaseUri: Uri,
|
||||
keyFileUri: Uri? = null,
|
||||
hardwareKey: HardwareKey? = null,
|
||||
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||
addOrUpdateDatabaseFile(DatabaseFile(
|
||||
databaseUri,
|
||||
keyFileUri
|
||||
databaseUri,
|
||||
keyFileUri,
|
||||
hardwareKey
|
||||
), databaseFileAddedOrUpdatedResult)
|
||||
}
|
||||
|
||||
@@ -130,6 +136,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
||||
?: "",
|
||||
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||
databaseFileToAddOrUpdate.hardwareKey?.value,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -147,13 +154,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
|
||||
fileDatabaseHistory.databaseUri)
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -172,10 +180,11 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
|
||||
if (returnValue > 0) {
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
databaseFileToDelete.databaseAlias
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
databaseFileToDelete.databaseAlias
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -35,6 +35,9 @@ data class FileDatabaseHistoryEntity(
|
||||
@ColumnInfo(name = "keyfile_uri")
|
||||
var keyFileUri: String?,
|
||||
|
||||
@ColumnInfo(name = "hardware_key")
|
||||
var hardwareKey: String?,
|
||||
|
||||
@ColumnInfo(name = "updated")
|
||||
val updated: Long
|
||||
) {
|
||||
|
||||
@@ -34,12 +34,10 @@ import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
@@ -398,7 +396,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
}
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
} ?: throw IODatabaseException()
|
||||
} ?: throw UnknownDatabaseLocationException()
|
||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||
}
|
||||
|
||||
|
||||
@@ -403,13 +403,11 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun isDeviceSecure(context: Context): Boolean {
|
||||
val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
||||
return keyguardManager?.isDeviceSecure ?: false
|
||||
return ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
||||
?.isDeviceSecure ?: false
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun biometricUnlockSupported(context: Context): Boolean {
|
||||
val biometricCanAuthenticate = try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
||||
@@ -430,28 +428,23 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
||||
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
(biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||
)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ContextCompat.getSystemService(context, KeyguardManager::class.java)?.apply {
|
||||
return isDeviceSecure
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entry key in keystore
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
|
||||
advancedCallback: AdvancedUnlockErrorCallback) {
|
||||
AdvancedUnlockManager{ fragmentActivity }.apply {
|
||||
|
||||
@@ -24,15 +24,16 @@ import android.net.Uri
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
|
||||
open class AssignMainCredentialInDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
protected val mDatabaseUri: Uri,
|
||||
protected val mMainCredential: MainCredential)
|
||||
: SaveDatabaseRunnable(context, database, true) {
|
||||
context: Context,
|
||||
database: Database,
|
||||
protected val mDatabaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: SaveDatabaseRunnable(context, database, true, mainCredential, challengeResponseRetriever) {
|
||||
|
||||
private var mBackupKey: ByteArray? = null
|
||||
|
||||
@@ -40,10 +41,7 @@ open class AssignMainCredentialInDatabaseRunnable (
|
||||
// Set key
|
||||
try {
|
||||
mBackupKey = ByteArray(database.masterKey.size)
|
||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||
|
||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
||||
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||
database.masterKey.copyInto(mBackupKey!!)
|
||||
} catch (e: Exception) {
|
||||
erase(mBackupKey)
|
||||
setError(e)
|
||||
|
||||
@@ -24,7 +24,8 @@ import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
class CreateDatabaseRunnable(context: Context,
|
||||
@@ -33,9 +34,10 @@ class CreateDatabaseRunnable(context: Context,
|
||||
private val databaseName: String,
|
||||
private val rootName: String,
|
||||
private val templateGroupName: String?,
|
||||
mainCredential: MainCredential,
|
||||
val mainCredential: MainCredential,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
||||
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential, challengeResponseRetriever) {
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
@@ -58,8 +60,11 @@ class CreateDatabaseRunnable(context: Context,
|
||||
// Add database to recent files
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
||||
.addOrUpdateDatabaseUri(
|
||||
mDatabaseUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mainCredential.keyFileUri else null,
|
||||
if (PreferencesUtil.rememberHardwareKey(context)) mainCredential.hardwareKey else null,
|
||||
)
|
||||
}
|
||||
|
||||
// Register the current time to init the lock timer
|
||||
@@ -72,6 +77,9 @@ class CreateDatabaseRunnable(context: Context,
|
||||
override fun onFinishRun() {
|
||||
super.onFinishRun()
|
||||
|
||||
if (result.isSuccess) {
|
||||
mDatabase.loaded = true
|
||||
}
|
||||
createDatabaseResult?.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.app.Service
|
||||
import android.content.*
|
||||
import android.content.Context.*
|
||||
import android.net.Uri
|
||||
@@ -38,14 +37,16 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.model.ProgressMessage
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||
@@ -89,11 +90,12 @@ import java.util.*
|
||||
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
|
||||
* Useful to retrieve a database instance and sending tasks commands
|
||||
*/
|
||||
class DatabaseTaskProvider {
|
||||
class DatabaseTaskProvider(private var context: Context,
|
||||
private var showDialog: Boolean = true) {
|
||||
|
||||
private var activity: FragmentActivity? = null
|
||||
private var service: Service? = null
|
||||
private var context: Context
|
||||
// To show dialog only if context is an activity
|
||||
private var activity: FragmentActivity? = try { context as? FragmentActivity? }
|
||||
catch (_: Exception) { null }
|
||||
|
||||
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
|
||||
|
||||
@@ -101,7 +103,10 @@ class DatabaseTaskProvider {
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result) -> Unit)? = null
|
||||
|
||||
private var intentDatabaseTask: Intent
|
||||
private var intentDatabaseTask: Intent = Intent(
|
||||
context.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java
|
||||
)
|
||||
|
||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||
@@ -111,30 +116,33 @@ class DatabaseTaskProvider {
|
||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||
|
||||
constructor(activity: FragmentActivity) {
|
||||
this.activity = activity
|
||||
this.context = activity
|
||||
this.intentDatabaseTask = Intent(activity.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java)
|
||||
}
|
||||
|
||||
constructor(service: Service) {
|
||||
this.service = service
|
||||
this.context = service
|
||||
this.intentDatabaseTask = Intent(service.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java)
|
||||
fun destroy() {
|
||||
this.activity = null
|
||||
this.onDatabaseRetrieved = null
|
||||
this.onActionFinish = null
|
||||
this.databaseTaskBroadcastReceiver = null
|
||||
this.mBinder = null
|
||||
this.serviceConnection = null
|
||||
this.progressTaskDialogFragment = null
|
||||
this.databaseChangedDialogFragment = null
|
||||
}
|
||||
|
||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
startDialog(titleId, messageId, warningId)
|
||||
override fun onStartAction(database: Database,
|
||||
progressMessage: ProgressMessage) {
|
||||
if (showDialog)
|
||||
startDialog(progressMessage)
|
||||
}
|
||||
|
||||
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
updateDialog(titleId, messageId, warningId)
|
||||
override fun onUpdateAction(database: Database,
|
||||
progressMessage: ProgressMessage) {
|
||||
if (showDialog)
|
||||
updateDialog(progressMessage)
|
||||
}
|
||||
|
||||
override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) {
|
||||
override fun onStopAction(database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result) {
|
||||
onActionFinish?.invoke(database, actionTask, result)
|
||||
// Remove the progress task
|
||||
stopDialog()
|
||||
@@ -181,9 +189,7 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDialog(titleId: Int? = null,
|
||||
messageId: Int? = null,
|
||||
warningId: Int? = null) {
|
||||
private fun startDialog(progressMessage: ProgressMessage) {
|
||||
activity?.let { activity ->
|
||||
activity.lifecycleScope.launch {
|
||||
if (progressTaskDialogFragment == null) {
|
||||
@@ -197,22 +203,17 @@ class DatabaseTaskProvider {
|
||||
PROGRESS_TASK_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
updateDialog(titleId, messageId, warningId)
|
||||
updateDialog(progressMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
private fun updateDialog(progressMessage: ProgressMessage) {
|
||||
progressTaskDialogFragment?.apply {
|
||||
titleId?.let {
|
||||
updateTitle(it)
|
||||
}
|
||||
messageId?.let {
|
||||
updateMessage(it)
|
||||
}
|
||||
warningId?.let {
|
||||
updateWarning(it)
|
||||
}
|
||||
updateTitle(progressMessage.titleId)
|
||||
updateMessage(progressMessage.messageId)
|
||||
updateWarning(progressMessage.warningId)
|
||||
setCancellable(progressMessage.cancelable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,9 +227,7 @@ class DatabaseTaskProvider {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||
addDatabaseListener(databaseListener)
|
||||
addDatabaseFileInfoListener(databaseInfoListener)
|
||||
addActionTaskListener(actionTaskListener)
|
||||
addServiceListeners(this)
|
||||
getService().checkDatabase()
|
||||
getService().checkDatabaseInfo()
|
||||
getService().checkAction()
|
||||
@@ -236,19 +235,29 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeDatabaseListener(databaseListener)
|
||||
removeServiceListeners(mBinder)
|
||||
mBinder = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||
service?.addDatabaseListener(databaseListener)
|
||||
service?.addDatabaseFileInfoListener(databaseInfoListener)
|
||||
service?.addActionTaskListener(actionTaskListener)
|
||||
}
|
||||
|
||||
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||
service?.removeActionTaskListener(actionTaskListener)
|
||||
service?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
service?.removeDatabaseListener(databaseListener)
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
initServiceConnection()
|
||||
serviceConnection?.let {
|
||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
|
||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,10 +271,6 @@ class DatabaseTaskProvider {
|
||||
serviceConnection = null
|
||||
}
|
||||
|
||||
fun isBinded(): Boolean {
|
||||
return mBinder != null
|
||||
}
|
||||
|
||||
fun registerProgressTask() {
|
||||
stopDialog()
|
||||
|
||||
@@ -299,9 +304,7 @@ class DatabaseTaskProvider {
|
||||
fun unregisterProgressTask() {
|
||||
stopDialog()
|
||||
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeDatabaseListener(databaseListener)
|
||||
removeServiceListeners(mBinder)
|
||||
mBinder = null
|
||||
|
||||
unBindService()
|
||||
@@ -321,7 +324,7 @@ class DatabaseTaskProvider {
|
||||
context.startService(intentDatabaseTask)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to perform database action", e)
|
||||
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +335,8 @@ class DatabaseTaskProvider {
|
||||
*/
|
||||
|
||||
fun startDatabaseCreate(databaseUri: Uri,
|
||||
mainCredential: MainCredential) {
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
@@ -355,9 +359,11 @@ class DatabaseTaskProvider {
|
||||
, ACTION_DATABASE_LOAD_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseMerge(fromDatabaseUri: Uri? = null,
|
||||
fun startDatabaseMerge(save: Boolean,
|
||||
fromDatabaseUri: Uri? = null,
|
||||
mainCredential: MainCredential? = null) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
}
|
||||
@@ -385,7 +391,8 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
|
||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||
mainCredential: MainCredential) {
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
@@ -702,6 +709,13 @@ class DatabaseTaskProvider {
|
||||
, ACTION_DATABASE_SAVE)
|
||||
}
|
||||
|
||||
fun startChallengeResponded(response: ByteArray?) {
|
||||
start(Bundle().apply {
|
||||
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
|
||||
}
|
||||
, ACTION_CHALLENGE_RESPONDED)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DatabaseTaskProvider::class.java.name
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseInputException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
@@ -35,8 +36,9 @@ import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class LoadDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
private val mUri: Uri,
|
||||
private val mDatabaseUri: Uri,
|
||||
private val mMainCredential: MainCredential,
|
||||
private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray,
|
||||
private val mReadonly: Boolean,
|
||||
private val mCipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
private val mFixDuplicateUUID: Boolean,
|
||||
@@ -51,18 +53,21 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.loadData(mUri,
|
||||
mMainCredential,
|
||||
mReadonly,
|
||||
context.contentResolver,
|
||||
UriUtil.getBinaryDir(context),
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater)
|
||||
mDatabase.loadData(
|
||||
context.contentResolver,
|
||||
mDatabaseUri,
|
||||
mMainCredential,
|
||||
mChallengeResponseRetriever,
|
||||
mReadonly,
|
||||
UriUtil.getBinaryDir(context),
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater
|
||||
)
|
||||
}
|
||||
catch (e: LoadDatabaseException) {
|
||||
catch (e: DatabaseInputException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
@@ -70,8 +75,11 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
// Save keyFile in app database
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
FileDatabaseHistoryAction.getInstance(context)
|
||||
.addOrUpdateDatabaseUri(mUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
||||
.addOrUpdateDatabaseUri(
|
||||
mDatabaseUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null,
|
||||
if (PreferencesUtil.rememberHardwareKey(context)) mMainCredential.hardwareKey else null,
|
||||
)
|
||||
}
|
||||
|
||||
// Register the biometric
|
||||
|
||||
@@ -22,36 +22,43 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
|
||||
class MergeDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
private val mDatabaseToMergeUri: Uri?,
|
||||
private val mDatabaseToMergeMainCredential: MainCredential?,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
class MergeDatabaseRunnable(
|
||||
context: Context,
|
||||
private val mDatabaseToMergeUri: Uri?,
|
||||
private val mDatabaseToMergeMainCredential: MainCredential?,
|
||||
private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
database: Database,
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
|
||||
|
||||
override fun onStartRun() {
|
||||
mDatabase.wasReloaded = true
|
||||
database.wasReloaded = true
|
||||
super.onStartRun()
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.mergeData(mDatabaseToMergeUri,
|
||||
mDatabaseToMergeMainCredential,
|
||||
database.mergeData(
|
||||
context.contentResolver,
|
||||
mDatabaseToMergeUri,
|
||||
mDatabaseToMergeMainCredential,
|
||||
mDatabaseToMergeChallengeResponseRetriever,
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
progressTaskUpdater
|
||||
)
|
||||
} catch (e: LoadDatabaseException) {
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
@@ -59,9 +66,11 @@ class MergeDatabaseRunnable(private val context: Context,
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
}
|
||||
super.onActionRun()
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
super.onFinishRun()
|
||||
mLoadDatabaseResult?.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
@@ -47,7 +47,7 @@ class ReloadDatabaseRunnable(private val context: Context,
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
progressTaskUpdater)
|
||||
} catch (e: LoadDatabaseException) {
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class RemoveUnlinkedDataDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
|
||||
@@ -23,11 +23,15 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
open class SaveDatabaseRunnable(protected var context: Context,
|
||||
protected var database: Database,
|
||||
private var saveDatabase: Boolean,
|
||||
private var mainCredential: MainCredential?, // If null, uses composite Key
|
||||
private var challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
private var databaseCopyUri: Uri? = null)
|
||||
: ActionRunnable() {
|
||||
|
||||
@@ -39,7 +43,12 @@ open class SaveDatabaseRunnable(protected var context: Context,
|
||||
database.checkVersion()
|
||||
if (saveDatabase && result.isSuccess) {
|
||||
try {
|
||||
database.saveData(databaseCopyUri, context.contentResolver)
|
||||
database.saveData(
|
||||
context.contentResolver,
|
||||
context.cacheDir,
|
||||
databaseCopyUri,
|
||||
mainCredential,
|
||||
challengeResponseRetriever)
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
@@ -22,14 +22,16 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class UpdateCompressionBinariesDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val oldCompressionAlgorithm: CompressionAlgorithm,
|
||||
private val newCompressionAlgorithm: CompressionAlgorithm,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
|
||||
|
||||
override fun onStartRun() {
|
||||
// Set new compression
|
||||
|
||||
@@ -23,14 +23,16 @@ import android.content.Context
|
||||
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class DeleteEntryHistoryDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val mainEntry: Entry,
|
||||
private val entryHistoryPosition: Int,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.content.Context
|
||||
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class RestoreEntryHistoryDatabaseRunnable (
|
||||
@@ -30,7 +31,8 @@ class RestoreEntryHistoryDatabaseRunnable (
|
||||
private val database: Database,
|
||||
private val mainEntry: Entry,
|
||||
private val entryHistoryPosition: Int,
|
||||
private val saveDatabase: Boolean)
|
||||
private val saveDatabase: Boolean,
|
||||
private val challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionRunnable() {
|
||||
|
||||
private var updateEntryRunnable: UpdateEntryRunnable? = null
|
||||
@@ -43,12 +45,15 @@ class RestoreEntryHistoryDatabaseRunnable (
|
||||
historyToRestore.addEntryToHistory(it)
|
||||
}
|
||||
// Update the entry with the fresh formatted entry to restore
|
||||
updateEntryRunnable = UpdateEntryRunnable(context,
|
||||
database,
|
||||
mainEntry,
|
||||
historyToRestore,
|
||||
saveDatabase,
|
||||
null)
|
||||
updateEntryRunnable = UpdateEntryRunnable(
|
||||
context,
|
||||
database,
|
||||
mainEntry,
|
||||
historyToRestore,
|
||||
saveDatabase,
|
||||
null,
|
||||
challengeResponseRetriever
|
||||
)
|
||||
|
||||
updateEntryRunnable?.onStartRun()
|
||||
|
||||
|
||||
@@ -22,13 +22,15 @@ package com.kunzisoft.keepass.database.action.node
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
abstract class ActionNodeDatabaseRunnable(
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
save: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, save) {
|
||||
save: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: SaveDatabaseRunnable(context, database, save, null, challengeResponseRetriever) {
|
||||
|
||||
/**
|
||||
* Function do to a node action
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class AddEntryRunnable constructor(
|
||||
context: Context,
|
||||
@@ -31,8 +32,9 @@ class AddEntryRunnable constructor(
|
||||
private val mNewEntry: Entry,
|
||||
private val mParent: Group,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
override fun nodeAction() {
|
||||
mNewEntry.touch(modified = true, touchParents = true)
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class AddGroupRunnable constructor(
|
||||
context: Context,
|
||||
@@ -30,8 +31,9 @@ class AddGroupRunnable constructor(
|
||||
private val mNewGroup: Group,
|
||||
private val mParent: Group,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
override fun nodeAction() {
|
||||
mNewGroup.touch(modified = true, touchParents = true)
|
||||
|
||||
@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class CopyNodesRunnable constructor(
|
||||
context: Context,
|
||||
@@ -33,8 +36,9 @@ class CopyNodesRunnable constructor(
|
||||
private val mNodesToCopy: List<Node>,
|
||||
private val mNewParent: Group,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
private var mEntriesCopied = ArrayList<Entry>()
|
||||
|
||||
|
||||
@@ -20,16 +20,20 @@
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class DeleteNodesRunnable(context: Context,
|
||||
database: Database,
|
||||
private val mNodesToDelete: List<Node>,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
private var mOldParent: Group? = null
|
||||
private var mCanRecycle: Boolean = false
|
||||
|
||||
@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class MoveNodesRunnable constructor(
|
||||
context: Context,
|
||||
@@ -33,8 +36,9 @@ class MoveNodesRunnable constructor(
|
||||
private val mNodesToMove: List<Node>,
|
||||
private val mNewParent: Group,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
private var mOldParent: Group? = null
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class UpdateEntryRunnable constructor(
|
||||
context: Context,
|
||||
@@ -31,8 +32,9 @@ class UpdateEntryRunnable constructor(
|
||||
private val mOldEntry: Entry,
|
||||
private val mNewEntry: Entry,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
override fun nodeAction() {
|
||||
if (mOldEntry.nodeId == mNewEntry.nodeId) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class UpdateGroupRunnable constructor(
|
||||
context: Context,
|
||||
@@ -30,8 +31,9 @@ class UpdateGroupRunnable constructor(
|
||||
private val mOldGroup: Group,
|
||||
private val mNewGroup: Group,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
override fun nodeAction() {
|
||||
if (mOldGroup.nodeId == mNewGroup.nodeId) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -54,12 +54,10 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
||||
import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.readBytes4ToUInt
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
|
||||
@@ -73,7 +71,7 @@ class Database {
|
||||
var fileUri: Uri? = null
|
||||
private set
|
||||
|
||||
private var mSearchHelper: SearchHelper? = null
|
||||
private var mSearchHelper: SearchHelper = SearchHelper()
|
||||
|
||||
var isReadOnly = false
|
||||
|
||||
@@ -384,10 +382,14 @@ class Database {
|
||||
set(masterKey) {
|
||||
mDatabaseKDB?.masterKey = masterKey
|
||||
mDatabaseKDBX?.masterKey = masterKey
|
||||
mDatabaseKDBX?.keyLastChanged = DateInstant()
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val transformSeed: ByteArray?
|
||||
get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed
|
||||
|
||||
var rootGroup: Group?
|
||||
get() {
|
||||
mDatabaseKDB?.rootGroup?.let {
|
||||
@@ -553,83 +555,31 @@ class Database {
|
||||
setDatabaseKDBX(newDatabase)
|
||||
this.fileUri = databaseUri
|
||||
// Set Database state
|
||||
this.loaded = true
|
||||
this.dataModifiedSinceLastLoading = false
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
|
||||
openDatabaseKDB: (InputStream) -> DatabaseKDB,
|
||||
openDatabaseKDBX: (InputStream) -> DatabaseKDBX) {
|
||||
var databaseInputStream: InputStream? = null
|
||||
try {
|
||||
// Load Data, pass Uris as InputStreams
|
||||
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri)
|
||||
?: throw IOException("Database input stream cannot be retrieve")
|
||||
|
||||
databaseInputStream = BufferedInputStream(databaseStream)
|
||||
if (!databaseInputStream.markSupported()) {
|
||||
throw IOException("Input stream does not support mark.")
|
||||
}
|
||||
|
||||
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
|
||||
databaseInputStream.mark(10)
|
||||
|
||||
// Get the file directory to save the attachments
|
||||
val sig1 = databaseInputStream.readBytes4ToUInt()
|
||||
val sig2 = databaseInputStream.readBytes4ToUInt()
|
||||
|
||||
// Return to the start
|
||||
databaseInputStream.reset()
|
||||
|
||||
when {
|
||||
// Header of database KDB
|
||||
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(openDatabaseKDB(databaseInputStream))
|
||||
|
||||
// Header of database KDBX
|
||||
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(openDatabaseKDBX(databaseInputStream))
|
||||
|
||||
// Header not recognized
|
||||
else -> throw SignatureDatabaseException()
|
||||
}
|
||||
|
||||
this.mSearchHelper = SearchHelper()
|
||||
loaded = true
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
} finally {
|
||||
databaseInputStream?.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun loadData(uri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
contentResolver: ContentResolver,
|
||||
cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
fixDuplicateUUID: Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
@Throws(DatabaseInputException::class)
|
||||
fun loadData(
|
||||
contentResolver: ContentResolver,
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
readOnly: Boolean,
|
||||
cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
fixDuplicateUUID: Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?
|
||||
) {
|
||||
|
||||
// Save database URI
|
||||
this.fileUri = uri
|
||||
this.fileUri = databaseUri
|
||||
|
||||
// Check if the file is writable
|
||||
this.isReadOnly = readOnly
|
||||
|
||||
// Pass KeyFile Uri as InputStreams
|
||||
var keyFileInputStream: InputStream? = null
|
||||
try {
|
||||
// Get keyFile inputStream
|
||||
mainCredential.keyFileUri?.let { keyFile ->
|
||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
|
||||
}
|
||||
|
||||
// Read database stream for the first time
|
||||
readDatabaseStream(contentResolver, uri,
|
||||
readDatabaseStream(contentResolver, databaseUri,
|
||||
{ databaseInputStream ->
|
||||
val databaseKDB = DatabaseKDB().apply {
|
||||
binaryCache.cacheDirectory = cacheDirectory
|
||||
@@ -639,12 +589,12 @@ class Database {
|
||||
.openDatabase(databaseInputStream,
|
||||
progressTaskUpdater
|
||||
) {
|
||||
databaseKDB.retrieveMasterKey(
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream
|
||||
databaseKDB.deriveMasterKey(
|
||||
contentResolver,
|
||||
mainCredential
|
||||
)
|
||||
}
|
||||
databaseKDB
|
||||
setDatabaseKDB(databaseKDB)
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
val databaseKDBX = DatabaseKDBX().apply {
|
||||
@@ -655,23 +605,23 @@ class Database {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream,
|
||||
progressTaskUpdater) {
|
||||
databaseKDBX.retrieveMasterKey(
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
databaseKDBX.deriveMasterKey(
|
||||
contentResolver,
|
||||
mainCredential,
|
||||
challengeResponseRetriever
|
||||
)
|
||||
}
|
||||
}
|
||||
databaseKDBX
|
||||
setDatabaseKDBX(databaseKDBX)
|
||||
}
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
loaded = true
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
Log.e(TAG, "Unable to load the database")
|
||||
if (e is DatabaseInputException)
|
||||
throw e
|
||||
throw DatabaseInputException(e)
|
||||
} finally {
|
||||
keyFileInputStream?.close()
|
||||
dataModifiedSinceLastLoading = false
|
||||
}
|
||||
}
|
||||
@@ -680,48 +630,44 @@ class Database {
|
||||
return mDatabaseKDBX != null
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun mergeData(databaseToMergeUri: Uri?,
|
||||
databaseToMergeMainCredential: MainCredential?,
|
||||
contentResolver: ContentResolver,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
@Throws(DatabaseInputException::class)
|
||||
fun mergeData(
|
||||
contentResolver: ContentResolver,
|
||||
databaseToMergeUri: Uri?,
|
||||
databaseToMergeMainCredential: MainCredential?,
|
||||
databaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?
|
||||
) {
|
||||
|
||||
mDatabaseKDB?.let {
|
||||
throw IODatabaseException("Unable to merge from a database V1")
|
||||
throw MergeDatabaseKDBException()
|
||||
}
|
||||
|
||||
// New database instance to get new changes
|
||||
val databaseToMerge = Database()
|
||||
databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri
|
||||
|
||||
// Pass KeyFile Uri as InputStreams
|
||||
var keyFileInputStream: InputStream? = null
|
||||
try {
|
||||
val databaseUri = databaseToMerge.fileUri
|
||||
if (databaseUri != null) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
// Get keyFile inputStream
|
||||
databaseToMergeMainCredential.keyFileUri?.let { keyFile ->
|
||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
|
||||
}
|
||||
}
|
||||
|
||||
databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
|
||||
readDatabaseStream(contentResolver, databaseUri,
|
||||
{ databaseInputStream ->
|
||||
val databaseToMergeKDB = DatabaseKDB()
|
||||
DatabaseInputKDB(databaseToMergeKDB)
|
||||
.openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
databaseToMergeKDB.retrieveMasterKey(
|
||||
databaseToMergeMainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
databaseToMergeKDB.deriveMasterKey(
|
||||
contentResolver,
|
||||
databaseToMergeMainCredential
|
||||
)
|
||||
} else {
|
||||
databaseToMergeKDB.masterKey = masterKey
|
||||
this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
|
||||
databaseToMergeKDB.copyMasterKeyFrom(thisDatabaseKDB)
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseToMergeKDB
|
||||
databaseToMerge.setDatabaseKDB(databaseToMergeKDB)
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
val databaseToMergeKDBX = DatabaseKDBX()
|
||||
@@ -729,18 +675,22 @@ class Database {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
databaseToMergeKDBX.retrieveMasterKey(
|
||||
databaseToMergeMainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
databaseToMergeKDBX.deriveMasterKey(
|
||||
contentResolver,
|
||||
databaseToMergeMainCredential,
|
||||
databaseToMergeChallengeResponseRetriever
|
||||
)
|
||||
} else {
|
||||
databaseToMergeKDBX.masterKey = masterKey
|
||||
this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
|
||||
databaseToMergeKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseToMergeKDBX
|
||||
databaseToMerge.setDatabaseKDBX(databaseToMergeKDBX)
|
||||
}
|
||||
)
|
||||
loaded = true
|
||||
|
||||
mDatabaseKDBX?.let { currentDatabaseKDBX ->
|
||||
val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply {
|
||||
@@ -760,24 +710,24 @@ class Database {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw IODatabaseException("Database URI is null, database cannot be merged")
|
||||
throw UnknownDatabaseLocationException()
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
Log.e(TAG, "Unable to merge the database")
|
||||
if (e is DatabaseException)
|
||||
throw e
|
||||
throw DatabaseInputException(e)
|
||||
} finally {
|
||||
keyFileInputStream?.close()
|
||||
databaseToMerge.clearAndClose()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun reloadData(contentResolver: ContentResolver,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
@Throws(DatabaseInputException::class)
|
||||
fun reloadData(
|
||||
contentResolver: ContentResolver,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?
|
||||
) {
|
||||
|
||||
// Retrieve the stream from the old database URI
|
||||
try {
|
||||
@@ -791,9 +741,11 @@ class Database {
|
||||
}
|
||||
DatabaseInputKDB(databaseKDB)
|
||||
.openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
databaseKDB.masterKey = masterKey
|
||||
this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
|
||||
databaseKDB.copyMasterKeyFrom(thisDatabaseKDB)
|
||||
}
|
||||
}
|
||||
databaseKDB
|
||||
setDatabaseKDB(databaseKDB)
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
val databaseKDBX = DatabaseKDBX()
|
||||
@@ -803,26 +755,144 @@ class Database {
|
||||
DatabaseInputKDBX(databaseKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
databaseKDBX.masterKey = masterKey
|
||||
this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
|
||||
databaseKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseKDBX
|
||||
setDatabaseKDBX(databaseKDBX)
|
||||
}
|
||||
)
|
||||
loaded = true
|
||||
} else {
|
||||
throw IODatabaseException("Database URI is null, database cannot be reloaded")
|
||||
throw UnknownDatabaseLocationException()
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
Log.e(TAG, "Unable to reload the database")
|
||||
if (e is DatabaseException)
|
||||
throw e
|
||||
throw DatabaseInputException(e)
|
||||
} finally {
|
||||
dataModifiedSinceLastLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun readDatabaseStream(contentResolver: ContentResolver,
|
||||
databaseUri: Uri,
|
||||
openDatabaseKDB: (InputStream) -> Unit,
|
||||
openDatabaseKDBX: (InputStream) -> Unit) {
|
||||
try {
|
||||
// Load Data, pass Uris as InputStreams
|
||||
val databaseStream = UriUtil.getUriInputStream(contentResolver, databaseUri)
|
||||
?: throw UnknownDatabaseLocationException()
|
||||
|
||||
BufferedInputStream(databaseStream).use { databaseInputStream ->
|
||||
|
||||
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
|
||||
databaseInputStream.mark(10)
|
||||
|
||||
// Get the file directory to save the attachments
|
||||
val sig1 = databaseInputStream.readBytes4ToUInt()
|
||||
val sig2 = databaseInputStream.readBytes4ToUInt()
|
||||
|
||||
// Return to the start
|
||||
databaseInputStream.reset()
|
||||
|
||||
when {
|
||||
// Header of database KDB
|
||||
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> openDatabaseKDB(
|
||||
databaseInputStream
|
||||
)
|
||||
// Header of database KDBX
|
||||
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> openDatabaseKDBX(
|
||||
databaseInputStream
|
||||
)
|
||||
// Header not recognized
|
||||
else -> throw SignatureDatabaseException()
|
||||
}
|
||||
}
|
||||
} catch (fileNotFoundException : FileNotFoundException) {
|
||||
throw FileNotFoundDatabaseException()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
fun saveData(contentResolver: ContentResolver,
|
||||
cacheDir: File,
|
||||
databaseCopyUri: Uri?,
|
||||
mainCredential: MainCredential?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) {
|
||||
val saveUri = databaseCopyUri ?: this.fileUri
|
||||
// Build temp database file to avoid file corruption if error
|
||||
val cacheFile = File(cacheDir, saveUri.hashCode().toString())
|
||||
try {
|
||||
if (saveUri != null) {
|
||||
// Save in a temp memory to avoid exception
|
||||
cacheFile.outputStream().use { outputStream ->
|
||||
mDatabaseKDB?.let { databaseKDB ->
|
||||
DatabaseOutputKDB(databaseKDB).apply {
|
||||
writeDatabase(outputStream) {
|
||||
if (mainCredential != null) {
|
||||
databaseKDB.deriveMasterKey(
|
||||
contentResolver,
|
||||
mainCredential
|
||||
)
|
||||
} else {
|
||||
// No master key change
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?: mDatabaseKDBX?.let { databaseKDBX ->
|
||||
DatabaseOutputKDBX(databaseKDBX).apply {
|
||||
writeDatabase(outputStream) {
|
||||
if (mainCredential != null) {
|
||||
// Build new master key from MainCredential
|
||||
databaseKDBX.deriveMasterKey(
|
||||
contentResolver,
|
||||
mainCredential,
|
||||
challengeResponseRetriever
|
||||
)
|
||||
} else {
|
||||
// Reuse composite key parts
|
||||
databaseKDBX.deriveCompositeKey(
|
||||
challengeResponseRetriever
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copy from the cache to the final stream
|
||||
UriUtil.getUriOutputStream(contentResolver, saveUri)?.use { outputStream ->
|
||||
cacheFile.inputStream().use { inputStream ->
|
||||
inputStream.readAllBytes { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw UnknownDatabaseLocationException()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to save database", e)
|
||||
if (e is DatabaseException)
|
||||
throw e
|
||||
throw DatabaseOutputException(e)
|
||||
} finally {
|
||||
try {
|
||||
Log.d(TAG, "Delete database cache file $cacheFile")
|
||||
cacheFile.delete()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Cache file $cacheFile cannot be deleted", e)
|
||||
}
|
||||
if (databaseCopyUri == null) {
|
||||
this.dataModifiedSinceLastLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun groupIsInRecycleBin(group: Group): Boolean {
|
||||
val groupKDB = group.groupKDB
|
||||
val groupKDBX = group.groupKDBX
|
||||
@@ -845,13 +915,13 @@ class Database {
|
||||
fun createVirtualGroupFromSearch(searchParameters: SearchParameters,
|
||||
fromGroup: NodeId<*>? = null,
|
||||
max: Int = Integer.MAX_VALUE): Group? {
|
||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||
return mSearchHelper.createVirtualGroupWithSearchResult(this,
|
||||
searchParameters, fromGroup, max)
|
||||
}
|
||||
|
||||
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
||||
max: Int = Integer.MAX_VALUE): Group? {
|
||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||
return mSearchHelper.createVirtualGroupWithSearchResult(this,
|
||||
SearchParameters().apply {
|
||||
searchQuery = searchInfoString
|
||||
searchInTitles = true
|
||||
@@ -908,40 +978,6 @@ class Database {
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
fun saveData(databaseCopyUri: Uri?, contentResolver: ContentResolver) {
|
||||
try {
|
||||
val saveUri = databaseCopyUri ?: this.fileUri
|
||||
if (saveUri != null) {
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
outputStream = UriUtil.getUriOutputStream(contentResolver, saveUri)
|
||||
outputStream?.let { definedOutputStream ->
|
||||
val databaseOutput =
|
||||
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||
?: mDatabaseKDBX?.let {
|
||||
DatabaseOutputKDBX(
|
||||
it,
|
||||
definedOutputStream
|
||||
)
|
||||
}
|
||||
databaseOutput?.output()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
} finally {
|
||||
outputStream?.close()
|
||||
}
|
||||
if (databaseCopyUri == null) {
|
||||
this.dataModifiedSinceLastLoading = false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to save database", e)
|
||||
throw DatabaseOutputException(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearIndexesAndBinaries(filesDirectory: File? = null) {
|
||||
this.mDatabaseKDB?.clearIndexes()
|
||||
this.mDatabaseKDBX?.clearIndexes()
|
||||
@@ -987,20 +1023,13 @@ class Database {
|
||||
}
|
||||
|
||||
fun validatePasswordEncoding(mainCredential: MainCredential): Boolean {
|
||||
val password = mainCredential.masterPassword
|
||||
val password = mainCredential.password
|
||||
val containsKeyFile = mainCredential.keyFileUri != null
|
||||
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
|
||||
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
|
||||
?: false
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
|
||||
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
|
||||
mDatabaseKDBX?.keyLastChanged = DateInstant()
|
||||
}
|
||||
|
||||
fun rootCanContainsEntry(): Boolean {
|
||||
return mDatabaseKDB?.rootCanContainsEntry() ?: mDatabaseKDBX?.rootCanContainsEntry() ?: false
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,13 @@
|
||||
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.content.ContentResolver
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.encrypt.aes.AESTransformer
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
@@ -31,8 +33,10 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
@@ -56,8 +60,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
KdfFactory.aesKdf
|
||||
)
|
||||
|
||||
override val passwordEncoding: String
|
||||
get() = "ISO-8859-1"
|
||||
override val passwordEncoding: Charset
|
||||
get() = Charsets.ISO_8859_1
|
||||
|
||||
override var numberKeyEncryptionRounds = 300L
|
||||
|
||||
@@ -116,20 +120,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
return newId
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
||||
|
||||
return if (key != null && keyInputStream != null) {
|
||||
getCompositeKey(key, keyInputStream)
|
||||
} else if (key != null) { // key.length() >= 0
|
||||
getPasswordKey(key)
|
||||
} else if (keyInputStream != null) { // key == null
|
||||
getFileKey(keyInputStream)
|
||||
} else {
|
||||
throw IllegalArgumentException("Key cannot be empty.")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) {
|
||||
// Encrypt the master key a few times to make brute-force key-search harder
|
||||
@@ -138,6 +128,41 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
finalKey = HashManager.hashSha256(masterSeed, transformedKey)
|
||||
}
|
||||
|
||||
fun deriveMasterKey(
|
||||
contentResolver: ContentResolver,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
// Exception when no password
|
||||
if (mainCredential.hardwareKey != null)
|
||||
throw HardwareKeyDatabaseException()
|
||||
if (mainCredential.password == null && mainCredential.keyFileUri == null)
|
||||
throw EmptyKeyDatabaseException()
|
||||
|
||||
// Retrieve plain data
|
||||
val password = mainCredential.password
|
||||
val keyFileUri = mainCredential.keyFileUri
|
||||
val passwordBytes = if (password != null) MainCredential.retrievePasswordKey(
|
||||
password,
|
||||
passwordEncoding
|
||||
) else null
|
||||
val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey(
|
||||
contentResolver,
|
||||
keyFileUri,
|
||||
false
|
||||
) else null
|
||||
|
||||
// Build master key
|
||||
if (passwordBytes != null
|
||||
&& keyFileBytes != null) {
|
||||
this.masterKey = HashManager.hashSha256(
|
||||
passwordBytes,
|
||||
keyFileBytes
|
||||
)
|
||||
} else {
|
||||
this.masterKey = passwordBytes ?: keyFileBytes ?: byteArrayOf(0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createGroup(): GroupKDB {
|
||||
return GroupKDB()
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.res.Resources
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
@@ -31,10 +32,7 @@ import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||
import com.kunzisoft.keepass.database.element.CustomData
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||
import com.kunzisoft.keepass.database.element.Tags
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
@@ -49,30 +47,27 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
|
||||
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
import com.kunzisoft.keepass.utils.longTo8Bytes
|
||||
import org.apache.commons.codec.binary.Hex
|
||||
import org.w3c.dom.Node
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import javax.crypto.Mac
|
||||
import javax.xml.XMLConstants
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.parsers.ParserConfigurationException
|
||||
import kotlin.collections.HashSet
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
|
||||
// To resave the database with same credential when already loaded
|
||||
private var mCompositeKey = CompositeKey()
|
||||
|
||||
var hmacKey: ByteArray? = null
|
||||
private set
|
||||
|
||||
@@ -233,6 +228,79 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
}
|
||||
}
|
||||
|
||||
fun deriveMasterKey(
|
||||
contentResolver: ContentResolver,
|
||||
mainCredential: MainCredential,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
|
||||
) {
|
||||
// Retrieve each plain credential
|
||||
val password = mainCredential.password
|
||||
val keyFileUri = mainCredential.keyFileUri
|
||||
val hardwareKey = mainCredential.hardwareKey
|
||||
val passwordBytes = if (password != null) MainCredential.retrievePasswordKey(
|
||||
password,
|
||||
passwordEncoding
|
||||
) else null
|
||||
val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey(
|
||||
contentResolver,
|
||||
keyFileUri,
|
||||
true
|
||||
) else null
|
||||
val hardwareKeyBytes = if (hardwareKey != null) MainCredential.retrieveHardwareKey(
|
||||
challengeResponseRetriever.invoke(hardwareKey, transformSeed)
|
||||
) else null
|
||||
|
||||
// Save to rebuild master password with new seed later
|
||||
mCompositeKey = CompositeKey(passwordBytes, keyFileBytes, hardwareKey)
|
||||
|
||||
// Build the master key
|
||||
this.masterKey = composedKeyToMasterKey(
|
||||
passwordBytes,
|
||||
keyFileBytes,
|
||||
hardwareKeyBytes
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
fun deriveCompositeKey(
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
|
||||
) {
|
||||
val passwordBytes = mCompositeKey.passwordData
|
||||
val keyFileBytes = mCompositeKey.keyFileData
|
||||
val hardwareKey = mCompositeKey.hardwareKey
|
||||
if (hardwareKey == null) {
|
||||
// If no hardware key, simply rebuild from composed keys
|
||||
this.masterKey = composedKeyToMasterKey(
|
||||
passwordBytes,
|
||||
keyFileBytes
|
||||
)
|
||||
} else {
|
||||
val hardwareKeyBytes = MainCredential.retrieveHardwareKey(
|
||||
challengeResponseRetriever.invoke(hardwareKey, transformSeed)
|
||||
)
|
||||
this.masterKey = composedKeyToMasterKey(
|
||||
passwordBytes,
|
||||
keyFileBytes,
|
||||
hardwareKeyBytes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun composedKeyToMasterKey(passwordData: ByteArray?,
|
||||
keyFileData: ByteArray?,
|
||||
hardwareKeyData: ByteArray? = null): ByteArray {
|
||||
return HashManager.hashSha256(
|
||||
passwordData,
|
||||
keyFileData,
|
||||
hardwareKeyData
|
||||
)
|
||||
}
|
||||
|
||||
fun copyMasterKeyFrom(databaseVersioned: DatabaseKDBX) {
|
||||
super.copyMasterKeyFrom(databaseVersioned)
|
||||
this.mCompositeKey = databaseVersioned.mCompositeKey
|
||||
}
|
||||
|
||||
fun getMinKdbxVersion(): UnsignedInt {
|
||||
val entryHandler = EntryOperationHandler()
|
||||
val groupHandler = GroupOperationHandler()
|
||||
@@ -364,8 +432,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
kdfEngine.setParallelism(kdfParameters!!, parallelism)
|
||||
}
|
||||
|
||||
override val passwordEncoding: String
|
||||
get() = "UTF-8"
|
||||
override val passwordEncoding: Charset
|
||||
get() = Charsets.UTF_8
|
||||
|
||||
private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? {
|
||||
if (groupUUID == UUID_ZERO)
|
||||
@@ -528,22 +596,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return mFieldReferenceEngine.compile(textReference, recursionLevel)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
||||
|
||||
var masterKey = byteArrayOf()
|
||||
|
||||
if (key != null && keyInputStream != null) {
|
||||
return getCompositeKey(key, keyInputStream)
|
||||
} else if (key != null) { // key.length() >= 0
|
||||
masterKey = getPasswordKey(key)
|
||||
} else if (keyInputStream != null) { // key == null
|
||||
masterKey = getFileKey(keyInputStream)
|
||||
}
|
||||
|
||||
return HashManager.hashSha256(masterKey)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun makeFinalKey(masterSeed: ByteArray) {
|
||||
|
||||
@@ -615,115 +667,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return ret
|
||||
}
|
||||
|
||||
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||
try {
|
||||
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
|
||||
|
||||
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
|
||||
try {
|
||||
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
|
||||
} catch (e : ParserConfigurationException) {
|
||||
Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
|
||||
}
|
||||
|
||||
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
|
||||
val doc = documentBuilder.parse(keyInputStream)
|
||||
|
||||
var xmlKeyFileVersion = 1F
|
||||
|
||||
val docElement = doc.documentElement
|
||||
val keyFileChildNodes = docElement.childNodes
|
||||
// <KeyFile> Root node
|
||||
if (docElement == null
|
||||
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
|
||||
return null
|
||||
}
|
||||
if (keyFileChildNodes.length < 2)
|
||||
return null
|
||||
for (keyFileChildPosition in 0 until keyFileChildNodes.length) {
|
||||
val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition)
|
||||
// <Meta>
|
||||
if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) {
|
||||
val metaChildNodes = keyFileChildNode.childNodes
|
||||
for (metaChildPosition in 0 until metaChildNodes.length) {
|
||||
val metaChildNode = metaChildNodes.item(metaChildPosition)
|
||||
// <Version>
|
||||
if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) {
|
||||
val versionChildNodes = metaChildNode.childNodes
|
||||
for (versionChildPosition in 0 until versionChildNodes.length) {
|
||||
val versionChildNode = versionChildNodes.item(versionChildPosition)
|
||||
if (versionChildNode.nodeType == Node.TEXT_NODE) {
|
||||
val versionText = versionChildNode.textContent.removeSpaceChars()
|
||||
try {
|
||||
xmlKeyFileVersion = versionText.toFloat()
|
||||
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// <Key>
|
||||
if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) {
|
||||
val keyChildNodes = keyFileChildNode.childNodes
|
||||
for (keyChildPosition in 0 until keyChildNodes.length) {
|
||||
val keyChildNode = keyChildNodes.item(keyChildPosition)
|
||||
// <Data>
|
||||
if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) {
|
||||
var hashString : String? = null
|
||||
if (keyChildNode.hasAttributes()) {
|
||||
val dataNodeAttributes = keyChildNode.attributes
|
||||
hashString = dataNodeAttributes
|
||||
.getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue
|
||||
}
|
||||
val dataChildNodes = keyChildNode.childNodes
|
||||
for (dataChildPosition in 0 until dataChildNodes.length) {
|
||||
val dataChildNode = dataChildNodes.item(dataChildPosition)
|
||||
if (dataChildNode.nodeType == Node.TEXT_NODE) {
|
||||
val dataString = dataChildNode.textContent.removeSpaceChars()
|
||||
when (xmlKeyFileVersion) {
|
||||
1F -> {
|
||||
// No hash in KeyFile XML version 1
|
||||
return Base64.decode(dataString, BASE_64_FLAG)
|
||||
}
|
||||
2F -> {
|
||||
return if (hashString != null
|
||||
&& checkKeyFileHash(dataString, hashString)) {
|
||||
Log.i(TAG, "Successful key file hash check.")
|
||||
Hex.decodeHex(dataString.toCharArray())
|
||||
} else {
|
||||
Log.e(TAG, "Unable to check the hash of the key file.")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun checkKeyFileHash(data: String, hash: String): Boolean {
|
||||
var success = false
|
||||
try {
|
||||
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
|
||||
val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray()))
|
||||
.copyOfRange(0, 4).toHexString()
|
||||
success = dataDigest == hash
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
override fun newGroupId(): NodeIdUUID {
|
||||
var newId: NodeIdUUID
|
||||
do {
|
||||
@@ -855,10 +798,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
mFieldReferenceEngine.clear()
|
||||
}
|
||||
|
||||
fun containsPublicCustomData(): Boolean {
|
||||
return publicCustomData.size() > 0
|
||||
}
|
||||
|
||||
fun buildNewBinaryAttachment(smallSize: Boolean,
|
||||
compression: Boolean,
|
||||
protection: Boolean,
|
||||
@@ -932,13 +871,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
|
||||
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited
|
||||
|
||||
private const val XML_NODE_ROOT_NAME = "KeyFile"
|
||||
private const val XML_NODE_META_NAME = "Meta"
|
||||
private const val XML_NODE_VERSION_NAME = "Version"
|
||||
private const val XML_NODE_KEY_NAME = "Key"
|
||||
private const val XML_NODE_DATA_NAME = "Data"
|
||||
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
|
||||
|
||||
const val BASE_64_FLAG = Base64.NO_WRAP
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.util.Log
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||
@@ -32,11 +31,9 @@ import com.kunzisoft.keepass.database.element.icon.IconsManager
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import org.apache.commons.codec.binary.Hex
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
abstract class DatabaseVersioned<
|
||||
@@ -46,7 +43,6 @@ abstract class DatabaseVersioned<
|
||||
Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
|
||||
> {
|
||||
|
||||
|
||||
// Algorithm used to encrypt the database
|
||||
abstract var encryptionAlgorithm: EncryptionAlgorithm
|
||||
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||
@@ -55,11 +51,12 @@ abstract class DatabaseVersioned<
|
||||
abstract val kdfAvailableList: List<KdfEngine>
|
||||
abstract var numberKeyEncryptionRounds: Long
|
||||
|
||||
protected abstract val passwordEncoding: String
|
||||
abstract val passwordEncoding: Charset
|
||||
|
||||
var masterKey = ByteArray(32)
|
||||
var finalKey: ByteArray? = null
|
||||
protected set
|
||||
var transformSeed: ByteArray? = null
|
||||
|
||||
abstract val version: String
|
||||
abstract val defaultFileExtension: String
|
||||
@@ -91,58 +88,6 @@ abstract class DatabaseVersioned<
|
||||
return getGroupIndexes().filter { it != rootGroup }
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) {
|
||||
masterKey = getMasterKey(key, keyfileInputStream)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
|
||||
val fileKey = getFileKey(keyfileInputStream)
|
||||
val passwordKey = getPasswordKey(key)
|
||||
return HashManager.hashSha256(passwordKey, fileKey)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected fun getPasswordKey(key: String): ByteArray {
|
||||
val bKey: ByteArray = try {
|
||||
key.toByteArray(charset(passwordEncoding))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
key.toByteArray()
|
||||
}
|
||||
return HashManager.hashSha256(bKey)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||
try {
|
||||
val keyData = keyInputStream.readBytes()
|
||||
|
||||
// Check XML key file
|
||||
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
||||
if (xmlKeyByteArray != null) {
|
||||
return xmlKeyByteArray
|
||||
}
|
||||
|
||||
// Check 32 bytes key file
|
||||
when (keyData.size) {
|
||||
32 -> return keyData
|
||||
64 -> try {
|
||||
return Hex.decodeHex(String(keyData).toCharArray())
|
||||
} catch (ignoredException: Exception) {
|
||||
// Key is not base 64, treat it as binary data
|
||||
}
|
||||
}
|
||||
// Hash file as binary data
|
||||
return HashManager.hashSha256(keyData)
|
||||
} catch (outOfMemoryError: OutOfMemoryError) {
|
||||
throw IOException("Keyfile data is too large", outOfMemoryError)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||
return null
|
||||
}
|
||||
@@ -158,20 +103,25 @@ abstract class DatabaseVersioned<
|
||||
|
||||
val bKey: ByteArray
|
||||
try {
|
||||
bKey = password.toByteArray(charset(encoding))
|
||||
bKey = password.toByteArray(encoding)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
return false
|
||||
}
|
||||
|
||||
val reEncoded: String
|
||||
try {
|
||||
reEncoded = String(bKey, charset(encoding))
|
||||
reEncoded = String(bKey, encoding)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
return false
|
||||
}
|
||||
return password == reEncoded
|
||||
}
|
||||
|
||||
fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned<GroupId, EntryId, Group, Entry>) {
|
||||
this.masterKey = databaseVersioned.masterKey
|
||||
this.transformSeed = databaseVersioned.transformSeed
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------------------
|
||||
* Node Creation
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class ClipboardException(e: Exception) : Exception(e)
|
||||
@@ -24,128 +24,172 @@ import androidx.annotation.StringRes
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import java.io.PrintStream
|
||||
import java.io.PrintWriter
|
||||
|
||||
abstract class DatabaseException : Exception {
|
||||
|
||||
var innerMessage: String? = null
|
||||
abstract var errorId: Int
|
||||
var parameters: (Array<out String>)? = null
|
||||
var mThrowable: Throwable? = null
|
||||
|
||||
constructor() : super()
|
||||
constructor(message: String) : super(message)
|
||||
constructor(message: String, throwable: Throwable) : super(message, throwable)
|
||||
constructor(throwable: Throwable) : super(throwable)
|
||||
constructor(message: String, throwable: Throwable) {
|
||||
mThrowable = throwable
|
||||
innerMessage = StringBuilder().apply {
|
||||
append(message)
|
||||
if (throwable.localizedMessage != null) {
|
||||
append(" ")
|
||||
append(throwable.localizedMessage)
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
constructor(throwable: Throwable) {
|
||||
mThrowable = throwable
|
||||
innerMessage = throwable.localizedMessage
|
||||
}
|
||||
|
||||
fun getLocalizedMessage(resources: Resources): String {
|
||||
parameters?.let {
|
||||
return resources.getString(errorId, *it)
|
||||
} ?: return resources.getString(errorId)
|
||||
val throwable = mThrowable
|
||||
if (throwable is DatabaseException)
|
||||
errorId = throwable.errorId
|
||||
val localMessage = parameters?.let {
|
||||
resources.getString(errorId, *it)
|
||||
} ?: resources.getString(errorId)
|
||||
return StringBuilder().apply {
|
||||
append(localMessage)
|
||||
if (innerMessage != null) {
|
||||
append(" ")
|
||||
append(innerMessage)
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
|
||||
override fun printStackTrace() {
|
||||
mThrowable?.printStackTrace()
|
||||
super.printStackTrace()
|
||||
}
|
||||
|
||||
override fun printStackTrace(s: PrintStream) {
|
||||
mThrowable?.printStackTrace(s)
|
||||
super.printStackTrace(s)
|
||||
}
|
||||
|
||||
override fun printStackTrace(s: PrintWriter) {
|
||||
mThrowable?.printStackTrace(s)
|
||||
super.printStackTrace(s)
|
||||
}
|
||||
}
|
||||
|
||||
open class LoadDatabaseException : DatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
constructor(throwable: Throwable) : super(throwable)
|
||||
}
|
||||
|
||||
class FileNotFoundDatabaseException : LoadDatabaseException {
|
||||
class FileNotFoundDatabaseException : DatabaseInputException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.file_not_found_content
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class InvalidAlgorithmDatabaseException : LoadDatabaseException {
|
||||
class CorruptedDatabaseException : DatabaseInputException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.corrupted_file
|
||||
}
|
||||
|
||||
class InvalidAlgorithmDatabaseException : DatabaseInputException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_algorithm
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class DuplicateUuidDatabaseException: LoadDatabaseException {
|
||||
class UnknownDatabaseLocationException : DatabaseException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_db_same_uuid
|
||||
constructor(type: Type, uuid: NodeId<*>) : super() {
|
||||
parameters = arrayOf(type.name, uuid.toString())
|
||||
}
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
override var errorId: Int = R.string.error_location_unknown
|
||||
}
|
||||
|
||||
class IODatabaseException : LoadDatabaseException {
|
||||
class HardwareKeyDatabaseException : DatabaseException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_hardware_key_unsupported
|
||||
}
|
||||
|
||||
class EmptyKeyDatabaseException : DatabaseException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_empty_key
|
||||
}
|
||||
|
||||
class SignatureDatabaseException : DatabaseInputException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_db_sig
|
||||
}
|
||||
|
||||
class VersionDatabaseException : DatabaseInputException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.unsupported_db_version
|
||||
}
|
||||
|
||||
class InvalidCredentialsDatabaseException : DatabaseInputException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_credentials
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
}
|
||||
|
||||
class KDFMemoryDatabaseException(exception: Throwable) : DatabaseInputException(exception) {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database_KDF_memory
|
||||
}
|
||||
|
||||
class NoMemoryDatabaseException(exception: Throwable) : DatabaseInputException(exception) {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_out_of_memory
|
||||
}
|
||||
|
||||
class DuplicateUuidDatabaseException(type: Type, uuid: NodeId<*>) : DatabaseInputException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_db_same_uuid
|
||||
init {
|
||||
parameters = arrayOf(type.name, uuid.toString())
|
||||
}
|
||||
}
|
||||
|
||||
class XMLMalformedDatabaseException : DatabaseInputException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_XML_malformed
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
}
|
||||
|
||||
class MergeDatabaseKDBException : DatabaseInputException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_unable_merge_database_kdb
|
||||
}
|
||||
|
||||
class MoveEntryDatabaseException : DatabaseException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_move_entry_here
|
||||
}
|
||||
|
||||
class MoveGroupDatabaseException : DatabaseException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_move_group_here
|
||||
}
|
||||
|
||||
class CopyEntryDatabaseException : DatabaseException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_copy_entry_here
|
||||
}
|
||||
|
||||
class CopyGroupDatabaseException : DatabaseException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_copy_group_here
|
||||
}
|
||||
|
||||
open class DatabaseInputException : DatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
constructor(throwable: Throwable) : super(throwable)
|
||||
}
|
||||
|
||||
class KDFMemoryDatabaseException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database_KDF_memory
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class SignatureDatabaseException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_db_sig
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class VersionDatabaseException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.unsupported_db_version
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class InvalidCredentialsDatabaseException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_credentials
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class NoMemoryDatabaseException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_out_of_memory
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class MoveEntryDatabaseException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_move_entry_here
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class MoveGroupDatabaseException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_move_group_here
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class CopyEntryDatabaseException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_copy_entry_here
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class CopyGroupDatabaseException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_copy_group_here
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
// TODO Output Exception
|
||||
open class DatabaseOutputException : DatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_save_database
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.file.input
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseInputException
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import java.io.InputStream
|
||||
|
||||
@@ -33,15 +33,9 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var m
|
||||
|
||||
/**
|
||||
* Load a versioned database file, return contents in a new DatabaseVersioned.
|
||||
*
|
||||
* @param databaseInputStream Existing file to load.
|
||||
* @param password Pass phrase for infile.
|
||||
* @return new DatabaseVersioned container.
|
||||
*
|
||||
* @throws LoadDatabaseException on database error (contains IO exceptions)
|
||||
*/
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
@Throws(DatabaseInputException::class)
|
||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)): D
|
||||
|
||||
@@ -47,7 +47,7 @@ import javax.crypto.CipherInputStream
|
||||
class DatabaseInputKDB(database: DatabaseKDB)
|
||||
: DatabaseInput<DatabaseKDB>(database) {
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
@Throws(DatabaseInputException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)): DatabaseKDB {
|
||||
@@ -76,6 +76,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
throw VersionDatabaseException()
|
||||
}
|
||||
|
||||
mDatabase.transformSeed = header.transformSeed
|
||||
assignMasterKey.invoke()
|
||||
|
||||
// Select algorithm
|
||||
@@ -310,18 +311,11 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
|
||||
stopContentTimer()
|
||||
|
||||
} catch (e: LoadDatabaseException) {
|
||||
} catch (e: Error) {
|
||||
mDatabase.clearAll()
|
||||
throw e
|
||||
} catch (e: IOException) {
|
||||
mDatabase.clearAll()
|
||||
throw IODatabaseException(e)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
mDatabase.clearAll()
|
||||
throw NoMemoryDatabaseException(e)
|
||||
} catch (e: Exception) {
|
||||
mDatabase.clearAll()
|
||||
throw LoadDatabaseException(e)
|
||||
if (e is OutOfMemoryError)
|
||||
throw NoMemoryDatabaseException(e)
|
||||
throw DatabaseInputException(e)
|
||||
}
|
||||
|
||||
return mDatabase
|
||||
|
||||
@@ -99,7 +99,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
this.isRAMSufficient = method
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
@Throws(DatabaseInputException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)): DatabaseKDBX {
|
||||
@@ -114,6 +114,8 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
hashOfHeader = headerAndHash.hash
|
||||
val pbHeader = headerAndHash.header
|
||||
|
||||
val transformSeed = header.transformSeed
|
||||
mDatabase.transformSeed = transformSeed
|
||||
assignMasterKey.invoke()
|
||||
mDatabase.makeFinalKey(header.masterSeed)
|
||||
|
||||
@@ -155,7 +157,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
throw InvalidCredentialsDatabaseException()
|
||||
}
|
||||
|
||||
val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
|
||||
val hmacKey = mDatabase.hmacKey ?: throw DatabaseInputException()
|
||||
|
||||
val blockKey = HmacBlock.getHmacKey64(hmacKey, UnsignedLong.MAX_BYTES)
|
||||
val hmac: Mac = HmacBlock.getHmacSha256(blockKey)
|
||||
@@ -187,7 +189,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
try {
|
||||
randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey)
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
throw DatabaseInputException(e)
|
||||
}
|
||||
|
||||
val xmlPullParserFactory = XmlPullParserFactory.newInstance().apply {
|
||||
@@ -200,19 +202,12 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
|
||||
stopContentTimer()
|
||||
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: XmlPullParserException) {
|
||||
throw IODatabaseException(e)
|
||||
} catch (e: IOException) {
|
||||
} catch (e: Error) {
|
||||
if (e is OutOfMemoryError)
|
||||
throw NoMemoryDatabaseException(e)
|
||||
if (e.message?.contains("Hash failed with code") == true)
|
||||
throw KDFMemoryDatabaseException(e)
|
||||
else
|
||||
throw IODatabaseException(e)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
throw NoMemoryDatabaseException(e)
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
throw DatabaseInputException(e)
|
||||
}
|
||||
|
||||
return mDatabase
|
||||
@@ -227,7 +222,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
val fieldId = dataInputStream.read().toByte()
|
||||
|
||||
val size = dataInputStream.readBytes4ToUInt().toKotlinInt()
|
||||
if (size < 0) throw IOException("Corrupted file")
|
||||
if (size < 0) throw CorruptedDatabaseException()
|
||||
|
||||
var data = ByteArray(0)
|
||||
try {
|
||||
@@ -238,7 +233,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// OOM only if corrupted file
|
||||
throw IOException("Corrupted file")
|
||||
throw CorruptedDatabaseException()
|
||||
}
|
||||
|
||||
readStream = true
|
||||
@@ -297,7 +292,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
Binaries
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
|
||||
@Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
|
||||
private fun readDocumentStreamed(xpp: XmlPullParser) {
|
||||
|
||||
ctxGroups.clear()
|
||||
@@ -324,11 +319,11 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
}
|
||||
|
||||
// Error checks
|
||||
if (ctx != KdbContext.Null) throw IOException("Malformed")
|
||||
if (ctxGroups.size != 0) throw IOException("Malformed")
|
||||
if (ctx != KdbContext.Null) throw XMLMalformedDatabaseException()
|
||||
if (ctxGroups.size != 0) throw XMLMalformedDatabaseException()
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
|
||||
@Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
|
||||
private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext {
|
||||
val name = xpp.name
|
||||
when (ctx) {
|
||||
@@ -352,7 +347,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
if (encodedHash.isNotEmpty() && hashOfHeader != null) {
|
||||
val hash = Base64.decode(encodedHash, BASE_64_FLAG)
|
||||
if (!Arrays.equals(hash, hashOfHeader)) {
|
||||
throw LoadDatabaseException()
|
||||
throw DatabaseInputException()
|
||||
}
|
||||
}
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemSettingsChanged, ignoreCase = true)) {
|
||||
@@ -824,7 +819,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
if (ctx != null) {
|
||||
contextName = ctx.name
|
||||
}
|
||||
throw RuntimeException("Invalid end element: Context " + contextName + "End element: " + name)
|
||||
throw XMLMalformedDatabaseException("Invalid end element: Context " + contextName + "End element: " + name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.InnerRandomStreamID, uIntTo4Bytes(header.innerRandomStream!!.id))
|
||||
}
|
||||
|
||||
if (databaseKDBX.containsPublicCustomData()) {
|
||||
if (databaseKDBX.publicCustomData.size() > 0) {
|
||||
val bos = ByteArrayOutputStream()
|
||||
VariantDictionary.serialize(databaseKDBX.publicCustomData, bos)
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.PublicCustomData, bos.toByteArray())
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.io.OutputStream
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.SecureRandom
|
||||
|
||||
abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(protected var mOutputStream: OutputStream) {
|
||||
abstract class DatabaseOutput<Header : DatabaseHeader> {
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
protected open fun setIVs(header: Header): SecureRandom {
|
||||
@@ -44,9 +44,7 @@ abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(pro
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
abstract fun output()
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
abstract fun outputHeader(outputStream: OutputStream): Header
|
||||
abstract fun writeDatabase(outputStream: OutputStream,
|
||||
assignMasterKey: () -> Unit)
|
||||
|
||||
}
|
||||
@@ -39,9 +39,8 @@ import java.security.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherOutputStream
|
||||
|
||||
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
outputStream: OutputStream)
|
||||
: DatabaseOutput<DatabaseHeaderKDB>(outputStream) {
|
||||
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB)
|
||||
: DatabaseOutput<DatabaseHeaderKDB>() {
|
||||
|
||||
private var headerHashBlock: ByteArray? = null
|
||||
|
||||
@@ -60,15 +59,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
override fun output() {
|
||||
override fun writeDatabase(outputStream: OutputStream,
|
||||
assignMasterKey: () -> Unit) {
|
||||
// Before we output the header, we should sort our list of groups
|
||||
// and remove any orphaned nodes that are no longer part of the tree hierarchy
|
||||
// also remove the virtual root not present in kdb
|
||||
val rootGroup = mDatabaseKDB.rootGroup
|
||||
sortNodesForOutput()
|
||||
|
||||
val header = outputHeader(mOutputStream)
|
||||
|
||||
val header = outputHeader(outputStream, assignMasterKey)
|
||||
val finalKey = getFinalKey(header)
|
||||
|
||||
val cipher: Cipher = try {
|
||||
@@ -81,7 +80,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
try {
|
||||
val cos = CipherOutputStream(mOutputStream, cipher)
|
||||
val cos = CipherOutputStream(outputStream, cipher)
|
||||
val bos = BufferedOutputStream(cos)
|
||||
outputPlanGroupAndEntries(bos)
|
||||
bos.flush()
|
||||
@@ -107,7 +106,8 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
|
||||
private fun outputHeader(outputStream: OutputStream,
|
||||
assignMasterKey: () -> Unit): DatabaseHeaderKDB {
|
||||
// Build header
|
||||
val header = DatabaseHeaderKDB()
|
||||
header.signature1 = DatabaseHeaderKDB.DBSIG_1
|
||||
@@ -132,6 +132,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
|
||||
setIVs(header)
|
||||
|
||||
mDatabaseKDB.transformSeed = header.transformSeed
|
||||
assignMasterKey()
|
||||
|
||||
// Header checksum
|
||||
val headerDigest: MessageDigest = HashManager.getHash256()
|
||||
|
||||
|
||||
@@ -56,9 +56,8 @@ import javax.crypto.CipherOutputStream
|
||||
import kotlin.experimental.or
|
||||
|
||||
|
||||
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
outputStream: OutputStream)
|
||||
: DatabaseOutput<DatabaseHeaderKDBX>(outputStream) {
|
||||
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX)
|
||||
: DatabaseOutput<DatabaseHeaderKDBX>() {
|
||||
|
||||
private var randomStream: StreamCipher? = null
|
||||
private lateinit var xml: XmlSerializer
|
||||
@@ -67,43 +66,34 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
private var headerHmac: ByteArray? = null
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
override fun output() {
|
||||
override fun writeDatabase(outputStream: OutputStream,
|
||||
assignMasterKey: () -> Unit) {
|
||||
|
||||
try {
|
||||
header = outputHeader(mOutputStream)
|
||||
header = outputHeader(outputStream, assignMasterKey)
|
||||
|
||||
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
|
||||
val cos = attachStreamEncryptor(header!!, mOutputStream)
|
||||
val cos = attachStreamEncryptor(header!!, outputStream)
|
||||
cos.write(header!!.streamStartBytes)
|
||||
|
||||
HashedBlockOutputStream(cos)
|
||||
} else {
|
||||
mOutputStream.write(hashOfHeader!!)
|
||||
mOutputStream.write(headerHmac!!)
|
||||
outputStream.write(hashOfHeader!!)
|
||||
outputStream.write(headerHmac!!)
|
||||
|
||||
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!))
|
||||
attachStreamEncryptor(header!!, HmacBlockOutputStream(outputStream, mDatabaseKDBX.hmacKey!!))
|
||||
}
|
||||
|
||||
val xmlOutputStream: OutputStream
|
||||
try {
|
||||
xmlOutputStream = when(mDatabaseKDBX.compressionAlgorithm) {
|
||||
CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
|
||||
else -> osPlain
|
||||
}
|
||||
|
||||
when(mDatabaseKDBX.compressionAlgorithm) {
|
||||
CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
|
||||
else -> osPlain
|
||||
}.use { xmlOutputStream ->
|
||||
if (!header!!.version.isBefore(FILE_VERSION_40)) {
|
||||
outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream)
|
||||
}
|
||||
|
||||
outputDatabase(xmlOutputStream)
|
||||
xmlOutputStream.close()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw DatabaseOutputException(e)
|
||||
} catch (e: IllegalStateException) {
|
||||
throw DatabaseOutputException(e)
|
||||
}
|
||||
|
||||
} catch (e: IOException) {
|
||||
} catch (e: Exception) {
|
||||
throw DatabaseOutputException(e)
|
||||
}
|
||||
}
|
||||
@@ -322,11 +312,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDBX {
|
||||
private fun outputHeader(outputStream: OutputStream,
|
||||
assignMasterKey: () -> Unit): DatabaseHeaderKDBX {
|
||||
try {
|
||||
val header = DatabaseHeaderKDBX(mDatabaseKDBX)
|
||||
setIVs(header)
|
||||
|
||||
mDatabaseKDBX.transformSeed = header.transformSeed
|
||||
assignMasterKey.invoke()
|
||||
|
||||
val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream)
|
||||
pho.output()
|
||||
|
||||
@@ -359,6 +353,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
writeBoolean(DatabaseKDBXXML.ElemEnableAutoType, group.enableAutoType)
|
||||
writeBoolean(DatabaseKDBXXML.ElemEnableSearching, group.enableSearching)
|
||||
writeUuid(DatabaseKDBXXML.ElemLastTopVisibleEntry, group.lastTopVisibleEntry)
|
||||
writeCustomData(group.customData)
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
|
||||
@@ -148,9 +148,9 @@ class SearchHelper {
|
||||
onDatabaseClosed.invoke()
|
||||
} else if (TimeoutHelper.checkTime(context)) {
|
||||
var searchWithoutUI = false
|
||||
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
||||
&& searchInfo != null && !searchInfo.manualSelection
|
||||
&& !searchInfo.containsOnlyNullValues()) {
|
||||
if (searchInfo != null
|
||||
&& !searchInfo.manualSelection
|
||||
&& !searchInfo.containsOnlyNullValues()) {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
searchInfo.toString(),
|
||||
|
||||
@@ -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 = CHALLENGE_RESPONSE_YUBIKEY
|
||||
|
||||
fun getStringValues(): List<String> {
|
||||
return values().map { it.value }
|
||||
}
|
||||
|
||||
fun fromPosition(position: Int): HardwareKey {
|
||||
return when (position) {
|
||||
// 0 -> FIDO2_SECRET
|
||||
0 -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.kunzisoft.keepass.hardware
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
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 com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
/**
|
||||
* Special activity to deal with hardware key drivers,
|
||||
* return the response to the database service once finished
|
||||
*/
|
||||
class HardwareKeyActivity: DatabaseModeActivity(){
|
||||
|
||||
// To manage hardware key challenge response
|
||||
private 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")
|
||||
mDatabaseTaskProvider?.startChallengeResponded(challengeResponse ?: ByteArray(0))
|
||||
} else {
|
||||
Log.e(TAG, "Response from challenge error")
|
||||
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
private var activityResultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
resultCallback
|
||||
)
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun showDatabaseDialog(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
val hardwareKey = HardwareKey.getHardwareKeyFromString(
|
||||
intent.getStringExtra(DATA_HARDWARE_KEY)
|
||||
)
|
||||
if (isHardwareKeyAvailable(this, hardwareKey, true) {
|
||||
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
|
||||
}) {
|
||||
when (hardwareKey) {
|
||||
/*
|
||||
HardwareKey.FIDO2_SECRET -> {
|
||||
// TODO FIDO2 under development
|
||||
throw Exception("FIDO2 not implemented")
|
||||
}
|
||||
*/
|
||||
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
|
||||
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
|
||||
}
|
||||
else -> {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
|
||||
// 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
|
||||
activityResultLauncher.launch(
|
||||
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
|
||||
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
|
||||
}
|
||||
)
|
||||
Log.d(TAG, "Challenge sent")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = HardwareKeyActivity::class.java.simpleName
|
||||
|
||||
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
|
||||
private const val DATA_SEED = "DATA_SEED"
|
||||
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"
|
||||
|
||||
fun launchHardwareKeyActivity(
|
||||
context: Context,
|
||||
hardwareKey: HardwareKey,
|
||||
seed: ByteArray?
|
||||
) {
|
||||
context.startActivity(Intent(context, HardwareKeyActivity::class.java).apply {
|
||||
flags = FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
|
||||
putExtra(DATA_SEED, seed)
|
||||
})
|
||||
}
|
||||
|
||||
fun isHardwareKeyAvailable(
|
||||
context: Context,
|
||||
hardwareKey: HardwareKey?,
|
||||
showDialog: Boolean = true,
|
||||
onDialogDismissed: DialogInterface.OnDismissListener? = null
|
||||
): Boolean {
|
||||
if (hardwareKey == null)
|
||||
return false
|
||||
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(context.packageManager) != null
|
||||
if (showDialog && !yubikeyDriverAvailable
|
||||
&& context is Activity)
|
||||
showHardwareKeyDriverNeeded(context, hardwareKey) {
|
||||
onDialogDismissed?.onDismiss(it)
|
||||
context.finish()
|
||||
}
|
||||
yubikeyDriverAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showHardwareKeyDriverNeeded(
|
||||
context: Context,
|
||||
hardwareKey: HardwareKey,
|
||||
onDialogDismissed: DialogInterface.OnDismissListener
|
||||
) {
|
||||
val builder = AlertDialog.Builder(context)
|
||||
builder
|
||||
.setMessage(
|
||||
context.getString(R.string.error_driver_required, hardwareKey.toString())
|
||||
)
|
||||
.setPositiveButton(R.string.download) { _, _ ->
|
||||
UriUtil.openExternalApp(
|
||||
context,
|
||||
context.getString(R.string.key_driver_app_id),
|
||||
context.getString(R.string.key_driver_url)
|
||||
)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.setOnDismissListener(onDialogDismissed)
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
package com.kunzisoft.keepass.magikeyboard
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.inputmethodservice.InputMethodService
|
||||
@@ -36,8 +37,11 @@ import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
@@ -91,6 +95,14 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
switchToPreviousKeyboard()
|
||||
}
|
||||
|
||||
fieldsAdapter = FieldsAdapter(this)
|
||||
fieldsAdapter?.onItemClickListener = object : FieldsAdapter.OnItemClickListener {
|
||||
override fun onItemClick(item: Field) {
|
||||
currentInputConnection.commitText(getEntryInfo()?.getGeneratedFieldValue(item.name) , 1)
|
||||
actionTabAutomatically()
|
||||
}
|
||||
}
|
||||
|
||||
registerLockReceiver(lockReceiver, true)
|
||||
}
|
||||
|
||||
@@ -119,15 +131,8 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
contentView = popupFieldsView
|
||||
}
|
||||
|
||||
val recyclerView = popupFieldsView.findViewById<androidx.recyclerview.widget.RecyclerView>(R.id.keyboard_popup_fields_list)
|
||||
fieldsAdapter = FieldsAdapter(this)
|
||||
fieldsAdapter?.onItemClickListener = object : FieldsAdapter.OnItemClickListener {
|
||||
override fun onItemClick(item: Field) {
|
||||
currentInputConnection.commitText(getEntryInfo()?.getGeneratedFieldValue(item.name) , 1)
|
||||
actionTabAutomatically()
|
||||
}
|
||||
}
|
||||
recyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this, androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, true)
|
||||
val recyclerView = popupFieldsView.findViewById<RecyclerView>(R.id.keyboard_popup_fields_list)
|
||||
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, true)
|
||||
recyclerView.adapter = fieldsAdapter
|
||||
|
||||
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
|
||||
@@ -139,7 +144,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
return rootKeyboardView
|
||||
}
|
||||
|
||||
return super.onCreateInputView()
|
||||
return rootKeyboardView
|
||||
}
|
||||
|
||||
private fun getEntryInfo(): EntryInfo? {
|
||||
@@ -351,38 +356,35 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
mDatabase,
|
||||
searchInfo,
|
||||
{ _, items ->
|
||||
if (items.size == 1) {
|
||||
if (entryUUID == null) {
|
||||
performSelection(
|
||||
items,
|
||||
{
|
||||
// Automatically populate keyboard
|
||||
removeEntryInfo()
|
||||
addEntryAndLaunchNotificationIfAllowed(
|
||||
this,
|
||||
items[0],
|
||||
true
|
||||
)
|
||||
assignKeyboardView()
|
||||
} else {
|
||||
// Choose another one
|
||||
launchEntrySelection(null)
|
||||
},
|
||||
{
|
||||
launchEntrySelection(searchInfo)
|
||||
}
|
||||
} else {
|
||||
// Select if multiple
|
||||
launchEntrySelection(searchInfo)
|
||||
}
|
||||
)
|
||||
},
|
||||
{ _ ->
|
||||
{
|
||||
// Select if not found
|
||||
launchEntrySelection(searchInfo)
|
||||
},
|
||||
{
|
||||
// Select if database not opened
|
||||
removeEntryInfo()
|
||||
launchEntrySelection(searchInfo)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun launchEntrySelection(searchInfo: SearchInfo?) {
|
||||
removeEntryInfo()
|
||||
EntrySelectionLauncherActivity.launch(this, searchInfo)
|
||||
}
|
||||
|
||||
@@ -476,5 +478,35 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
it.packageName == context.packageName
|
||||
} ?: false
|
||||
}
|
||||
|
||||
fun performSelection(items: List<EntryInfo>,
|
||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||
if (items.size == 1) {
|
||||
val itemFound = items[0]
|
||||
if (entryUUID != itemFound.id) {
|
||||
actionPopulateKeyboard.invoke(itemFound)
|
||||
} else {
|
||||
// Force selection if magikeyboard already populated
|
||||
actionEntrySelection.invoke(false)
|
||||
}
|
||||
} else if (items.size > 1) {
|
||||
// Select the one we want in the selection
|
||||
actionEntrySelection.invoke(true)
|
||||
} else {
|
||||
// Select an arbitrary one
|
||||
actionEntrySelection.invoke(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||
entry: EntryInfo,
|
||||
toast: Boolean = true) {
|
||||
// Populate Magikeyboard with entry
|
||||
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||
// Consume the selection mode
|
||||
EntrySelectionHelper.removeModesFromIntent(activity.intent)
|
||||
activity.moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,6 @@ class CipherEncryptDatabase(): Parcelable {
|
||||
parcel.readByteArray(specParameters)
|
||||
}
|
||||
|
||||
fun replaceContent(copy: CipherEncryptDatabase) {
|
||||
this.encryptedValue = copy.encryptedValue
|
||||
this.specParameters = copy.specParameters
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeParcelable(databaseUri, flags)
|
||||
parcel.writeEnum(credentialStorage)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
data class DatabaseFile(var databaseUri: Uri? = null,
|
||||
var keyFileUri: Uri? = null,
|
||||
var hardwareKey: HardwareKey? = null,
|
||||
var databaseDecodedPath: String? = null,
|
||||
var databaseAlias: String? = null,
|
||||
var databaseFileExists: Boolean = false,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.ServiceCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
@@ -36,7 +37,10 @@ import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
@@ -275,7 +279,7 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
AttachmentState.COMPLETE,
|
||||
AttachmentState.CANCELED,
|
||||
AttachmentState.ERROR -> {
|
||||
stopForeground(false)
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||
notificationManager?.notify(attachmentNotification.notificationId, builder.build())
|
||||
} else -> {
|
||||
startForeground(attachmentNotification.notificationId, builder.build())
|
||||
|
||||
@@ -33,6 +33,10 @@ class ClipboardEntryNotificationField : Parcelable {
|
||||
|
||||
private var id: NotificationFieldId = NotificationFieldId.UNKNOWN
|
||||
var label: String = ""
|
||||
val isSensitive: Boolean
|
||||
get() {
|
||||
return id == NotificationFieldId.PASSWORD
|
||||
}
|
||||
|
||||
val actionKey: String
|
||||
get() = getActionKey(id)
|
||||
|
||||
@@ -31,7 +31,6 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import java.util.*
|
||||
|
||||
class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
|
||||
@@ -75,7 +74,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
}
|
||||
ACTION_CLEAN_CLIPBOARD == intent.action -> {
|
||||
mTimerJob?.cancel()
|
||||
cleanClipboard()
|
||||
clipboardHelper?.cleanClipboard()
|
||||
stopNotificationAndSendLockIfNeeded()
|
||||
}
|
||||
else -> for (actionKey in ClipboardEntryNotificationField.allActionKeys) {
|
||||
@@ -153,7 +152,11 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
|
||||
try {
|
||||
var generatedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
||||
clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
|
||||
clipboardHelper?.copyToClipboard(
|
||||
fieldToCopy.label,
|
||||
generatedValue,
|
||||
fieldToCopy.isSensitive
|
||||
)
|
||||
|
||||
val builder = buildNewNotification()
|
||||
.setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp)
|
||||
@@ -186,13 +189,17 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
// New auto generated value
|
||||
if (generatedValue != newGeneratedValue) {
|
||||
generatedValue = newGeneratedValue
|
||||
clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
|
||||
clipboardHelper?.copyToClipboard(
|
||||
fieldToCopy.label,
|
||||
generatedValue,
|
||||
fieldToCopy.isSensitive
|
||||
)
|
||||
}
|
||||
}) {
|
||||
stopNotificationAndSendLockIfNeeded()
|
||||
// Clean password only if no next field
|
||||
if (nextFields.size <= 0)
|
||||
cleanClipboard()
|
||||
clipboardHelper?.cleanClipboard()
|
||||
}
|
||||
} else {
|
||||
// No timer
|
||||
@@ -202,25 +209,15 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Clipboard can't be populate", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun cleanClipboard() {
|
||||
try {
|
||||
clipboardHelper?.cleanClipboard()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Clipboard can't be cleaned", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
cleanClipboard()
|
||||
|
||||
clipboardHelper?.cleanClipboard()
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cleanClipboard()
|
||||
clipboardHelper?.cleanClipboard()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.media.app.NotificationCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
@@ -37,12 +38,15 @@ import com.kunzisoft.keepass.database.action.node.*
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.model.ProgressMessage
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
@@ -53,6 +57,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import java.util.*
|
||||
|
||||
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
|
||||
@@ -61,20 +66,24 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
// File description
|
||||
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
|
||||
private var mLastLocalSaveTime: Long = 0
|
||||
|
||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
private var mDatabaseListeners = LinkedList<DatabaseListener>()
|
||||
private var mDatabaseInfoListeners = LinkedList<DatabaseInfoListener>()
|
||||
private var mDatabaseListeners = mutableListOf<DatabaseListener>()
|
||||
private var mDatabaseInfoListeners = mutableListOf<DatabaseInfoListener>()
|
||||
private var mActionTaskBinder = ActionTaskBinder()
|
||||
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
||||
private var mActionTaskListeners = mutableListOf<ActionTaskListener>()
|
||||
// Channel to connect asynchronously a response
|
||||
private var mResponseChallengeChannel: Channel<ByteArray?>? = null
|
||||
|
||||
private var mActionRunning = false
|
||||
private var mTaskRemovedRequested = false
|
||||
private var mCreationState = false
|
||||
private var mSaveState = false
|
||||
|
||||
private var mIconId: Int = R.drawable.notification_ic_database_load
|
||||
private var mTitleId: Int = R.string.database_opened
|
||||
private var mMessageId: Int? = null
|
||||
private var mWarningId: Int? = null
|
||||
private var mProgressMessage: ProgressMessage = ProgressMessage(R.string.database_opened)
|
||||
|
||||
override fun retrieveChannelId(): String {
|
||||
return CHANNEL_DATABASE_ID
|
||||
@@ -126,9 +135,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
|
||||
interface ActionTaskListener {
|
||||
fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?)
|
||||
fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?)
|
||||
fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result)
|
||||
fun onStartAction(database: Database,
|
||||
progressMessage: ProgressMessage)
|
||||
fun onUpdateAction(database: Database,
|
||||
progressMessage: ProgressMessage)
|
||||
fun onStopAction(database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result)
|
||||
}
|
||||
|
||||
interface RequestChallengeListener {
|
||||
fun onChallengeResponseRequested(
|
||||
hardwareKey: HardwareKey,
|
||||
seed: ByteArray?
|
||||
)
|
||||
}
|
||||
|
||||
fun checkDatabase() {
|
||||
@@ -165,7 +185,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
Log.i(TAG, "Database file modified " +
|
||||
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
|
||||
// Call listener to indicate a change in database info
|
||||
if (!mCreationState && previousDatabaseInfo != null) {
|
||||
if (!mSaveState && previousDatabaseInfo != null) {
|
||||
mDatabaseInfoListeners.forEach { listener ->
|
||||
listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo)
|
||||
}
|
||||
@@ -197,12 +217,47 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
mDatabase?.let { database ->
|
||||
if (mActionRunning) {
|
||||
mActionTaskListeners.forEach { actionTaskListener ->
|
||||
actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId)
|
||||
actionTaskListener.onStartAction(
|
||||
database, mProgressMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun sendResponseToChallenge(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeChallengeResponse() {
|
||||
// Init the channels
|
||||
if (mResponseChallengeChannel == null) {
|
||||
mResponseChallengeChannel = Channel(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeChallengeResponse() {
|
||||
mResponseChallengeChannel?.close()
|
||||
mResponseChallengeChannel = null
|
||||
}
|
||||
|
||||
private fun cancelChallengeResponse(@StringRes error: Int) {
|
||||
mResponseChallengeChannel?.cancel(CancellationException(getString(error)))
|
||||
mResponseChallengeChannel = null
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
super.onBind(intent)
|
||||
return mActionTaskBinder
|
||||
@@ -219,8 +274,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
}
|
||||
|
||||
// Get save state
|
||||
mSaveState = if (intent != null) {
|
||||
if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||
!database.isReadOnly && intent.getBooleanExtra(
|
||||
SAVE_DATABASE_KEY,
|
||||
mSaveState
|
||||
)
|
||||
} else (intent.action == ACTION_DATABASE_CREATE_TASK
|
||||
|| intent.action == ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||
|| intent.action == ACTION_DATABASE_SAVE)
|
||||
} else false
|
||||
|
||||
// Create the notification
|
||||
buildMessage(intent, database.isReadOnly)
|
||||
buildNotification(intent)
|
||||
|
||||
val intentAction = intent?.action
|
||||
|
||||
@@ -258,7 +325,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK,
|
||||
ACTION_DATABASE_UPDATE_PARALLELISM_TASK,
|
||||
ACTION_DATABASE_UPDATE_ITERATIONS_TASK -> buildDatabaseUpdateElementActionTask(intent, database)
|
||||
ACTION_DATABASE_SAVE -> buildDatabaseSave(intent, database)
|
||||
ACTION_DATABASE_SAVE -> buildDatabaseSaveActionTask(intent, database)
|
||||
ACTION_CHALLENGE_RESPONDED -> buildChallengeRespondedActionTask(intent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -272,15 +340,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
mActionRunning = true
|
||||
|
||||
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
|
||||
putExtra(DATABASE_TASK_TITLE_KEY, mTitleId)
|
||||
putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId)
|
||||
putExtra(DATABASE_TASK_WARNING_KEY, mWarningId)
|
||||
putExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId)
|
||||
putExtra(DATABASE_TASK_MESSAGE_KEY, mProgressMessage.messageId)
|
||||
putExtra(DATABASE_TASK_WARNING_KEY, mProgressMessage.warningId)
|
||||
})
|
||||
|
||||
mActionTaskListeners.forEach { actionTaskListener ->
|
||||
actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId)
|
||||
actionTaskListener.onStartAction(
|
||||
database, mProgressMessage
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
actionRunnable
|
||||
@@ -325,7 +394,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
try {
|
||||
startService(Intent(applicationContext,
|
||||
DatabaseTaskNotificationService::class.java))
|
||||
} catch (e: IllegalStateException) {}
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.w(TAG, "Cannot restart the database task service", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
mTaskRemovedRequested = false
|
||||
@@ -353,61 +424,51 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMessage(intent: Intent?, readOnly: Boolean) {
|
||||
private fun buildNotification(intent: Intent?) {
|
||||
// Assign elements for updates
|
||||
val intentAction = intent?.action
|
||||
|
||||
var saveAction = false
|
||||
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||
saveAction = !readOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
|
||||
}
|
||||
|
||||
mIconId = if (intentAction == null)
|
||||
// Get icon depending action state
|
||||
val iconId = if (intentAction == null)
|
||||
R.drawable.notification_ic_database_open
|
||||
else
|
||||
R.drawable.notification_ic_database_load
|
||||
R.drawable.notification_ic_database_action
|
||||
|
||||
mTitleId = when {
|
||||
saveAction -> {
|
||||
R.string.saving_database
|
||||
}
|
||||
intentAction == null -> {
|
||||
// Title depending on action
|
||||
mProgressMessage.titleId =
|
||||
if (intentAction == null) {
|
||||
R.string.database_opened
|
||||
}
|
||||
else -> {
|
||||
when (intentAction) {
|
||||
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
|
||||
ACTION_DATABASE_LOAD_TASK,
|
||||
ACTION_DATABASE_MERGE_TASK,
|
||||
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
|
||||
ACTION_DATABASE_SAVE -> R.string.saving_database
|
||||
else -> {
|
||||
} else when (intentAction) {
|
||||
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
|
||||
ACTION_DATABASE_LOAD_TASK,
|
||||
ACTION_DATABASE_MERGE_TASK,
|
||||
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
|
||||
ACTION_DATABASE_ASSIGN_PASSWORD_TASK,
|
||||
ACTION_DATABASE_SAVE -> R.string.saving_database
|
||||
else -> {
|
||||
if (mSaveState)
|
||||
R.string.saving_database
|
||||
else
|
||||
R.string.command_execution
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mMessageId = when (intentAction) {
|
||||
ACTION_DATABASE_LOAD_TASK,
|
||||
ACTION_DATABASE_MERGE_TASK,
|
||||
ACTION_DATABASE_RELOAD_TASK -> null
|
||||
else -> null
|
||||
}
|
||||
// Updated later
|
||||
mProgressMessage.messageId = null
|
||||
|
||||
mWarningId =
|
||||
if (!saveAction
|
||||
|| intentAction == ACTION_DATABASE_LOAD_TASK
|
||||
|| intentAction == ACTION_DATABASE_MERGE_TASK
|
||||
|| intentAction == ACTION_DATABASE_RELOAD_TASK)
|
||||
null
|
||||
else
|
||||
// Warning if data is saved
|
||||
mProgressMessage.warningId =
|
||||
if (mSaveState)
|
||||
R.string.do_not_kill_app
|
||||
else
|
||||
null
|
||||
|
||||
val notificationBuilder = buildNewNotification().apply {
|
||||
setSmallIcon(mIconId)
|
||||
setSmallIcon(iconId)
|
||||
intent?.let {
|
||||
setContentTitle(getString(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mTitleId)))
|
||||
setContentTitle(getString(
|
||||
intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId))
|
||||
)
|
||||
}
|
||||
setAutoCancel(false)
|
||||
setContentIntent(null)
|
||||
@@ -513,15 +574,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateMessage(resId: Int) {
|
||||
mMessageId = resId
|
||||
private fun notifyProgressMessage() {
|
||||
mDatabase?.let { database ->
|
||||
mActionTaskListeners.forEach { actionTaskListener ->
|
||||
actionTaskListener.onUpdateAction(database, mTitleId, mMessageId, mWarningId)
|
||||
actionTaskListener.onUpdateAction(
|
||||
database, mProgressMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateMessage(resId: Int) {
|
||||
mProgressMessage.messageId = resId
|
||||
notifyProgressMessage()
|
||||
}
|
||||
|
||||
override fun actionOnLock() {
|
||||
if (!TimeoutHelper.temporarilyDisableLock) {
|
||||
closeDatabase(mDatabase)
|
||||
@@ -539,6 +606,43 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
|
||||
private fun retrieveResponseFromChallenge(hardwareKey: HardwareKey,
|
||||
seed: ByteArray?): ByteArray {
|
||||
// Request a challenge - response
|
||||
var response: ByteArray
|
||||
runBlocking {
|
||||
// Initialize the channels
|
||||
initializeChallengeResponse()
|
||||
val previousMessage = mProgressMessage.copy()
|
||||
mProgressMessage.apply {
|
||||
messageId = R.string.waiting_challenge_request
|
||||
cancelable = {
|
||||
cancelChallengeResponse(R.string.error_cancel_by_user)
|
||||
}
|
||||
}
|
||||
// Send the request
|
||||
notifyProgressMessage()
|
||||
HardwareKeyActivity
|
||||
.launchHardwareKeyActivity(
|
||||
this@DatabaseTaskNotificationService,
|
||||
hardwareKey,
|
||||
seed
|
||||
)
|
||||
// Wait the response
|
||||
mProgressMessage.apply {
|
||||
messageId = R.string.waiting_challenge_response
|
||||
}
|
||||
notifyProgressMessage()
|
||||
response = mResponseChallengeChannel?.receive() ?: byteArrayOf()
|
||||
// Close channels
|
||||
closeChallengeResponse()
|
||||
// Restore previous message
|
||||
mProgressMessage = previousMessage
|
||||
notifyProgressMessage()
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
private fun buildDatabaseCreateActionTask(intent: Intent, database: Database): ActionRunnable? {
|
||||
|
||||
if (intent.hasExtra(DATABASE_URI_KEY)
|
||||
@@ -550,15 +654,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
if (databaseUri == null)
|
||||
return null
|
||||
|
||||
mCreationState = true
|
||||
|
||||
return CreateDatabaseRunnable(this,
|
||||
database,
|
||||
databaseUri,
|
||||
getString(R.string.database_default_name),
|
||||
getString(R.string.database),
|
||||
getString(R.string.template_group_name),
|
||||
mainCredential
|
||||
mainCredential,
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
) { result ->
|
||||
result.data = Bundle().apply {
|
||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||
@@ -586,17 +691,18 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
if (databaseUri == null)
|
||||
return null
|
||||
|
||||
mCreationState = false
|
||||
|
||||
return LoadDatabaseRunnable(
|
||||
this,
|
||||
database,
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
|
||||
this
|
||||
this,
|
||||
database,
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
},
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
|
||||
this
|
||||
) { result ->
|
||||
// Add each info to reload database after thrown duplicate UUID exception
|
||||
result.data = Bundle().apply {
|
||||
@@ -623,9 +729,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
|
||||
return MergeDatabaseRunnable(
|
||||
this,
|
||||
database,
|
||||
databaseToMergeUri,
|
||||
databaseToMergeMainCredential,
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
},
|
||||
database,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
},
|
||||
this
|
||||
) { result ->
|
||||
// No need to add each info to reload database
|
||||
@@ -653,7 +766,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
database,
|
||||
databaseUri,
|
||||
intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
||||
)
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -687,7 +802,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
newGroup,
|
||||
parent,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -712,7 +830,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
oldGroup,
|
||||
newGroup,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -737,7 +858,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
newEntry,
|
||||
parent,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -762,7 +886,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
oldEntry,
|
||||
newEntry,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -783,7 +910,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
getListNodesFromBundle(database, intent.extras!!),
|
||||
newParent,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -804,7 +934,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
getListNodesFromBundle(database, intent.extras!!),
|
||||
newParent,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -820,7 +953,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
database,
|
||||
getListNodesFromBundle(database, intent.extras!!),
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -838,7 +974,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
database,
|
||||
mainEntry,
|
||||
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -857,7 +996,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
database,
|
||||
mainEntry,
|
||||
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -881,7 +1023,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
oldElement,
|
||||
newElement,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
).apply {
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}.apply {
|
||||
mAfterSaveDatabase = { result ->
|
||||
result.data = intent.extras
|
||||
}
|
||||
@@ -897,7 +1041,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
return RemoveUnlinkedDataDatabaseRunnable(this,
|
||||
database,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
).apply {
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}.apply {
|
||||
mAfterSaveDatabase = { result ->
|
||||
result.data = intent.extras
|
||||
}
|
||||
@@ -911,7 +1057,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||
return SaveDatabaseRunnable(this,
|
||||
database,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
null,
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
).apply {
|
||||
mAfterSaveDatabase = { result ->
|
||||
result.data = intent.extras
|
||||
@@ -925,7 +1075,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
/**
|
||||
* Save database without parameter
|
||||
*/
|
||||
private fun buildDatabaseSave(intent: Intent, database: Database): ActionRunnable? {
|
||||
private fun buildDatabaseSaveActionTask(intent: Intent, database: Database): ActionRunnable? {
|
||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||
|
||||
var databaseCopyUri: Uri? = null
|
||||
@@ -936,12 +1086,34 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
SaveDatabaseRunnable(this,
|
||||
database,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
null,
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
},
|
||||
databaseCopyUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildChallengeRespondedActionTask(intent: Intent): ActionRunnable? {
|
||||
return if (intent.hasExtra(DATA_BYTES)) {
|
||||
object : ActionRunnable() {
|
||||
override fun onStartRun() {}
|
||||
override fun onActionRun() {
|
||||
mainScope.launch {
|
||||
intent.getByteArrayExtra(DATA_BYTES)?.let { response ->
|
||||
sendResponseToChallenge(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onFinishRun() {}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = DatabaseTaskNotificationService::class.java.name
|
||||
@@ -978,6 +1150,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
|
||||
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
|
||||
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
|
||||
const val ACTION_CHALLENGE_RESPONDED = "ACTION_CHALLENGE_RESPONDED"
|
||||
|
||||
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
|
||||
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
|
||||
@@ -1001,9 +1174,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
const val NEW_NODES_KEY = "NEW_NODES_KEY"
|
||||
const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time
|
||||
const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time
|
||||
|
||||
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
|
||||
private var mLastLocalSaveTime: Long = 0
|
||||
const val DATA_BYTES = "DATA_BYTES"
|
||||
|
||||
fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> {
|
||||
val nodesAction = ArrayList<Node>()
|
||||
|
||||
@@ -149,30 +149,23 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
||||
|
||||
fun launchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean) {
|
||||
|
||||
val containsURLToCopy = entry.url.isNotEmpty()
|
||||
val containsUsernameToCopy = entry.username.isNotEmpty()
|
||||
val containsPasswordToCopy = entry.password.isNotEmpty()
|
||||
val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
|
||||
|
||||
var startService = false
|
||||
val intent = Intent(context, KeyboardEntryNotificationService::class.java)
|
||||
|
||||
if (containsURLToCopy || containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) {
|
||||
if (toast) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.keyboard_notification_entry_content_title, entry.title),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
if (toast) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.keyboard_notification_entry_content_title,
|
||||
entry.getVisualTitle()
|
||||
),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
// Show the notification if allowed in Preferences
|
||||
if (PreferencesUtil.isKeyboardNotificationEntryEnable(context)) {
|
||||
startService = true
|
||||
context.startService(intent.apply {
|
||||
putExtra(ENTRY_INFO_KEY, entry)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
MagikeyboardService.removeEntry(context)
|
||||
// Show the notification if allowed in Preferences
|
||||
if (PreferencesUtil.isKeyboardNotificationEntryEnable(context)) {
|
||||
startService = true
|
||||
context.startService(intent.apply {
|
||||
putExtra(ENTRY_INFO_KEY, entry)
|
||||
})
|
||||
}
|
||||
|
||||
if (!startService)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package com.kunzisoft.keepass.services
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.app.ServiceCompat
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LockReceiver
|
||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||
@@ -33,6 +34,7 @@ abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
protected open fun actionOnLock() {
|
||||
// Stop the service in all cases
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
|
||||
@@ -115,8 +115,8 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
mDatabaseViewModel.saveDatabase(save)
|
||||
}
|
||||
|
||||
private fun mergeDatabase() {
|
||||
mDatabaseViewModel.mergeDatabase(false)
|
||||
private fun mergeDatabase(save: Boolean) {
|
||||
mDatabaseViewModel.mergeDatabase(save)
|
||||
}
|
||||
|
||||
private fun reloadDatabase() {
|
||||
@@ -671,7 +671,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
true
|
||||
}
|
||||
R.id.menu_merge_database -> {
|
||||
mergeDatabase()
|
||||
mergeDatabase(!mDatabaseReadOnly)
|
||||
true
|
||||
}
|
||||
R.id.menu_reload_database -> {
|
||||
|
||||
@@ -96,6 +96,12 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.remember_keyfile_locations_default))
|
||||
}
|
||||
|
||||
fun rememberHardwareKey(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.remember_hardware_key_key),
|
||||
context.resources.getBoolean(R.bool.remember_hardware_key_default))
|
||||
}
|
||||
|
||||
fun automaticallyFocusSearch(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.auto_focus_search_key),
|
||||
@@ -479,29 +485,33 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.enable_keep_screen_on_default))
|
||||
}
|
||||
|
||||
fun isScreenshotModeEnabled(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.enable_screenshot_mode_key),
|
||||
context.resources.getBoolean(R.bool.enable_screenshot_mode_key_default))
|
||||
}
|
||||
|
||||
fun isAdvancedUnlockEnable(context: Context): Boolean {
|
||||
return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context)
|
||||
}
|
||||
|
||||
fun isBiometricUnlockEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val biometricSupported = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.biometricUnlockSupported(context)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
||||
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
|
||||
&& biometricSupported
|
||||
&& (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.biometricUnlockSupported(context)
|
||||
} else {
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
fun isDeviceCredentialUnlockEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
// Priority to biometric unlock
|
||||
val biometricAlreadySupported = isBiometricUnlockEnable(context)
|
||||
return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key),
|
||||
context.resources.getBoolean(R.bool.device_credential_unlock_enable_default))
|
||||
&& !biometricAlreadySupported
|
||||
&& !isBiometricUnlockEnable(context)
|
||||
}
|
||||
|
||||
fun isTempAdvancedUnlockEnable(context: Context): Boolean {
|
||||
@@ -620,15 +630,8 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.keyboard_selection_entry_default))
|
||||
}
|
||||
|
||||
fun isKeyboardSearchShareEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.keyboard_search_share_key),
|
||||
context.resources.getBoolean(R.bool.keyboard_search_share_default))
|
||||
&& MagikeyboardService.activatedInSettings(context)
|
||||
}
|
||||
|
||||
fun isKeyboardSaveSearchInfoEnable(context: Context): Boolean {
|
||||
if (!isKeyboardSearchShareEnable(context))
|
||||
if (!MagikeyboardService.activatedInSettings(context))
|
||||
return false
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.keyboard_save_search_info_key),
|
||||
@@ -683,12 +686,6 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.autofill_close_database_default))
|
||||
}
|
||||
|
||||
fun isAutofillAutoSearchEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key),
|
||||
context.resources.getBoolean(R.bool.autofill_auto_search_default))
|
||||
}
|
||||
|
||||
fun isAutofillInlineSuggestionsEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.autofill_inline_suggestions_key),
|
||||
@@ -824,7 +821,6 @@ object PreferencesUtil {
|
||||
context.getString(R.string.keyboard_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||
context.getString(R.string.keyboard_selection_entry_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.keyboard_search_share_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.keyboard_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.keyboard_auto_go_action_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.keyboard_key_vibrate_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
@@ -834,7 +830,6 @@ object PreferencesUtil {
|
||||
context.getString(R.string.keyboard_previous_fill_in_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.keyboard_previous_lock_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_close_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_auto_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_inline_suggestions_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_manual_selection_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.autofill_save_search_info_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
|
||||
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
|
||||
@@ -24,15 +24,18 @@ import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
import java.lang.Exception
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
|
||||
open class ProgressTaskDialogFragment : DialogFragment() {
|
||||
|
||||
@StringRes
|
||||
private var title = UNDEFINED
|
||||
@@ -40,10 +43,12 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
|
||||
private var message = UNDEFINED
|
||||
@StringRes
|
||||
private var warning = UNDEFINED
|
||||
private var cancellable: (() -> Unit)? = null
|
||||
|
||||
private var titleView: TextView? = null
|
||||
private var messageView: TextView? = null
|
||||
private var warningView: TextView? = null
|
||||
private var cancelButton: Button? = null
|
||||
private var progressView: ProgressBar? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
@@ -63,11 +68,13 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
|
||||
titleView = root.findViewById(R.id.progress_dialog_title)
|
||||
messageView = root.findViewById(R.id.progress_dialog_message)
|
||||
warningView = root.findViewById(R.id.progress_dialog_warning)
|
||||
cancelButton = root.findViewById(R.id.progress_dialog_cancel)
|
||||
progressView = root.findViewById(R.id.progress_dialog_bar)
|
||||
|
||||
updateTitle(title)
|
||||
updateMessage(message)
|
||||
updateWarning(warning)
|
||||
setCancellable(cancellable)
|
||||
|
||||
isCancelable = false
|
||||
|
||||
@@ -84,7 +91,7 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
|
||||
}
|
||||
|
||||
private fun updateView(textView: TextView?, @StringRes resId: Int) {
|
||||
activity?.runOnUiThread {
|
||||
activity?.lifecycleScope?.launch {
|
||||
if (resId == UNDEFINED) {
|
||||
textView?.visibility = View.GONE
|
||||
} else {
|
||||
@@ -94,21 +101,35 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle(@StringRes resId: Int) {
|
||||
this.title = resId
|
||||
private fun updateCancelable() {
|
||||
activity?.lifecycleScope?.launch {
|
||||
cancelButton?.isVisible = cancellable != null
|
||||
cancelButton?.setOnClickListener {
|
||||
cancellable?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle(@StringRes resId: Int?) {
|
||||
this.title = resId ?: UNDEFINED
|
||||
updateView(titleView, title)
|
||||
}
|
||||
|
||||
override fun updateMessage(@StringRes resId: Int) {
|
||||
this.message = resId
|
||||
fun updateMessage(@StringRes resId: Int?) {
|
||||
this.message = resId ?: UNDEFINED
|
||||
updateView(messageView, message)
|
||||
}
|
||||
|
||||
fun updateWarning(@StringRes resId: Int) {
|
||||
this.warning = resId
|
||||
fun updateWarning(@StringRes resId: Int?) {
|
||||
this.warning = resId ?: UNDEFINED
|
||||
updateView(warningView, warning)
|
||||
}
|
||||
|
||||
fun setCancellable(cancellable: (() -> Unit)?) {
|
||||
this.cancellable = cancellable
|
||||
updateCancelable()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ProgressTaskDialogFragment::class.java.simpleName
|
||||
const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment"
|
||||
|
||||
@@ -24,117 +24,117 @@ import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PersistableBundle
|
||||
import android.text.SpannableString
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.util.Linkify
|
||||
import android.util.Log
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.exception.ClipboardException
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
|
||||
class ClipboardHelper(private val context: Context) {
|
||||
class ClipboardHelper(context: Context) {
|
||||
|
||||
private var mAppContext = context.applicationContext
|
||||
private var mClipboardManager: ClipboardManager? = null
|
||||
|
||||
private val mTimer = Timer()
|
||||
|
||||
private fun getClipboardManager(): ClipboardManager? {
|
||||
if (mClipboardManager == null)
|
||||
mClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
|
||||
mClipboardManager = mAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
|
||||
return mClipboardManager
|
||||
}
|
||||
|
||||
fun timeoutCopyToClipboard(text: String, toastString: String = "") {
|
||||
if (toastString.isNotEmpty())
|
||||
Toast.makeText(context, toastString, Toast.LENGTH_LONG).show()
|
||||
|
||||
fun timeoutCopyToClipboard(label: String, text: String, sensitive: Boolean = false) {
|
||||
try {
|
||||
copyToClipboard(text)
|
||||
} catch (e: ClipboardException) {
|
||||
copyToClipboard(label, text, sensitive)
|
||||
} catch (e: Exception) {
|
||||
showClipboardErrorDialog()
|
||||
return
|
||||
}
|
||||
|
||||
val clipboardTimeout = PreferencesUtil.getClipboardTimeout(context)
|
||||
val clipboardTimeout = PreferencesUtil.getClipboardTimeout(mAppContext)
|
||||
if (clipboardTimeout > 0) {
|
||||
mTimer.schedule(ClearClipboardTask(context, text), clipboardTimeout)
|
||||
mTimer.schedule(ClearClipboardTask(text), clipboardTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
fun getClipboard(context: Context): CharSequence {
|
||||
if (getClipboardManager()?.hasPrimaryClip() == true) {
|
||||
val data = getClipboardManager()?.primaryClip
|
||||
if (data != null && data.itemCount > 0) {
|
||||
val text = data.getItemAt(0).coerceToText(context)
|
||||
if (text != null) {
|
||||
return text
|
||||
fun copyToClipboard(label: String, value: String, sensitive: Boolean = false) {
|
||||
getClipboardManager()?.setPrimaryClip(ClipData.newPlainText(DEFAULT_LABEL, value).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
description.extras = PersistableBundle().apply {
|
||||
putBoolean("android.content.extra.IS_SENSITIVE", sensitive)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (label.isNotEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
mAppContext,
|
||||
mAppContext.getString(
|
||||
R.string.copy_field,
|
||||
label
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@Throws(ClipboardException::class)
|
||||
fun copyToClipboard(value: String) {
|
||||
copyToClipboard("", value)
|
||||
}
|
||||
|
||||
@Throws(ClipboardException::class)
|
||||
fun copyToClipboard(label: String, value: String) {
|
||||
try {
|
||||
getClipboardManager()?.setPrimaryClip(ClipData.newPlainText(label, value))
|
||||
} catch (e: Exception) {
|
||||
throw ClipboardException(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Throws(ClipboardException::class)
|
||||
fun cleanClipboard(label: String = "") {
|
||||
fun cleanClipboard() {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
getClipboardManager()?.clearPrimaryClip()
|
||||
} else {
|
||||
copyToClipboard(label, "")
|
||||
copyToClipboard(DEFAULT_LABEL, "")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw ClipboardException(e)
|
||||
Log.e("ClipboardHelper", "Unable to clean the clipboard", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Task which clears the clipboard, and sends a toast to the foreground.
|
||||
private inner class ClearClipboardTask (private val mCtx: Context,
|
||||
private val mClearText: String) : TimerTask() {
|
||||
private inner class ClearClipboardTask (private val mClearText: String) : TimerTask() {
|
||||
override fun run() {
|
||||
val currentClip = getClipboard(mCtx).toString()
|
||||
if (currentClip == mClearText) {
|
||||
try {
|
||||
cleanClipboard()
|
||||
R.string.clipboard_cleared
|
||||
} catch (e: ClipboardException) {
|
||||
R.string.clipboard_error_clear
|
||||
if (getClipboard(mAppContext).toString() == mClearText) {
|
||||
cleanClipboard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getClipboard(context: Context): CharSequence {
|
||||
if (getClipboardManager()?.hasPrimaryClip() == true) {
|
||||
val data = getClipboardManager()?.primaryClip
|
||||
if (data != null && data.itemCount > 0) {
|
||||
val text = data.getItemAt(0).coerceToText(context)
|
||||
if (text != null) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun showClipboardErrorDialog() {
|
||||
val textDescription = context.getString(R.string.clipboard_error)
|
||||
val textDescription = mAppContext.getString(R.string.clipboard_error)
|
||||
val spannableString = SpannableString(textDescription)
|
||||
val textView = TextView(context).apply {
|
||||
val textView = TextView(mAppContext).apply {
|
||||
text = spannableString
|
||||
autoLinkMask = Activity.RESULT_OK
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
Linkify.addLinks(spannableString, Linkify.WEB_URLS)
|
||||
AlertDialog.Builder(context)
|
||||
AlertDialog.Builder(mAppContext)
|
||||
.setTitle(R.string.clipboard_error_title)
|
||||
.setView(textView)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_LABEL = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,26 @@ object ParcelableUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun Parcel.readByteArrayCompat(): ByteArray? {
|
||||
val dataLength = readInt()
|
||||
return if (dataLength >= 0) {
|
||||
val data = ByteArray(dataLength)
|
||||
readByteArray(data)
|
||||
data
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Parcel.writeByteArrayCompat(data: ByteArray?) {
|
||||
if (data != null) {
|
||||
writeInt(data.size)
|
||||
writeByteArray(data)
|
||||
} else {
|
||||
writeInt(-1)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : Enum<T>> Parcel.readEnum() =
|
||||
readString()?.let { enumValueOf<T>(it) }
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
@@ -226,10 +227,10 @@ object UriUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun getUriFromIntent(intent: Intent, key: String): Uri? {
|
||||
fun getUriFromIntent(intent: Intent?, key: String): Uri? {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
val clipData = intent.clipData
|
||||
val clipData = intent?.clipData
|
||||
if (clipData != null) {
|
||||
if (clipData.description.label == key) {
|
||||
if (clipData.itemCount == 1) {
|
||||
@@ -242,7 +243,7 @@ object UriUtil {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return intent.getParcelableExtra(key)
|
||||
return intent?.getParcelableExtra(key)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -269,35 +270,55 @@ object UriUtil {
|
||||
|
||||
fun contributingUser(context: Context): Boolean {
|
||||
return (Education.isEducationScreenReclickedPerformed(context)
|
||||
|| isExternalAppInstalled(context, "com.kunzisoft.keepass.pro")
|
||||
|| isExternalAppInstalled(
|
||||
context,
|
||||
context.getString(R.string.keepro_app_id),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun isExternalAppInstalled(context: Context, packageName: String): Boolean {
|
||||
fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean {
|
||||
try {
|
||||
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
|
||||
Education.setEducationScreenReclickedPerformed(context)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "App not accessible", e)
|
||||
if (showError)
|
||||
Log.e(TAG, "App not accessible", e)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun openExternalApp(context: Context, packageName: String) {
|
||||
fun openExternalApp(context: Context, packageName: String, sourcesURL: String? = null) {
|
||||
var launchIntent: Intent? = null
|
||||
try {
|
||||
launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)?.apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
} catch (ignored: Exception) { }
|
||||
try {
|
||||
if (launchIntent == null) {
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setData(Uri.parse("https://play.google.com/store/apps/details?id=$packageName"))
|
||||
.setData(
|
||||
Uri.parse(
|
||||
if (sourcesURL != null
|
||||
&& !BuildConfig.CLOSED_STORE
|
||||
) {
|
||||
sourcesURL
|
||||
} else {
|
||||
context.getString(
|
||||
if (BuildConfig.CLOSED_STORE)
|
||||
R.string.play_store_url
|
||||
else
|
||||
R.string.f_droid_url,
|
||||
packageName
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
context.startActivity(launchIntent)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import android.content.Context
|
||||
import android.text.InputType
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Filter
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
@@ -11,15 +13,55 @@ class InheritedCompletionView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : AppCompatAutoCompleteTextView(context, attrs) {
|
||||
|
||||
val adapter = ArrayAdapter(
|
||||
private val adapter = ArrayAdapterNoFilter(
|
||||
context,
|
||||
android.R.layout.simple_list_item_1,
|
||||
InheritedStatus.listOfStrings(context))
|
||||
android.R.layout.simple_list_item_1
|
||||
)
|
||||
|
||||
private class ArrayAdapterNoFilter(context: Context,
|
||||
@LayoutRes private val layoutResource: Int)
|
||||
: ArrayAdapter<String>(context, layoutResource) {
|
||||
val items = InheritedStatus.listOfStrings(context)
|
||||
|
||||
override fun getCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): String {
|
||||
return items[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
// Or just return p0
|
||||
return items[position].hashCode().toLong()
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
override fun performFiltering(p0: CharSequence?): FilterResults {
|
||||
return FilterResults().apply {
|
||||
values = items
|
||||
}
|
||||
}
|
||||
|
||||
override fun publishResults(p0: CharSequence?, p1: FilterResults?) {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
setAdapter(adapter)
|
||||
isFocusable = false
|
||||
isFocusableInTouchMode = false
|
||||
//hardwareKeyCompletion.isEnabled = false
|
||||
isCursorVisible = false
|
||||
setTextIsSelectable(false)
|
||||
inputType = InputType.TYPE_NULL
|
||||
adapter.filter.filter(null)
|
||||
setAdapter(adapter)
|
||||
setOnClickListener {
|
||||
showDropDown()
|
||||
}
|
||||
}
|
||||
|
||||
fun getValue(): Boolean? {
|
||||
@@ -28,7 +70,6 @@ class InheritedCompletionView @JvmOverloads constructor(
|
||||
|
||||
fun setValue(inherited: Boolean?) {
|
||||
setText(context.getString(InheritedStatus.getStatusFromValue(inherited).stringId))
|
||||
adapter.filter.filter(null)
|
||||
}
|
||||
|
||||
private enum class InheritedStatus(val stringId: Int, val value: Boolean?) {
|
||||
|
||||
@@ -39,19 +39,24 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0)
|
||||
: FrameLayout(context, attrs, defStyle) {
|
||||
|
||||
private var passwordTextView: EditText
|
||||
private var keyFileSelectionView: KeyFileSelectionView
|
||||
private var checkboxPasswordView: CompoundButton
|
||||
private var passwordTextView: EditText
|
||||
private var checkboxKeyFileView: CompoundButton
|
||||
private var keyFileSelectionView: KeyFileSelectionView
|
||||
private var checkboxHardwareView: CompoundButton
|
||||
private var hardwareKeySelectionView: HardwareKeySelectionView
|
||||
|
||||
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
||||
var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
||||
var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
||||
var onValidateListener: (() -> Unit)? = null
|
||||
|
||||
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
|
||||
@@ -60,15 +65,17 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_main_credentials, this)
|
||||
|
||||
passwordTextView = findViewById(R.id.password_text_view)
|
||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||
passwordTextView = findViewById(R.id.password_text_view)
|
||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkbox)
|
||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||
checkboxHardwareView = findViewById(R.id.hardware_key_checkbox)
|
||||
hardwareKeySelectionView = findViewById(R.id.hardware_key_selection)
|
||||
|
||||
val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
onValidateListener?.invoke()
|
||||
validateCredential()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -91,7 +98,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
if (keyEvent.action == KeyEvent.ACTION_DOWN
|
||||
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
|
||||
) {
|
||||
onValidateListener?.invoke()
|
||||
validateCredential()
|
||||
handled = true
|
||||
}
|
||||
handled
|
||||
@@ -100,10 +107,30 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
|
||||
onPasswordChecked?.onCheckedChanged(view, checked)
|
||||
}
|
||||
checkboxKeyFileView.setOnCheckedChangeListener { view, checked ->
|
||||
if (checked) {
|
||||
if (keyFileSelectionView.uri == null) {
|
||||
checkboxKeyFileView.isChecked = false
|
||||
}
|
||||
}
|
||||
onKeyFileChecked?.onCheckedChanged(view, checked)
|
||||
}
|
||||
checkboxHardwareView.setOnCheckedChangeListener { view, checked ->
|
||||
if (checked) {
|
||||
if (hardwareKeySelectionView.hardwareKey == null) {
|
||||
checkboxHardwareView.isChecked = false
|
||||
}
|
||||
}
|
||||
onHardwareKeyChecked?.onCheckedChanged(view, checked)
|
||||
}
|
||||
|
||||
hardwareKeySelectionView.selectionListener = { _ ->
|
||||
checkboxHardwareView.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
|
||||
fun validateCredential() {
|
||||
onValidateListener?.invoke()
|
||||
}
|
||||
|
||||
fun populatePasswordTextView(text: String?) {
|
||||
@@ -118,7 +145,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
fun populateKeyFileTextView(uri: Uri?) {
|
||||
fun populateKeyFileView(uri: Uri?) {
|
||||
if (uri == null || uri.toString().isEmpty()) {
|
||||
keyFileSelectionView.uri = null
|
||||
if (checkboxKeyFileView.isChecked)
|
||||
@@ -130,16 +157,36 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
fun populateHardwareKeyView(hardwareKey: HardwareKey?) {
|
||||
if (hardwareKey == null) {
|
||||
hardwareKeySelectionView.hardwareKey = null
|
||||
if (checkboxHardwareView.isChecked)
|
||||
checkboxHardwareView.isChecked = false
|
||||
} else {
|
||||
hardwareKeySelectionView.hardwareKey = hardwareKey
|
||||
if (!checkboxHardwareView.isChecked)
|
||||
checkboxHardwareView.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
|
||||
}
|
||||
|
||||
fun isFill(): Boolean {
|
||||
return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked
|
||||
return checkboxPasswordView.isChecked
|
||||
|| (checkboxKeyFileView.isChecked && keyFileSelectionView.uri != null)
|
||||
|| (checkboxHardwareView.isChecked && hardwareKeySelectionView.hardwareKey != null)
|
||||
}
|
||||
|
||||
fun getMainCredential(): MainCredential {
|
||||
return MainCredential().apply {
|
||||
this.masterPassword = if (checkboxPasswordView.isChecked)
|
||||
this.password = if (checkboxPasswordView.isChecked)
|
||||
passwordTextView.text?.toString() else null
|
||||
this.keyFileUri = if (checkboxKeyFileView.isChecked)
|
||||
keyFileSelectionView.uri else null
|
||||
this.hardwareKey = if (checkboxHardwareView.isChecked)
|
||||
hardwareKeySelectionView.hardwareKey else null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +198,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
// TODO HARDWARE_KEY
|
||||
return when (mCredentialStorage) {
|
||||
CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked
|
||||
CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked
|
||||
CredentialStorage.KEY_FILE -> false
|
||||
CredentialStorage.HARDWARE_KEY -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
||||
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
|
||||
setCopyButtonClickListener { label, value ->
|
||||
mOnCopyActionClickListener
|
||||
?.invoke(Field(label, ProtectedString(false, value)))
|
||||
?.invoke(Field(label, ProtectedString(true, value)))
|
||||
}
|
||||
} else {
|
||||
setCopyButtonState(TextFieldView.ButtonState.GONE)
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.text.InputFilter
|
||||
import android.text.InputType
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.ContextThemeWrapper
|
||||
|
||||
@@ -20,12 +20,9 @@
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.text.InputFilter
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.util.Linkify
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
|
||||
@@ -226,8 +226,8 @@ fun View.updateLockPaddingLeft() {
|
||||
|
||||
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
|
||||
if (!result.isSuccess) {
|
||||
result.exception?.errorId?.let { errorId ->
|
||||
Toast.makeText(this, errorId, Toast.LENGTH_LONG).show()
|
||||
result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->
|
||||
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
|
||||
} ?: result.message?.let { message ->
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@@ -236,8 +236,8 @@ fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
|
||||
|
||||
fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) {
|
||||
if (!result.isSuccess) {
|
||||
result.exception?.errorId?.let { errorId ->
|
||||
Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show()
|
||||
result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->
|
||||
Snackbar.make(this, errorMessage, Snackbar.LENGTH_LONG).asError().show()
|
||||
} ?: result.message?.let { message ->
|
||||
Snackbar.make(this, message, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user