mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
445 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b98bd740c | ||
|
|
5adeb5cde0 | ||
|
|
b949d5d861 | ||
|
|
b4264a30a4 | ||
|
|
cf799c0f68 | ||
|
|
97f0ca519b | ||
|
|
cf4047b701 | ||
|
|
40608a3eb5 | ||
|
|
99cb50d031 | ||
|
|
b0d0c35241 | ||
|
|
6044c93a4a | ||
|
|
b544b5d54d | ||
|
|
852378e484 | ||
|
|
711a344860 | ||
|
|
72087c7e5c | ||
|
|
f17d211fbd | ||
|
|
ae903ad236 | ||
|
|
7c3a15ce79 | ||
|
|
2230fe66ab | ||
|
|
84a62a32ff | ||
|
|
da8ef9340c | ||
|
|
af068349e4 | ||
|
|
56cb5953dd | ||
|
|
2fc2a9c7c1 | ||
|
|
69e7cdbc47 | ||
|
|
39d9a74a73 | ||
|
|
b609d4e182 | ||
|
|
7212c73481 | ||
|
|
3ee4caa153 | ||
|
|
28e4d929bb | ||
|
|
e8ecf28f7c | ||
|
|
3d5adbfc01 | ||
|
|
72bfc50703 | ||
|
|
a60e2e780d | ||
|
|
9210851765 | ||
|
|
a46251be7b | ||
|
|
e8ec27dc38 | ||
|
|
30dd7c567c | ||
|
|
b196145578 | ||
|
|
ac347db2d1 | ||
|
|
013c437cf7 | ||
|
|
1f600d60e3 | ||
|
|
d5ecaeb331 | ||
|
|
db8b0100de | ||
|
|
5f41177a1f | ||
|
|
0db2b7023e | ||
|
|
a2c2a21dde | ||
|
|
d7a3e7fedd | ||
|
|
5e4ee167fc | ||
|
|
c911b7c511 | ||
|
|
c79d1f1b81 | ||
|
|
daf717becd | ||
|
|
48d4483484 | ||
|
|
c6b0ee27df | ||
|
|
0053726d0b | ||
|
|
1395af88d1 | ||
|
|
2e3ade1b4a | ||
|
|
90c43acfbf | ||
|
|
90b68fd972 | ||
|
|
ef6aeceb20 | ||
|
|
ef8685f0e7 | ||
|
|
3021ed158b | ||
|
|
a57043f496 | ||
|
|
fdfd124fee | ||
|
|
71739de91a | ||
|
|
041b1fbf53 | ||
|
|
3a72b32b4a | ||
|
|
994f174300 | ||
|
|
c0f32254bb | ||
|
|
fd98dbeebe | ||
|
|
69ac6e6698 | ||
|
|
35e224d227 | ||
|
|
2da8552a53 | ||
|
|
a9a5047949 | ||
|
|
17c98f7fea | ||
|
|
c3bc890665 | ||
|
|
7a295c2541 | ||
|
|
01b1b74c6a | ||
|
|
fd25d21c72 | ||
|
|
6b1d8d24dd | ||
|
|
5d002f5128 | ||
|
|
98314c466f | ||
|
|
4f7afd7c97 | ||
|
|
a9e139ff7e | ||
|
|
4ff483a8d2 | ||
|
|
1916b79df1 | ||
|
|
98e15a7717 | ||
|
|
dfd18e3c7f | ||
|
|
8fbbaae05b | ||
|
|
8a60056866 | ||
|
|
e9d20a51a5 | ||
|
|
a28d77ba32 | ||
|
|
d143605a40 | ||
|
|
15972efb4f | ||
|
|
dae5f65c0d | ||
|
|
564b5f10ea | ||
|
|
e6e40f9bd4 | ||
|
|
bd15e36b52 | ||
|
|
43faca3061 | ||
|
|
82af9bada2 | ||
|
|
2befa68c93 | ||
|
|
4034a2bfc4 | ||
|
|
0d93e867cf | ||
|
|
8cac4eb51c | ||
|
|
933d34ff1d | ||
|
|
d34f460b98 | ||
|
|
7632face63 | ||
|
|
c4cbd2e587 | ||
|
|
ad454c2e4a | ||
|
|
fbb03c3ecf | ||
|
|
1a4d963d53 | ||
|
|
0c58992c21 | ||
|
|
0eaeb3b90b | ||
|
|
fba8443d0a | ||
|
|
601874442c | ||
|
|
fa34618d67 | ||
|
|
a60fc83379 | ||
|
|
c2c9ebe4c7 | ||
|
|
fdd86e2b9e | ||
|
|
34f0f45862 | ||
|
|
c0345d4dc4 | ||
|
|
24e859c4ce | ||
|
|
1fff0c526c | ||
|
|
1ee1bb8d95 | ||
|
|
b8e996a5af | ||
|
|
df999002af | ||
|
|
64f5a4152b | ||
|
|
0af99d1830 | ||
|
|
733f337312 | ||
|
|
1f99b1f884 | ||
|
|
c3b5598a09 | ||
|
|
26cb36ccd0 | ||
|
|
67e0496efc | ||
|
|
6c4d040564 | ||
|
|
cb5a725d50 | ||
|
|
1c30d533ad | ||
|
|
845d1a581b | ||
|
|
23bebf9597 | ||
|
|
536e038306 | ||
|
|
9e1f6d29a5 | ||
|
|
7a9469e59d | ||
|
|
c12eb3d643 | ||
|
|
da0f02e536 | ||
|
|
04bcc6631c | ||
|
|
698e3b7fb1 | ||
|
|
6de02384c1 | ||
|
|
df3bd7e0a1 | ||
|
|
c8c232639f | ||
|
|
192d6eedd0 | ||
|
|
9cae3f0794 | ||
|
|
a680db9707 | ||
|
|
fe526089d7 | ||
|
|
dfd7ade416 | ||
|
|
3cd65345c5 | ||
|
|
2d398908de | ||
|
|
756454abc3 | ||
|
|
b7619b45b1 | ||
|
|
1369a3cad9 | ||
|
|
f46c062c4e | ||
|
|
0a0abef4d4 | ||
|
|
3a8245ee74 | ||
|
|
7be554a378 | ||
|
|
7007efa627 | ||
|
|
6c37f7b12c | ||
|
|
05defff5ef | ||
|
|
e4569662ba | ||
|
|
4727f7a761 | ||
|
|
89b15e715d | ||
|
|
ef72df02e3 | ||
|
|
b6201262f1 | ||
|
|
b99fa9ffcf | ||
|
|
1497ab85b2 | ||
|
|
2ade463974 | ||
|
|
3a67ec09d5 | ||
|
|
dca800b1bb | ||
|
|
70665f110d | ||
|
|
3b39cafb99 | ||
|
|
0c1b94468d | ||
|
|
7a841bbf57 | ||
|
|
0066f1c77a | ||
|
|
14dbff603d | ||
|
|
42afa93293 | ||
|
|
2fa25a51ad | ||
|
|
2b7f41477f | ||
|
|
be0d5f80c3 | ||
|
|
3844188fcc | ||
|
|
ef81ba5a8f | ||
|
|
2871668d8f | ||
|
|
2b5ecb2f84 | ||
|
|
e397b92c36 | ||
|
|
e273eb6e03 | ||
|
|
28b624afa3 | ||
|
|
fb1e6cdc3f | ||
|
|
8b6499d040 | ||
|
|
054af507ad | ||
|
|
ac9bb9b666 | ||
|
|
809e1929e5 | ||
|
|
a1b1338d67 | ||
|
|
bd4cacfab1 | ||
|
|
e0343bdc55 | ||
|
|
b743d004e2 | ||
|
|
4b20e035b2 | ||
|
|
afe5fddc50 | ||
|
|
d68ca1b51f | ||
|
|
061b087229 | ||
|
|
bb3a379965 | ||
|
|
593b5c6338 | ||
|
|
56f8a1bf9f | ||
|
|
962b547b36 | ||
|
|
6df8ff4310 | ||
|
|
52f17140b8 | ||
|
|
75c2bb4a87 | ||
|
|
f36f6c3155 | ||
|
|
b88b92c5b0 | ||
|
|
d2c569c4f0 | ||
|
|
cb1316564e | ||
|
|
245d3f7df2 | ||
|
|
3729b3c5a0 | ||
|
|
7ce5eb3c27 | ||
|
|
43defea85e | ||
|
|
8470c4e39b | ||
|
|
1f678fc975 | ||
|
|
082c839639 | ||
|
|
600d548fce | ||
|
|
3035f9b686 | ||
|
|
6eae0f02d3 | ||
|
|
87be2f4b9e | ||
|
|
3b054504a1 | ||
|
|
a88f6b968a | ||
|
|
1fc4f150bf | ||
|
|
1f4e59cbdc | ||
|
|
b5dc8d9adf | ||
|
|
43f7e08548 | ||
|
|
05fc6f87ec | ||
|
|
daae535fa1 | ||
|
|
90c8cb3455 | ||
|
|
daeee10de9 | ||
|
|
6c1c401a71 | ||
|
|
fd7f0fceb2 | ||
|
|
26b8a616be | ||
|
|
d88882f439 | ||
|
|
09dc1d6baa | ||
|
|
f4f5e86979 | ||
|
|
a41afb7f1e | ||
|
|
32d9cfbe29 | ||
|
|
7210652567 | ||
|
|
ab15967ad7 | ||
|
|
44df4ec181 | ||
|
|
7afe356082 | ||
|
|
87597553b8 | ||
|
|
27e5f58d5e | ||
|
|
762c946d35 | ||
|
|
21a927e3e9 | ||
|
|
f93bb7436a | ||
|
|
6294fddbba | ||
|
|
c5719dfaf2 | ||
|
|
673fd67f15 | ||
|
|
25524c48e9 | ||
|
|
631b924c33 | ||
|
|
fba12bc278 | ||
|
|
e809109bb2 | ||
|
|
0e31890624 | ||
|
|
0124021ce5 | ||
|
|
74db6bf77f | ||
|
|
efde33182e | ||
|
|
ec68b22330 | ||
|
|
bf7e014f8c | ||
|
|
40e1607698 | ||
|
|
4a132f06fe | ||
|
|
0396dd975d | ||
|
|
80a38c0c54 | ||
|
|
2aa6461094 | ||
|
|
258433b3b8 | ||
|
|
79e723545c | ||
|
|
a6b20455ef | ||
|
|
9659b55bf3 | ||
|
|
ca148ef546 | ||
|
|
322b21d645 | ||
|
|
ed2ba65ecf | ||
|
|
defc8b1c57 | ||
|
|
a90ecc56d8 | ||
|
|
2faa0ac320 | ||
|
|
e391fd59fe | ||
|
|
25df86606c | ||
|
|
811f33eb3f | ||
|
|
ca7e2ed89d | ||
|
|
6f4cd79e2c | ||
|
|
da1caf4b8b | ||
|
|
4a0a8e44ca | ||
|
|
6bc2c3481b | ||
|
|
0aa89ea9ff | ||
|
|
f31a30bf47 | ||
|
|
dc75837ac7 | ||
|
|
9849b0a1da | ||
|
|
2c15a1ddd6 | ||
|
|
98eb9976cf | ||
|
|
0d9a5810b1 | ||
|
|
1adaa137a5 | ||
|
|
44a428d15a | ||
|
|
5416a7942a | ||
|
|
9e0024baf5 | ||
|
|
8d47ce38c2 | ||
|
|
80af43c0ca | ||
|
|
225f8243c2 | ||
|
|
68bc118add | ||
|
|
abbc584402 | ||
|
|
6635594639 | ||
|
|
10db77d402 | ||
|
|
000fd7e520 | ||
|
|
c8ced4ae59 | ||
|
|
9209ca9af7 | ||
|
|
c7bd90c610 | ||
|
|
fceb9c3547 | ||
|
|
030c49b571 | ||
|
|
f2d6a6a536 | ||
|
|
8f61521f05 | ||
|
|
89af7ec5d0 | ||
|
|
362f1aebed | ||
|
|
5226527cec | ||
|
|
b8464cd0e5 | ||
|
|
46e7b04d66 | ||
|
|
73111b770f | ||
|
|
995d485700 | ||
|
|
5ebbbef667 | ||
|
|
c79144400f | ||
|
|
b56556f5a2 | ||
|
|
35d5f01b8e | ||
|
|
501c647236 | ||
|
|
e77c7b84a3 | ||
|
|
d9f4e9b6ab | ||
|
|
1d7f7d2a5b | ||
|
|
df408e862b | ||
|
|
66845926d5 | ||
|
|
0a06acbf1d | ||
|
|
5d3c6798c0 | ||
|
|
b9d2b9ddc9 | ||
|
|
8a6525f45e | ||
|
|
41d89b590d | ||
|
|
8354d08ff5 | ||
|
|
16725b21f3 | ||
|
|
93ba17792f | ||
|
|
940b96cf21 | ||
|
|
157253ce24 | ||
|
|
46b3810f34 | ||
|
|
05c030dbbb | ||
|
|
5b0fd99351 | ||
|
|
2946a6e231 | ||
|
|
47896fcdc9 | ||
|
|
bc51345f0d | ||
|
|
750e1b6c43 | ||
|
|
4a2106837c | ||
|
|
39b9fc350a | ||
|
|
53560dbe29 | ||
|
|
5172dbe114 | ||
|
|
4f0ff67fdf | ||
|
|
bac971ced8 | ||
|
|
64bf5ba165 | ||
|
|
90c4a5e1b8 | ||
|
|
b08a5d9cda | ||
|
|
d5be79948d | ||
|
|
6b64be4925 | ||
|
|
e8e6eb6ca5 | ||
|
|
ee9383dd0b | ||
|
|
aee0b82cff | ||
|
|
ba5913da57 | ||
|
|
3238b9b2ce | ||
|
|
5215181c0f | ||
|
|
d3676a1454 | ||
|
|
792ce6f86e | ||
|
|
ddbd0376fc | ||
|
|
496655093c | ||
|
|
755293eff7 | ||
|
|
b6e51a1f32 | ||
|
|
08f17f3f19 | ||
|
|
14440725fc | ||
|
|
b4f84c5cd6 | ||
|
|
ad6b1cead1 | ||
|
|
e06398ff19 | ||
|
|
919ad5cfd4 | ||
|
|
b3fb721588 | ||
|
|
590497852d | ||
|
|
ebd7a9c7cf | ||
|
|
939fb2fa54 | ||
|
|
2245daffe9 | ||
|
|
784b25ada8 | ||
|
|
146d631794 | ||
|
|
cc062d7f0e | ||
|
|
298dd4af61 | ||
|
|
739ba3b14d | ||
|
|
2864ea9868 | ||
|
|
ed8d3247ca | ||
|
|
ff5de7b327 | ||
|
|
aa77552ff4 | ||
|
|
12456c0ea2 | ||
|
|
a7a93fa2a2 | ||
|
|
ff7cd29b77 | ||
|
|
f7065acc40 | ||
|
|
e4e2e5c43c | ||
|
|
48d240b010 | ||
|
|
9f2deb56b9 | ||
|
|
951257bed8 | ||
|
|
f27ce804fb | ||
|
|
1d896e83b3 | ||
|
|
a2c2925610 | ||
|
|
97e5f90603 | ||
|
|
593b7188dc | ||
|
|
4c4e61a711 | ||
|
|
140f09c77e | ||
|
|
c0fe4faf8a | ||
|
|
066fff7aca | ||
|
|
6dcbdffed4 | ||
|
|
3a07dea6d7 | ||
|
|
72ed07ef17 | ||
|
|
51512b4588 | ||
|
|
aed49e19e8 | ||
|
|
113601d09a | ||
|
|
b7d0b65715 | ||
|
|
3075591885 | ||
|
|
e9596f56db | ||
|
|
3559830738 | ||
|
|
e42beccb22 | ||
|
|
d05b7394e8 | ||
|
|
00b11ea659 | ||
|
|
99f0f096d1 | ||
|
|
416329d50d | ||
|
|
cb0d1b05d7 | ||
|
|
a2845c33f8 | ||
|
|
6d06265d94 | ||
|
|
49d03efe56 | ||
|
|
7f13a3ca76 | ||
|
|
cec9d168e3 | ||
|
|
ecf253451d | ||
|
|
89a6219659 | ||
|
|
9dec308caa | ||
|
|
fb1459de9b | ||
|
|
f3bef64461 | ||
|
|
2509caff6b | ||
|
|
232aafe2c0 | ||
|
|
ed26cb4891 | ||
|
|
0b966a6cd1 | ||
|
|
c84afd2281 | ||
|
|
1711f09547 | ||
|
|
4460a44e99 | ||
|
|
c8f7bcfb52 | ||
|
|
bd2bd842af |
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,49 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**KeePass Database**
|
||||
|
||||
- Created with: [e.g Windows KeePass 2.42]
|
||||
- Version: [e.g. 2]
|
||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
|
||||
- Size: [e.g. 150Mo]
|
||||
- Contains attachment: [e.g. Yes]
|
||||
|
||||
**KeePassDX:**
|
||||
|
||||
- Version: [e.g. 2.5.0.0beta23]
|
||||
- Build: [e.g. Free]
|
||||
- Language: [e.g. French]
|
||||
|
||||
**Android:**
|
||||
|
||||
- Device: [e.g. GalaxyS8]
|
||||
- Version: [e.g. 8.1]
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
- Browser for Autofill: [e.g. Chrome version X]
|
||||
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Bug report
|
||||
description: Report a bug.
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please check out the [Wiki](https://github.com/Kunzisoft/KeePassDX/wiki) and [existing issues](https://github.com/Kunzisoft/KeePassDX/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) to see if your problem has already been reported.
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Checks
|
||||
options:
|
||||
- label: I have read the Wiki, searched the open issues, and still think this is a new bug.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug
|
||||
attributes:
|
||||
label: "Explain the problem clearly and succinctly:"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: "Describe what you expected to happen:"
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: "KeePassDX version:"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: app-build
|
||||
attributes:
|
||||
label: "Build:"
|
||||
multiple: true
|
||||
options:
|
||||
- Free
|
||||
- Libre
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: database-version
|
||||
attributes:
|
||||
label: "Database version:"
|
||||
- type: input
|
||||
id: file-provider
|
||||
attributes:
|
||||
label: "File provider (`content://` URI)"
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: "Android version:"
|
||||
- type: input
|
||||
id: android-device
|
||||
attributes:
|
||||
label: "Android device:"
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: "Additional context:"
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea.
|
||||
labels: ["feature"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please check out the [Wiki](https://github.com/Kunzisoft/KeePassDX/wiki) and [existing issues](https://github.com/Kunzisoft/KeePassDX/issues?q=is%3Aissue%20state%3Aopen%20label%3Afeature) to see if your feature has already been reported.
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Checks
|
||||
options:
|
||||
- label: I have read the Wiki, searched the open issues, and still think this is a new feature.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: "Explain the problem clearly and succinctly:"
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: "Describe the solution you'd like:"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: "Describe alternatives you've considered:"
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: "Additional context:"
|
||||
46
CHANGELOG
46
CHANGELOG
@@ -1,3 +1,49 @@
|
||||
KeePassDX(4.1.9)
|
||||
* Fix landscape UI #2198 #2200 (@chenxiaolong)
|
||||
* Fix start loop and flash screen #2201
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.1.8)
|
||||
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
||||
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
|
||||
* Fix merge deletion #1516
|
||||
* Fix space in search #175
|
||||
* Fix deletable recycle bin #2163
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.1.7)
|
||||
* Fix CipherDatabase for biometric states #2119
|
||||
|
||||
KeePassDX(4.1.6)
|
||||
* Auto open biometric prompt from database list #2113
|
||||
* Fix Keystore errors #2114 #2115
|
||||
* Complete biometric refactoring for better compatibility
|
||||
|
||||
KeePassDX(4.1.5)
|
||||
* Fix auto prompt #2111
|
||||
|
||||
KeePassDX(4.1.4)
|
||||
* Fix UnlockManager #2098 #2101
|
||||
* Auto device unlock prompt #2105
|
||||
* Small fixes ##2066
|
||||
|
||||
KeePassDX(4.1.3)
|
||||
* Fix Autofill Registration #2089
|
||||
* Fix Biometric errors #2081
|
||||
* Fixed timestamp in copy file #1981 #1983
|
||||
* Fix Template Email #1986
|
||||
* Fix Search #2096
|
||||
|
||||
KeePassDX(4.1.2)
|
||||
* Fix URL search #1940 #1946 #2003 #2040 #2044
|
||||
* Fix Autofill popup #2054
|
||||
* Fix Group notes #2053
|
||||
* Fix Dialog background #2005 #2004 (Thx @codokie)
|
||||
* Fix OTP configuration #2042 #2065 (Thx @Dev-ClayP)
|
||||
* Fix small UI elements #1987 #2007 (Thx @ymcx)
|
||||
* RTL layout support #2021 (Thx @codokie)
|
||||
* App Metadata to translation #1823
|
||||
|
||||
KeePassDX(4.1.1)
|
||||
* Fix date parser #1933
|
||||
* Fix domain search #1820 #1936
|
||||
|
||||
75
Gemfile.lock
75
Gemfile.lock
@@ -9,31 +9,35 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1009.0)
|
||||
aws-sdk-core (3.213.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1146.0)
|
||||
aws-sdk-core (3.229.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
logger
|
||||
aws-sdk-kms (1.110.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.171.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-s3 (1.196.1)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (3.2.2)
|
||||
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.5)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
@@ -55,11 +59,11 @@ GEM
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-multipart (1.1.1)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
@@ -67,8 +71,8 @@ GEM
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.225.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.228.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -108,7 +112,7 @@ GEM
|
||||
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 (~> 0.4.1)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-plugin-versioning_android (0.1.1)
|
||||
fastlane-sirp (1.0.0)
|
||||
@@ -130,12 +134,12 @@ GEM
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
@@ -151,36 +155,39 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.7)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.8.2)
|
||||
jwt (2.9.3)
|
||||
json (2.13.2)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multi_json (1.17.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.2.1)
|
||||
naturally (2.3.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.1)
|
||||
public_suffix (6.0.1)
|
||||
rake (13.2.1)
|
||||
plist (3.7.2)
|
||||
public_suffix (6.0.2)
|
||||
rake (13.3.0)
|
||||
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.3.9)
|
||||
rouge (2.0.7)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
signet (0.20.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
@@ -207,8 +214,8 @@ GEM
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.4.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty (0.4.1)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
@@ -220,4 +227,4 @@ DEPENDENCIES
|
||||
fastlane-plugin-versioning_android
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.10
|
||||
2.6.9
|
||||
|
||||
@@ -54,7 +54,7 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
||||
|--------|--------|---------|
|
||||
| [Google Play](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free) |  | Free + [Pro](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro) |
|
||||
| [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) |  | Libre |
|
||||
| [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.free) |  | Free |
|
||||
| [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.free) |  | Free & [Libre](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.libre) |
|
||||
| [GitHub](https://github.com/Kunzisoft/KeePassDX/releases) / [Obtainium](https://github.com/ImranR98/Obtainium) |  | Free & Libre |
|
||||
|
||||
## Package authenticity from GitHub
|
||||
@@ -96,7 +96,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2024 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
Copyright © 2025 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
namespace 'com.kunzisoft.keepass'
|
||||
compileSdkVersion 34
|
||||
compileSdkVersion 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 34
|
||||
versionCode = 133
|
||||
versionName = "4.1.1"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 35
|
||||
versionCode = 143
|
||||
versionName = "4.1.9"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -91,12 +91,15 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +118,7 @@ dependencies {
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||
implementation "androidx.core:core-ktx:$android_core_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
||||
implementation "com.google.android.material:material:$android_material_version"
|
||||
// Token auto complete
|
||||
@@ -143,5 +147,4 @@ dependencies {
|
||||
|
||||
// Tests
|
||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||
androidTestImplementation "androidx.test:rules:$android_test_version"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "a20aec7cf09664b1102ec659fa51160a",
|
||||
"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, `read_only` INTEGER, `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": "readOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "updated",
|
||||
"columnName": "updated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"database_uri"
|
||||
]
|
||||
},
|
||||
"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": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"database_uri"
|
||||
]
|
||||
},
|
||||
"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, 'a20aec7cf09664b1102ec659fa51160a')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@
|
||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||
android:largeHeap="true"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/KeepassDXStyle.Night"
|
||||
tools:targetApi="s">
|
||||
<meta-data
|
||||
@@ -163,7 +164,7 @@
|
||||
android:configChanges="keyboardHidden"
|
||||
android:excludeFromRecents="true"/>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity" />
|
||||
android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||
<activity
|
||||
@@ -220,7 +221,7 @@
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
||||
android:name="com.kunzisoft.keepass.services.DeviceUnlockNotificationService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
@@ -358,78 +358,41 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
val imageView = imageViewRef.get() ?: return
|
||||
|
||||
isViewTranslateAnimationRunning = true
|
||||
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
imageView.run {
|
||||
val translationY = if (velY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(dismissAnimationInterpolator)
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) {
|
||||
originalViewBounds.top + imageView.height - imageView.top
|
||||
|
||||
imageView.run {
|
||||
val translationY = if (velY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - imageView.height - imageView.top
|
||||
}.toFloat()).apply {
|
||||
duration = dismissAnimationDuration
|
||||
interpolator = dismissAnimationInterpolator
|
||||
addUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
start()
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(dismissAnimationInterpolator)
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,137 +620,76 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
|
||||
private fun restoreViewTransform() {
|
||||
val imageView = imageViewRef.get() ?: return
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
imageView.run {
|
||||
animate()
|
||||
.setDuration(restoreAnimationDuration)
|
||||
.setInterpolator(restoreAnimationInterpolator)
|
||||
.translationY((originalViewBounds.top - top).toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
imageView.run {
|
||||
animate()
|
||||
.setDuration(restoreAnimationDuration)
|
||||
.setInterpolator(restoreAnimationInterpolator)
|
||||
.translationY((originalViewBounds.top - top).toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, (originalViewBounds.top - imageView.top).toFloat()).apply {
|
||||
duration = restoreAnimationDuration
|
||||
interpolator = restoreAnimationInterpolator
|
||||
addUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
start()
|
||||
}
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun startDragToDismissAnimation() {
|
||||
val imageView = imageViewRef.get() ?: return
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
imageView.run {
|
||||
val translationY = if (y - initialY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
|
||||
imageView.run {
|
||||
val translationY = if (y - initialY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY).apply {
|
||||
duration = dismissAnimationDuration
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
addUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
start()
|
||||
}
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun processFlingToDismiss(velocityY: Float) {
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.text.HtmlCompat
|
||||
@@ -56,7 +58,7 @@ class AboutActivity : StylishActivity() {
|
||||
var version: String
|
||||
var build: String
|
||||
try {
|
||||
version = packageManager.getPackageInfoCompat(packageName).versionName
|
||||
version = packageManager.getPackageInfoCompat(packageName).versionName ?: ""
|
||||
build = BuildConfig.BUILD_VERSION
|
||||
} catch (e: NameNotFoundException) {
|
||||
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
||||
@@ -76,6 +78,8 @@ class AboutActivity : StylishActivity() {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
textDirection = View.TEXT_DIRECTION_ANY_RTL
|
||||
|
||||
}
|
||||
|
||||
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
||||
|
||||
@@ -41,11 +41,9 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.WebDomain
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.utils.WebDomain
|
||||
import java.lang.RuntimeException
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
@@ -126,83 +124,85 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
if (autofillComponent == null) {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||
PreferencesUtil.applicationIdBlocklist(this))
|
||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||
PreferencesUtil.webDomainBlocklist(this))) {
|
||||
} else if (KeeAutofillService.autofillAllowedFor(
|
||||
applicationId = searchInfo.applicationId,
|
||||
webDomain = searchInfo.webDomain,
|
||||
context = this
|
||||
)) {
|
||||
// If database is open
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, items ->
|
||||
// Items found
|
||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||
finish()
|
||||
},
|
||||
{ openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillResult(this,
|
||||
openedDatabase,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showBlockRestartMessage()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
} else {
|
||||
// If database is open
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, items ->
|
||||
// Items found
|
||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||
finish()
|
||||
},
|
||||
{ openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillResult(this,
|
||||
openedDatabase,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchRegistration(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
registerInfo: RegisterInfo?) {
|
||||
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||
PreferencesUtil.applicationIdBlocklist(this))
|
||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||
PreferencesUtil.webDomainBlocklist(this))) {
|
||||
showBlockRestartMessage()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
} else {
|
||||
if (KeeAutofillService.autofillAllowedFor(
|
||||
applicationId = searchInfo.applicationId,
|
||||
webDomain = searchInfo.webDomain,
|
||||
context = this
|
||||
)) {
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, _ ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
openedDatabase,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{ openedDatabase ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
openedDatabase,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, _ ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
openedDatabase,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{ openedDatabase ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
openedDatabase,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showBlockRestartMessage()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -38,13 +37,10 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
@@ -83,11 +79,13 @@ import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.EnumSet
|
||||
import java.util.UUID
|
||||
|
||||
class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
private var footer: ViewGroup? = null
|
||||
private var container: View? = null
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var appBarLayout: AppBarLayout? = null
|
||||
@@ -139,6 +137,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
// Get views
|
||||
footer = findViewById(R.id.activity_entry_footer)
|
||||
container = findViewById(R.id.activity_entry_container)
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
appBarLayout = findViewById(R.id.app_bar)
|
||||
@@ -154,8 +153,12 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
setTransparentNavigationBar {
|
||||
// To fix margin with API 27
|
||||
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
||||
container?.applyWindowInsets(EnumSet.of(
|
||||
WindowInsetPosition.TOP_MARGINS,
|
||||
WindowInsetPosition.BOTTOM_MARGINS,
|
||||
WindowInsetPosition.START_MARGINS,
|
||||
WindowInsetPosition.END_MARGINS,
|
||||
))
|
||||
}
|
||||
|
||||
// Empty title
|
||||
|
||||
@@ -97,9 +97,10 @@ import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||
import java.util.EnumSet
|
||||
import java.util.UUID
|
||||
|
||||
class EntryEditActivity : DatabaseLockActivity(),
|
||||
@@ -180,8 +181,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
|
||||
// To apply fit window with transparency
|
||||
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
|
||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
|
||||
container?.applyWindowInsets(EnumSet.of(
|
||||
WindowInsetPosition.TOP_MARGINS,
|
||||
WindowInsetPosition.BOTTOM_MARGINS,
|
||||
WindowInsetPosition.START_MARGINS,
|
||||
WindowInsetPosition.END_MARGINS,
|
||||
))
|
||||
}
|
||||
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
@@ -503,7 +508,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
// Padding if lock button visible
|
||||
entryEditAddToolBar?.updateLockPaddingLeft()
|
||||
entryEditAddToolBar?.updateLockPaddingStart()
|
||||
|
||||
mAttachmentFileBinderManager?.apply {
|
||||
registerProgressTask()
|
||||
@@ -604,16 +609,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
isVisible = isEnabled
|
||||
}
|
||||
menu?.findItem(R.id.menu_add_attachment)?.apply {
|
||||
// Attachment not compatible below KitKat
|
||||
isEnabled = !mIsTemplate
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
isVisible = isEnabled
|
||||
}
|
||||
menu?.findItem(R.id.menu_add_otp)?.apply {
|
||||
// OTP not compatible below KitKat
|
||||
isEnabled = mAllowOTP
|
||||
&& !mIsTemplate
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
isVisible = isEnabled
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
|
||||
@@ -70,7 +70,6 @@ import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
import com.kunzisoft.keepass.utils.allowCreateDocumentByStorageAccessFramework
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
@@ -263,7 +262,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
GroupActivity.launch(
|
||||
this@FileDatabaseSelectActivity,
|
||||
database,
|
||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
|
||||
false
|
||||
)
|
||||
}
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
@@ -316,6 +315,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
launchPasswordActivity(databaseUri, null, null)
|
||||
// Delete flickering for kitkat <=
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
@@ -329,13 +329,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
// Show open and create button or special mode
|
||||
when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> {
|
||||
if (packageManager.allowCreateDocumentByStorageAccessFramework()) {
|
||||
// There is an activity which can handle this intent.
|
||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||
} else{
|
||||
// No Activity found that can handle this intent.
|
||||
createDatabaseButtonView?.visibility = View.GONE
|
||||
}
|
||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||
}
|
||||
else -> {
|
||||
// Disable create button if in selection mode or request for autofill
|
||||
|
||||
@@ -48,6 +48,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
@@ -113,10 +114,11 @@ import com.kunzisoft.keepass.view.applyWindowInsets
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
||||
import org.joda.time.Instant
|
||||
import org.joda.time.LocalDateTime
|
||||
import java.util.EnumSet
|
||||
|
||||
|
||||
class GroupActivity : DatabaseLockActivity(),
|
||||
@@ -131,6 +133,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
private var header: ViewGroup? = null
|
||||
private var footer: ViewGroup? = null
|
||||
private var drawerLayout: DrawerLayout? = null
|
||||
private var constraintLayout: ConstraintLayout? = null
|
||||
private var databaseNavView: NavigationDatabaseView? = null
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var coordinatorError: CoordinatorLayout? = null
|
||||
@@ -279,6 +282,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
header = findViewById(R.id.activity_group_header)
|
||||
footer = findViewById(R.id.activity_group_footer)
|
||||
drawerLayout = findViewById(R.id.drawer_layout)
|
||||
constraintLayout = findViewById(R.id.activity_group_container_view)
|
||||
databaseNavView = findViewById(R.id.database_nav_view)
|
||||
coordinatorLayout = findViewById(R.id.group_coordinator)
|
||||
coordinatorError = findViewById(R.id.error_coordinator)
|
||||
@@ -296,8 +300,19 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
|
||||
// To apply fit window with transparency
|
||||
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||
drawerLayout?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
|
||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
|
||||
constraintLayout?.applyWindowInsets(EnumSet.of(
|
||||
WindowInsetPosition.TOP_MARGINS,
|
||||
WindowInsetPosition.BOTTOM_MARGINS,
|
||||
WindowInsetPosition.START_MARGINS,
|
||||
WindowInsetPosition.END_MARGINS,
|
||||
))
|
||||
// The background of the drawer is meant to overlap system bars, so use padding
|
||||
databaseNavView?.applyWindowInsets(EnumSet.of(
|
||||
WindowInsetPosition.TOP_PADDING,
|
||||
WindowInsetPosition.BOTTOM_PADDING,
|
||||
// Only on the start side, since the drawer is anchored to one side of the screen
|
||||
WindowInsetPosition.START_PADDING,
|
||||
))
|
||||
}
|
||||
|
||||
lockView?.setOnClickListener {
|
||||
@@ -343,7 +358,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
mExternalFileHelper?.createDocument(
|
||||
getString(R.string.database_file_name_default) +
|
||||
"_" +
|
||||
Instant.now().toString() +
|
||||
LocalDateTime.now().toString() +
|
||||
mDatabase?.defaultFileExtension)
|
||||
}
|
||||
R.id.menu_lock_all -> {
|
||||
@@ -1130,7 +1145,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
View.GONE
|
||||
}
|
||||
// Padding if lock button visible
|
||||
toolbarAction?.updateLockPaddingLeft()
|
||||
toolbarAction?.updateLockPaddingStart()
|
||||
|
||||
loadGroup()
|
||||
}
|
||||
@@ -1373,7 +1388,8 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
else -> {
|
||||
// Load the previous group
|
||||
loadMainGroup(mPreviousGroupsIds.removeLast())
|
||||
loadMainGroup(mPreviousGroupsIds
|
||||
.removeAt(mPreviousGroupsIds.lastIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
@@ -212,7 +212,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
// Padding if lock button visible
|
||||
toolbar.updateLockPaddingLeft()
|
||||
toolbar.updateLockPaddingStart()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
|
||||
@@ -18,7 +18,7 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||
|
||||
class KeyGeneratorActivity : DatabaseLockActivity() {
|
||||
@@ -84,7 +84,7 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
// Padding if lock button visible
|
||||
toolbar.updateLockPaddingLeft()
|
||||
toolbar.updateLockPaddingStart()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
|
||||
@@ -32,7 +32,6 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
@@ -43,32 +42,42 @@ import androidx.appcompat.widget.Toolbar
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.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.model.CipherDecryptDatabase
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
import com.kunzisoft.keepass.model.DatabaseFile
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||
import com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||
@@ -79,26 +88,31 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
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 com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
|
||||
class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||
class MainCredentialActivity : DatabaseModeActivity() {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
private var filenameView: TextView? = null
|
||||
private var logotypeButton: View? = null
|
||||
private var advancedUnlockButton: View? = null
|
||||
private var deviceUnlockButton: View? = null
|
||||
private var mainCredentialView: MainCredentialView? = null
|
||||
private var confirmButtonView: Button? = null
|
||||
private var infoContainerView: ViewGroup? = null
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
||||
|
||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
|
||||
private val mDeviceUnlockViewModel: DeviceUnlockViewModel? by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ViewModelProvider(this)[DeviceUnlockViewModel::class.java]
|
||||
} else null
|
||||
}
|
||||
|
||||
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||
|
||||
@@ -131,7 +145,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
|
||||
filenameView = findViewById(R.id.filename)
|
||||
logotypeButton = findViewById(R.id.activity_password_logotype)
|
||||
advancedUnlockButton = findViewById(R.id.fragment_advanced_unlock_container_view)
|
||||
deviceUnlockButton = findViewById(R.id.fragment_device_unlock_container_view)
|
||||
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||
@@ -140,7 +154,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||
} else {
|
||||
PreferencesUtil.enableReadOnlyDatabase(this)
|
||||
false
|
||||
}
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
|
||||
@@ -166,21 +180,15 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
|
||||
// Listen password checkbox to init advanced unlock and confirmation button
|
||||
mainCredentialView?.onPasswordChecked =
|
||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableConfirmationButton()
|
||||
}
|
||||
mainCredentialView?.onKeyFileChecked =
|
||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableConfirmationButton()
|
||||
}
|
||||
mainCredentialView?.onHardwareKeyChecked =
|
||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableConfirmationButton()
|
||||
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel?.checkConditionToStoreCredential(
|
||||
condition = verified
|
||||
)
|
||||
}
|
||||
// TODO Async by ViewModel
|
||||
enableConfirmationButton()
|
||||
}
|
||||
|
||||
// Observe if default database
|
||||
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||
@@ -202,6 +210,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
mForceReadOnly = databaseFileNotExists
|
||||
|
||||
// Restore read-only state from database file if not forced
|
||||
if (!mForceReadOnly) {
|
||||
databaseFile?.readOnly?.let { savedReadOnlyState ->
|
||||
mReadOnly = savedReadOnlyState
|
||||
}
|
||||
}
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Post init uri with KeyFile only if needed
|
||||
@@ -228,20 +243,55 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
|
||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel?.let { deviceUnlockViewModel ->
|
||||
deviceUnlockViewModel.uiState.collect { uiState ->
|
||||
// New value received
|
||||
uiState.credentialRequiredCipher?.let { cipher ->
|
||||
deviceUnlockViewModel.encryptCredential(
|
||||
credential = getCredentialForEncryption(),
|
||||
cipher = cipher
|
||||
)
|
||||
}
|
||||
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||
onCredentialEncrypted(cipherEncryptDatabase)
|
||||
deviceUnlockViewModel.consumeCredentialEncrypted()
|
||||
}
|
||||
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||
onCredentialDecrypted(cipherDecryptDatabase)
|
||||
deviceUnlockViewModel.consumeCredentialDecrypted()
|
||||
}
|
||||
uiState.exception?.let { error ->
|
||||
Snackbar.make(
|
||||
coordinatorLayout,
|
||||
deviceUnlockError(error, this@MainCredentialActivity),
|
||||
Snackbar.LENGTH_LONG
|
||||
).asError().show()
|
||||
deviceUnlockViewModel.exceptionShown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Init Biometric elements only if allowed
|
||||
if (PreferencesUtil.isAdvancedUnlockEnable(this)) {
|
||||
advancedUnlockFragment = supportFragmentManager
|
||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
||||
if (advancedUnlockFragment == null) {
|
||||
advancedUnlockFragment = AdvancedUnlockFragment().also {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& PreferencesUtil.isDeviceUnlockEnable(this)) {
|
||||
deviceUnlockFragment = supportFragmentManager
|
||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment?
|
||||
if (deviceUnlockFragment == null) {
|
||||
deviceUnlockFragment = DeviceUnlockFragment().also {
|
||||
supportFragmentManager.commit {
|
||||
replace(
|
||||
R.id.fragment_advanced_unlock_container_view,
|
||||
R.id.fragment_device_unlock_container_view,
|
||||
it,
|
||||
UNLOCK_FRAGMENT_TAG
|
||||
)
|
||||
@@ -258,18 +308,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||
}
|
||||
|
||||
// Don't allow auto open prompt if lock become when UI visible
|
||||
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
}
|
||||
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
|
||||
mDatabase?.let { database ->
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
@@ -296,9 +337,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
// Recheck advanced unlock if error
|
||||
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
|
||||
|
||||
if (result.isSuccess) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
} else {
|
||||
@@ -400,23 +438,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
}
|
||||
|
||||
override fun retrieveCredentialForEncryption(): ByteArray {
|
||||
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||
?: byteArrayOf()
|
||||
}
|
||||
|
||||
override fun conditionToStoreCredential(): Boolean {
|
||||
return mainCredentialView?.conditionToStoreCredential() == true
|
||||
}
|
||||
|
||||
override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||
// Load the database if password is registered with biometric
|
||||
loadDatabase(mDatabaseFileUri,
|
||||
mainCredentialView?.getMainCredential(),
|
||||
cipherEncryptDatabase
|
||||
)
|
||||
}
|
||||
|
||||
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
|
||||
override fun passwordToStore(password: String?): ByteArray? {
|
||||
return password?.toByteArray()
|
||||
@@ -433,7 +454,20 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||
private fun getCredentialForEncryption(): ByteArray {
|
||||
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||
?: byteArrayOf()
|
||||
}
|
||||
|
||||
private fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||
// Load the database if password is registered with biometric
|
||||
loadDatabase(mDatabaseFileUri,
|
||||
mainCredentialView?.getMainCredential(),
|
||||
cipherEncryptDatabase
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||
// Load the database if password is retrieve from biometric
|
||||
// Retrieve from biometric
|
||||
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||
@@ -485,7 +519,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
loadDatabase()
|
||||
} else {
|
||||
// Init Biometric elements
|
||||
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel?.connect(databaseFileUri)
|
||||
}
|
||||
}
|
||||
|
||||
enableConfirmationButton()
|
||||
@@ -513,13 +549,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
// Reinit locking activity UI variable
|
||||
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||
super.onSaveInstanceState(outState)
|
||||
@@ -635,7 +664,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
try {
|
||||
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to find read mode menu")
|
||||
Log.e(TAG, "Unable to find read mode menu", e)
|
||||
}
|
||||
performedNextEducation(menu)
|
||||
},
|
||||
@@ -645,17 +674,17 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !readOnlyEducationPerformed) {
|
||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
|
||||
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this)
|
||||
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||
&& advancedUnlockButton != null) {
|
||||
&& deviceUnlockButton != null) {
|
||||
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
|
||||
advancedUnlockButton!!,
|
||||
deviceUnlockButton!!,
|
||||
{
|
||||
startActivity(
|
||||
Intent(
|
||||
this,
|
||||
AdvancedUnlockSettingsActivity::class.java
|
||||
DeviceUnlockSettingsActivity::class.java
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -664,7 +693,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (ignored: Exception) {}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,6 +714,12 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
R.id.menu_open_file_read_mode_key -> {
|
||||
mReadOnly = !mReadOnly
|
||||
changeOpenFileReadIcon(item)
|
||||
// Save the read-only state to database
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
FileDatabaseHistoryAction.getInstance(applicationContext).addOrUpdateDatabaseFile(
|
||||
DatabaseFile(databaseUri = databaseUri, readOnly = mReadOnly)
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||
}
|
||||
@@ -692,6 +727,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = MainCredentialActivity::class.java.name
|
||||
|
||||
@@ -248,11 +248,7 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private fun retrieveGroupInfoFromViews() {
|
||||
mGroupInfo.title = nameTextView.text.toString()
|
||||
// Only if there
|
||||
val newNotes = notesTextView.text.toString()
|
||||
if (newNotes.isNotEmpty()) {
|
||||
mGroupInfo.notes = newNotes
|
||||
}
|
||||
mGroupInfo.notes = notesTextView.text?.toString()
|
||||
mGroupInfo.expires = expirationView.activation
|
||||
mGroupInfo.expiryTime = expirationView.dateTime
|
||||
mGroupInfo.searchable = searchableView.getValue()
|
||||
|
||||
@@ -41,6 +41,7 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
|
||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||
@@ -224,6 +225,9 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
|
||||
|
||||
// Ensure that the UX does not prevent user from hiding/unhiding text
|
||||
otpSecretContainer?.errorIconDrawable = null
|
||||
|
||||
// Set the default value of OTP element
|
||||
upgradeType()
|
||||
upgradeTokenType()
|
||||
@@ -310,11 +314,16 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
s?.toString()?.let { userString ->
|
||||
try {
|
||||
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
||||
otpSecretContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||
if (userString.length >= MIN_OTP_SECRET) {
|
||||
try {
|
||||
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
||||
otpSecretContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||
}
|
||||
} else {
|
||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_length,
|
||||
MIN_OTP_SECRET)
|
||||
}
|
||||
mSecretWellFormed = otpSecretContainer?.error == null
|
||||
}
|
||||
|
||||
@@ -176,21 +176,14 @@ class SortDialogFragment : DatabaseDialogFragment() {
|
||||
return bundle
|
||||
}
|
||||
|
||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean): SortDialogFragment {
|
||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean,
|
||||
recycleBinBottom: Boolean): SortDialogFragment {
|
||||
recycleBinBottom: Boolean?): SortDialogFragment {
|
||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||
recycleBinBottom?.let {
|
||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||
}
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
|
||||
@@ -76,9 +76,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
|
||||
private var mRecycleBinEnable: Boolean = false
|
||||
private var mRecycleBin: Group? = null
|
||||
|
||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
@@ -102,21 +99,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
R.id.menu_sort -> {
|
||||
context?.let { context ->
|
||||
val sortDialogFragment: SortDialogFragment =
|
||||
if (mRecycleBinEnable) {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
if (mDatabase?.isRecycleBinEnabled == true) {
|
||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
)
|
||||
} else {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context)
|
||||
)
|
||||
}
|
||||
|
||||
} else null
|
||||
)
|
||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||
}
|
||||
true
|
||||
@@ -165,9 +155,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
||||
mRecycleBin = database?.recycleBin
|
||||
|
||||
context?.let { context ->
|
||||
database?.let { database ->
|
||||
mAdapter = NodesAdapter(context, database).apply {
|
||||
@@ -312,6 +299,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
}
|
||||
|
||||
private fun containsRecycleBin(nodes: List<Node>): Boolean {
|
||||
return mDatabase?.isRecycleBinEnabled == true
|
||||
&& nodes.any { it == mDatabase?.recycleBin }
|
||||
}
|
||||
|
||||
fun actionNodesCallback(database: ContextualDatabase,
|
||||
nodes: List<Node>,
|
||||
menuListener: NodesActionMenuListener?,
|
||||
@@ -336,8 +328,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
// Open and Edit for a single item
|
||||
if (nodes.size == 1) {
|
||||
// Edition
|
||||
if (database.isReadOnly
|
||||
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
|
||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
} else {
|
||||
@@ -357,8 +348,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (database.isReadOnly
|
||||
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
|
||||
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||
menu?.removeItem(R.id.menu_delete)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,14 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.LockReceiver
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||
import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
PasswordEncodingDialogFragment.Listener {
|
||||
@@ -184,8 +188,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mLockReceiver = LockReceiver {
|
||||
mDatabase = null
|
||||
closeDatabase(database)
|
||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||
mExitLock = true
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
@@ -413,8 +415,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
LOCKING_ACTIVITY_UI_VISIBLE = true
|
||||
}
|
||||
|
||||
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
||||
@@ -429,8 +429,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||
|
||||
super.onPause()
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
@@ -480,9 +478,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
|
||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||
|
||||
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.stylish
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -77,7 +78,18 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
startActivity(intent)
|
||||
}
|
||||
finish()
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
overrideActivityTransition(
|
||||
OVERRIDE_TRANSITION_OPEN,
|
||||
android.R.anim.fade_in,
|
||||
android.R.anim.fade_out
|
||||
)
|
||||
else
|
||||
overridePendingTransition(
|
||||
android.R.anim.fade_in,
|
||||
android.R.anim.fade_out
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.adapters
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
@@ -529,6 +530,8 @@ class NodesAdapter (
|
||||
holder?.otpToken?.apply {
|
||||
text = otpElement?.tokenString
|
||||
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
||||
textDirection = View.TEXT_DIRECTION_LTR
|
||||
|
||||
}
|
||||
holder?.otpContainer?.setOnClickListener {
|
||||
otpElement?.token?.let { token ->
|
||||
|
||||
@@ -19,15 +19,53 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.app
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class App : MultiDexApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver)
|
||||
|
||||
Stylish.load(this)
|
||||
PRNGFixes.apply()
|
||||
}
|
||||
}
|
||||
|
||||
object AppLifecycleObserver : DefaultLifecycleObserver {
|
||||
|
||||
var isAppInForeground: Boolean = false
|
||||
private set
|
||||
|
||||
var lockBackgroundEvent = false
|
||||
|
||||
private val _appJustLaunched = MutableSharedFlow<Unit>(replay = 0)
|
||||
val appJustLaunched = _appJustLaunched.asSharedFlow()
|
||||
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
super.onStart(owner)
|
||||
val wasPreviouslyInBackground = !isAppInForeground
|
||||
isAppInForeground = true
|
||||
if (!lockBackgroundEvent && wasPreviouslyInBackground) {
|
||||
GlobalScope.launch {
|
||||
_appJustLaunched.emit(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
super.onStop(owner)
|
||||
isAppInForeground = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
package com.kunzisoft.keepass.app;
|
||||
|
||||
/*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will Google be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, as long as the origin is not misrepresented.
|
||||
*/
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SecureRandomSpi;
|
||||
import java.security.Security;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Fixes for the output of the default PRNG having low entropy.
|
||||
*
|
||||
* The fixes need to be applied via {@link #apply()} before any use of Java
|
||||
* Cryptography Architecture primitives. A good place to invoke them is in the
|
||||
* application's {@code onCreate}.
|
||||
*/
|
||||
public final class PRNGFixes {
|
||||
|
||||
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
|
||||
getBuildFingerprintAndDeviceSerial();
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private PRNGFixes() {}
|
||||
|
||||
/**
|
||||
* Applies all fixes.
|
||||
*
|
||||
* @throws SecurityException if a fix is needed but could not be applied.
|
||||
*/
|
||||
public static void apply() {
|
||||
try {
|
||||
if (supportedOnThisDevice()) {
|
||||
applyOpenSSLFix();
|
||||
installLinuxPRNGSecureRandom();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Do nothing, do the best we can to implement the workaround
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean supportedOnThisDevice() {
|
||||
// Blacklist on samsung devices
|
||||
if (Build.MANUFACTURER.toLowerCase(Locale.ENGLISH).contains("samsung")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onSELinuxEnforce()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File urandom = new File("/dev/urandom");
|
||||
|
||||
// Test permissions
|
||||
if ( !(urandom.canRead() && urandom.canWrite()) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Test actually writing to urandom
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(urandom);
|
||||
fos.write(0);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
private static boolean onSELinuxEnforce() {
|
||||
try {
|
||||
ProcessBuilder builder = new ProcessBuilder("getenforce");
|
||||
builder.redirectErrorStream(true);
|
||||
java.lang.Process process = builder.start();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
process.waitFor();
|
||||
|
||||
String output = reader.readLine();
|
||||
|
||||
if (output == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return output.toLowerCase(Locale.US).startsWith("enforcing");
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
|
||||
* fix is not needed.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void applyOpenSSLFix() throws SecurityException {
|
||||
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
|
||||
|| (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_seed", byte[].class)
|
||||
.invoke(null, generateSeed());
|
||||
|
||||
// Mix output of Linux PRNG into OpenSSL's PRNG
|
||||
int bytesRead = (Integer) Class.forName(
|
||||
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_load_file", String.class, long.class)
|
||||
.invoke(null, "/dev/urandom", 1024);
|
||||
if (bytesRead != 1024) {
|
||||
throw new IOException(
|
||||
"Unexpected number of bytes read from Linux PRNG: "
|
||||
+ bytesRead);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
|
||||
* default. Does nothing if the implementation is already the default or if
|
||||
* there is not need to install the implementation.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void installLinuxPRNGSecureRandom()
|
||||
throws SecurityException {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
// Install a Linux PRNG-based SecureRandom implementation as the
|
||||
// default, if not yet installed.
|
||||
Provider[] secureRandomProviders =
|
||||
Security.getProviders("SecureRandom.SHA1PRNG");
|
||||
if ((secureRandomProviders == null)
|
||||
|| (secureRandomProviders.length < 1)
|
||||
|| (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
secureRandomProviders[0].getClass()))) {
|
||||
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
|
||||
}
|
||||
|
||||
// Assert that new SecureRandom() and
|
||||
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
|
||||
// by the Linux PRNG-based SecureRandom implementation.
|
||||
SecureRandom rng1 = new SecureRandom();
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng1.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"new SecureRandom() backed by wrong Provider: "
|
||||
+ rng1.getProvider().getClass());
|
||||
}
|
||||
|
||||
SecureRandom rng2;
|
||||
try {
|
||||
rng2 = SecureRandom.getInstance("SHA1PRNG");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new SecurityException("SHA1PRNG not available", e);
|
||||
}
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng2.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
|
||||
+ " Provider: " + rng2.getProvider().getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code Provider} of {@code SecureRandom} engines which pass through
|
||||
* all requests to the Linux PRNG.
|
||||
*/
|
||||
private static class LinuxPRNGSecureRandomProvider extends Provider {
|
||||
|
||||
public LinuxPRNGSecureRandomProvider() {
|
||||
super("LinuxPRNG",
|
||||
1.0,
|
||||
"A Linux-specific random number provider that uses"
|
||||
+ " /dev/urandom");
|
||||
// Although /dev/urandom is not a SHA-1 PRNG, some apps
|
||||
// explicitly request a SHA1PRNG SecureRandom and we thus need to
|
||||
// prevent them from getting the default implementation whose output
|
||||
// may have low entropy.
|
||||
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
|
||||
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
|
||||
* ({@code /dev/urandom}).
|
||||
*/
|
||||
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
|
||||
|
||||
/*
|
||||
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
|
||||
* are passed through to the Linux PRNG (/dev/urandom). Instances of
|
||||
* this class seed themselves by mixing in the current time, PID, UID,
|
||||
* build fingerprint, and hardware serial number (where available) into
|
||||
* Linux PRNG.
|
||||
*
|
||||
* Concurrency: Read requests to the underlying Linux PRNG are
|
||||
* serialized (on sLock) to ensure that multiple threads do not get
|
||||
* duplicated PRNG output.
|
||||
*/
|
||||
|
||||
private static final File URANDOM_FILE = new File("/dev/urandom");
|
||||
|
||||
|
||||
private static final Object sLock = new Object();
|
||||
|
||||
/**
|
||||
* Input stream for reading from Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static DataInputStream sUrandomIn;
|
||||
|
||||
/**
|
||||
* Output stream for writing to Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static OutputStream sUrandomOut;
|
||||
|
||||
/**
|
||||
* Whether this engine instance has been seeded. This is needed because
|
||||
* each instance needs to seed itself if the client does not explicitly
|
||||
* seed it.
|
||||
*/
|
||||
private boolean mSeeded;
|
||||
|
||||
@Override
|
||||
protected void engineSetSeed(byte[] bytes) {
|
||||
try {
|
||||
OutputStream out;
|
||||
synchronized (sLock) {
|
||||
out = getUrandomOutputStream();
|
||||
}
|
||||
out.write(bytes);
|
||||
out.flush();
|
||||
mSeeded = true;
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException(
|
||||
"Failed to mix seed into " + URANDOM_FILE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void engineNextBytes(byte[] bytes) {
|
||||
if (!mSeeded) {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
engineSetSeed(generateSeed());
|
||||
}
|
||||
|
||||
try {
|
||||
DataInputStream in;
|
||||
synchronized (sLock) {
|
||||
in = getUrandomInputStream();
|
||||
}
|
||||
synchronized (in) {
|
||||
in.readFully(bytes);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException(
|
||||
"Failed to read from " + URANDOM_FILE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] engineGenerateSeed(int size) {
|
||||
byte[] seed = new byte[size];
|
||||
engineNextBytes(seed);
|
||||
return seed;
|
||||
}
|
||||
|
||||
private DataInputStream getUrandomInputStream() {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomIn == null) {
|
||||
// NOTE: Consider inserting a BufferedInputStream between
|
||||
// DataInputStream and FileInputStream if you need higher
|
||||
// PRNG output performance and can live with future PRNG
|
||||
// output being pulled into this process prematurely.
|
||||
try {
|
||||
sUrandomIn = new DataInputStream(
|
||||
new FileInputStream(URANDOM_FILE));
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to open "
|
||||
+ URANDOM_FILE + " for reading", e);
|
||||
}
|
||||
}
|
||||
return sUrandomIn;
|
||||
}
|
||||
}
|
||||
|
||||
private OutputStream getUrandomOutputStream() {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomOut == null) {
|
||||
try {
|
||||
sUrandomOut = new FileOutputStream(URANDOM_FILE);
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to open "
|
||||
+ URANDOM_FILE + " for writing", e);
|
||||
}
|
||||
}
|
||||
return sUrandomOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a device- and invocation-specific seed to be mixed into the
|
||||
* Linux PRNG.
|
||||
*/
|
||||
private static byte[] generateSeed() {
|
||||
try {
|
||||
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
|
||||
DataOutputStream seedBufferOut =
|
||||
new DataOutputStream(seedBuffer);
|
||||
seedBufferOut.writeLong(System.currentTimeMillis());
|
||||
seedBufferOut.writeLong(System.nanoTime());
|
||||
seedBufferOut.writeInt(Process.myPid());
|
||||
seedBufferOut.writeInt(Process.myUid());
|
||||
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
|
||||
seedBufferOut.close();
|
||||
return seedBuffer.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to generate seed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hardware serial number of this device.
|
||||
*
|
||||
* @return serial number or {@code null} if not available.
|
||||
*/
|
||||
private static String getDeviceSerialNumber() {
|
||||
// We're using the Reflection API because Build.SERIAL is only available
|
||||
// since API Level 9 (Gingerbread, Android 2.3).
|
||||
try {
|
||||
return (String) Build.class.getField("SERIAL").get(null);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getBuildFingerprintAndDeviceSerial() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String fingerprint = Build.FINGERPRINT;
|
||||
if (fingerprint != null) {
|
||||
result.append(fingerprint);
|
||||
}
|
||||
String serial = getDeviceSerialNumber();
|
||||
if (serial != null) {
|
||||
result.append(serial);
|
||||
}
|
||||
try {
|
||||
return result.toString().getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 encoding not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,11 @@ import android.content.Context
|
||||
import androidx.room.AutoMigration
|
||||
|
||||
@Database(
|
||||
version = 2,
|
||||
version = 3,
|
||||
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
|
||||
autoMigrations = [
|
||||
AutoMigration (from = 1, to = 2)
|
||||
AutoMigration (from = 1, to = 2),
|
||||
AutoMigration (from = 2, to = 3)
|
||||
]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
@@ -28,9 +28,10 @@ import android.os.IBinder
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||
import com.kunzisoft.keepass.services.DeviceUnlockNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.IOActionTask
|
||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||
@@ -43,19 +44,19 @@ class CipherDatabaseAction(context: Context) {
|
||||
AppDatabase.getDatabase(applicationContext).cipherDatabaseDao()
|
||||
|
||||
// Temp DAO to easily remove content if object no longer in memory
|
||||
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||
private var useTempDao = PreferencesUtil.isTempDeviceUnlockEnable(applicationContext)
|
||||
|
||||
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
|
||||
private var mBinder: DeviceUnlockNotificationService.DeviceUnlockBinder? = null
|
||||
private var mServiceConnection: ServiceConnection? = null
|
||||
|
||||
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
|
||||
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
|
||||
private var mDeviceUnlockBroadcastReceiver = DeviceUnlockNotificationService.DeviceUnlockReceiver {
|
||||
deleteAll()
|
||||
removeAllDataAndDetach()
|
||||
}
|
||||
|
||||
fun reloadPreferences() {
|
||||
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||
private fun reloadPreferences() {
|
||||
useTempDao = PreferencesUtil.isTempDeviceUnlockEnable(applicationContext)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -70,15 +71,15 @@ class CipherDatabaseAction(context: Context) {
|
||||
|
||||
@Synchronized
|
||||
private fun attachService(performedAction: () -> Unit) {
|
||||
ContextCompat.registerReceiver(applicationContext, mAdvancedUnlockBroadcastReceiver,
|
||||
ContextCompat.registerReceiver(applicationContext, mDeviceUnlockBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
|
||||
addAction(DeviceUnlockNotificationService.REMOVE_DEVICE_UNLOCK_KEY_ACTION)
|
||||
}, ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
|
||||
mServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
|
||||
mBinder = (serviceBinder as DeviceUnlockNotificationService.DeviceUnlockBinder)
|
||||
performedAction.invoke()
|
||||
}
|
||||
|
||||
@@ -87,7 +88,7 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
AdvancedUnlockNotificationService.bindService(applicationContext,
|
||||
DeviceUnlockNotificationService.bindService(applicationContext,
|
||||
mServiceConnection!!,
|
||||
Context.BIND_AUTO_CREATE)
|
||||
} catch (e: Exception) {
|
||||
@@ -99,11 +100,11 @@ class CipherDatabaseAction(context: Context) {
|
||||
@Synchronized
|
||||
private fun detachService() {
|
||||
try {
|
||||
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
|
||||
applicationContext.unregisterReceiver(mDeviceUnlockBroadcastReceiver)
|
||||
} catch (_: Exception) {}
|
||||
|
||||
mServiceConnection?.let {
|
||||
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
|
||||
DeviceUnlockNotificationService.unbindService(applicationContext, it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,23 +124,27 @@ class CipherDatabaseAction(context: Context) {
|
||||
private fun onClear() {
|
||||
mBinder = null
|
||||
mServiceConnection = null
|
||||
mDatabaseListeners.forEach {
|
||||
it.onCipherDatabaseCleared()
|
||||
mDatabaseListeners.forEach { listener ->
|
||||
listener.onCipherDatabaseCleared()
|
||||
}
|
||||
}
|
||||
|
||||
interface CipherDatabaseListener {
|
||||
fun onCipherDatabaseRetrieved(databaseUri: Uri, cipherDatabase: CipherEncryptDatabase?)
|
||||
fun onCipherDatabaseAddedOrUpdated(cipherDatabase: CipherEncryptDatabase)
|
||||
fun onCipherDatabaseDeleted(databaseUri: Uri)
|
||||
fun onAllCipherDatabasesDeleted()
|
||||
fun onCipherDatabaseCleared()
|
||||
}
|
||||
|
||||
fun getCipherDatabase(databaseUri: Uri,
|
||||
cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
|
||||
cipherDatabaseResultListener: ((CipherEncryptDatabase?) -> Unit)? = null) {
|
||||
if (useTempDao) {
|
||||
serviceActionTask {
|
||||
var cipherDatabase: CipherEncryptDatabase? = null
|
||||
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
|
||||
cipherDatabase = CipherEncryptDatabase().apply {
|
||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
||||
this.databaseUri = cipherDatabaseEntity.databaseUri.toUri()
|
||||
this.encryptedValue = Base64.decode(
|
||||
cipherDatabaseEntity.encryptedValue,
|
||||
BASE64_FLAG
|
||||
@@ -150,7 +155,11 @@ class CipherDatabaseAction(context: Context) {
|
||||
)
|
||||
}
|
||||
}
|
||||
cipherDatabaseResultListener.invoke(cipherDatabase)
|
||||
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
|
||||
mDatabaseListeners.forEach { listener ->
|
||||
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
@@ -158,7 +167,7 @@ class CipherDatabaseAction(context: Context) {
|
||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||
?.let { cipherDatabaseEntity ->
|
||||
CipherEncryptDatabase().apply {
|
||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
||||
this.databaseUri = cipherDatabaseEntity.databaseUri.toUri()
|
||||
this.encryptedValue = Base64.decode(
|
||||
cipherDatabaseEntity.encryptedValue,
|
||||
Base64.NO_WRAP
|
||||
@@ -170,21 +179,29 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener.invoke(it)
|
||||
{ cipherDatabase ->
|
||||
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
|
||||
mDatabaseListeners.forEach { listener ->
|
||||
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
|
||||
}
|
||||
}
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun containsCipherDatabase(databaseUri: Uri,
|
||||
private fun containsCipherDatabase(databaseUri: Uri?,
|
||||
contains: (Boolean) -> Unit) {
|
||||
getCipherDatabase(databaseUri) {
|
||||
contains.invoke(it != null)
|
||||
if (databaseUri == null) {
|
||||
contains.invoke(false)
|
||||
} else {
|
||||
getCipherDatabase(databaseUri) {
|
||||
contains.invoke(it != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCipherParameters(databaseUri: Uri) {
|
||||
fun resetCipherParameters(databaseUri: Uri?) {
|
||||
containsCipherDatabase(databaseUri) { contains ->
|
||||
if (contains) {
|
||||
mBinder?.resetTimer()
|
||||
@@ -206,7 +223,11 @@ class CipherDatabaseAction(context: Context) {
|
||||
// The only case to create service (not needed to get an info)
|
||||
serviceActionTask(true) {
|
||||
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
cipherDatabaseResultListener?.invoke() ?: run {
|
||||
mDatabaseListeners.forEach { listener ->
|
||||
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
@@ -221,7 +242,11 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
cipherDatabaseResultListener?.invoke() ?: run {
|
||||
mDatabaseListeners.forEach { listener ->
|
||||
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
|
||||
}
|
||||
}
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
@@ -233,7 +258,11 @@ class CipherDatabaseAction(context: Context) {
|
||||
if (useTempDao) {
|
||||
serviceActionTask {
|
||||
mBinder?.deleteByDatabaseUri(databaseUri)
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
cipherDatabaseResultListener?.invoke() ?: run {
|
||||
mDatabaseListeners.forEach { listener ->
|
||||
listener.onCipherDatabaseDeleted(databaseUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
@@ -241,10 +270,15 @@ class CipherDatabaseAction(context: Context) {
|
||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
cipherDatabaseResultListener?.invoke() ?: run {
|
||||
mDatabaseListeners.forEach { listener ->
|
||||
listener.onCipherDatabaseDeleted(databaseUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
reloadPreferences()
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
@@ -259,8 +293,12 @@ class CipherDatabaseAction(context: Context) {
|
||||
cipherDatabaseDao.deleteAll()
|
||||
}
|
||||
).execute()
|
||||
mDatabaseListeners.forEach { listener ->
|
||||
listener.onAllCipherDatabasesDeleted()
|
||||
}
|
||||
// Unbind
|
||||
removeAllDataAndDetach()
|
||||
reloadPreferences()
|
||||
}
|
||||
|
||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||
|
||||
@@ -49,6 +49,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
databaseUri,
|
||||
fileDatabaseHistoryEntity?.keyFileUri?.parseUri(),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
|
||||
fileDatabaseHistoryEntity?.readOnly,
|
||||
fileDatabaseHistoryEntity?.databaseUri?.decodeUri(),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias
|
||||
?: ""),
|
||||
@@ -99,6 +100,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
fileDatabaseHistoryEntity.databaseUri.parseUri(),
|
||||
fileDatabaseHistoryEntity.keyFileUri?.parseUri(),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
|
||||
fileDatabaseHistoryEntity.readOnly,
|
||||
fileDatabaseHistoryEntity.databaseUri.decodeUri(),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
@@ -147,6 +149,8 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
?: "",
|
||||
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||
databaseFileToAddOrUpdate.hardwareKey?.value,
|
||||
databaseFileToAddOrUpdate.readOnly
|
||||
?: fileDatabaseHistoryRetrieve?.readOnly,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -168,6 +172,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
fileDatabaseHistory.databaseUri.parseUri(),
|
||||
fileDatabaseHistory.keyFileUri?.parseUri(),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||
fileDatabaseHistory.readOnly,
|
||||
fileDatabaseHistory.databaseUri.decodeUri(),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
@@ -195,6 +200,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
fileDatabaseHistory.databaseUri.parseUri(),
|
||||
fileDatabaseHistory.keyFileUri?.parseUri(),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||
fileDatabaseHistory.readOnly,
|
||||
fileDatabaseHistory.databaseUri.decodeUri(),
|
||||
databaseFileToDelete.databaseAlias
|
||||
)
|
||||
|
||||
@@ -38,6 +38,9 @@ data class FileDatabaseHistoryEntity(
|
||||
@ColumnInfo(name = "hardware_key")
|
||||
var hardwareKey: String?,
|
||||
|
||||
@ColumnInfo(name = "read_only")
|
||||
var readOnly: Boolean?,
|
||||
|
||||
@ColumnInfo(name = "updated")
|
||||
val updated: Long
|
||||
) {
|
||||
|
||||
@@ -21,12 +21,21 @@ package com.kunzisoft.keepass.autofill
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.service.autofill.*
|
||||
import android.service.autofill.AutofillService
|
||||
import android.service.autofill.FillCallback
|
||||
import android.service.autofill.FillRequest
|
||||
import android.service.autofill.FillResponse
|
||||
import android.service.autofill.InlinePresentation
|
||||
import android.service.autofill.Presentations
|
||||
import android.service.autofill.SaveCallback
|
||||
import android.service.autofill.SaveInfo
|
||||
import android.service.autofill.SaveRequest
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillId
|
||||
import android.widget.RemoteViews
|
||||
@@ -35,6 +44,7 @@ import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
@@ -99,8 +109,12 @@ class KeeAutofillService : AutofillService() {
|
||||
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||
|
||||
// Build search info only if applicationId or webDomain are not blocked
|
||||
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||
if (autofillAllowedFor(
|
||||
applicationId = parseResult.applicationId,
|
||||
applicationIdBlocklist = applicationIdBlocklist,
|
||||
webDomain = parseResult.webDomain,
|
||||
webDomainBlocklist = webDomainBlocklist)
|
||||
) {
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
@@ -258,7 +272,7 @@ class KeeAutofillService : AutofillService() {
|
||||
val inlinePresentationSpecs =
|
||||
inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||
&& inlinePresentationSpecs.size > 0
|
||||
&& inlinePresentationSpecs.isNotEmpty()
|
||||
) {
|
||||
val inlinePresentationSpec = inlinePresentationSpecs[0]
|
||||
|
||||
@@ -274,11 +288,7 @@ class KeeAutofillService : AutofillService() {
|
||||
this,
|
||||
0,
|
||||
Intent(this, AutofillSettingsActivity::class.java),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
).apply {
|
||||
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
||||
@@ -352,8 +362,12 @@ class KeeAutofillService : AutofillService() {
|
||||
val latestStructure = request.fillContexts.last().structure
|
||||
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||
|
||||
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||
if (autofillAllowedFor(
|
||||
applicationId = parseResult.applicationId,
|
||||
applicationIdBlocklist = applicationIdBlocklist,
|
||||
webDomain = parseResult.webDomain,
|
||||
webDomainBlocklist = webDomainBlocklist)
|
||||
) {
|
||||
Log.d(TAG, "autofill onSaveRequest password")
|
||||
|
||||
// Build expiration from date or from year and month
|
||||
@@ -414,6 +428,28 @@ class KeeAutofillService : AutofillService() {
|
||||
companion object {
|
||||
private val TAG = KeeAutofillService::class.java.name
|
||||
|
||||
fun autofillAllowedFor(applicationId: String?,
|
||||
webDomain: String?,
|
||||
context: Context
|
||||
): Boolean {
|
||||
return autofillAllowedFor(
|
||||
applicationId = applicationId,
|
||||
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(context),
|
||||
webDomain = webDomain,
|
||||
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(context))
|
||||
}
|
||||
|
||||
fun autofillAllowedFor(applicationId: String?,
|
||||
applicationIdBlocklist: Set<String>?,
|
||||
webDomain: String?,
|
||||
webDomainBlocklist: Set<String>?
|
||||
): Boolean {
|
||||
return autofillAllowedFor(applicationId, applicationIdBlocklist)
|
||||
// To prevent unrecognized autofill popup id
|
||||
&& applicationId?.contains(APPLICATION_ID_POPUP_WINDOW) != true
|
||||
&& autofillAllowedFor(webDomain, webDomainBlocklist)
|
||||
}
|
||||
|
||||
fun autofillAllowedFor(element: String?, blockList: Set<String>?): Boolean {
|
||||
element?.let { elementNotNull ->
|
||||
if (blockList?.any { appIdBlocked ->
|
||||
|
||||
@@ -27,8 +27,7 @@ import android.view.autofill.AutofillId
|
||||
import android.view.autofill.AutofillValue
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
/**
|
||||
@@ -52,7 +51,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
applicationId = windowNode.title.toString().split("/")[0]
|
||||
Log.d(TAG, "Autofill applicationId: $applicationId")
|
||||
|
||||
if (applicationId?.contains("PopupWindow:") == false) {
|
||||
if (applicationId?.contains(APPLICATION_ID_POPUP_WINDOW) == false) {
|
||||
if (parseViewNode(windowNode.rootViewNode))
|
||||
break@mainLoop
|
||||
}
|
||||
@@ -583,5 +582,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
|
||||
companion object {
|
||||
private val TAG = StructureParser::class.java.name
|
||||
|
||||
const val APPLICATION_ID_POPUP_WINDOW = "PopupWindow:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import javax.crypto.Cipher
|
||||
|
||||
data class AdvancedUnlockCryptoPrompt(var cipher: Cipher,
|
||||
@StringRes var promptTitleId: Int,
|
||||
@StringRes var promptDescriptionId: Int? = null,
|
||||
var isDeviceCredentialOperation: Boolean,
|
||||
var isBiometricOperation: Boolean)
|
||||
@@ -1,678 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
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
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
|
||||
|
||||
private var mBuilderListener: BuilderListener? = null
|
||||
|
||||
private var mAdvancedUnlockEnabled = false
|
||||
private var mAutoOpenPromptEnabled = false
|
||||
|
||||
private var advancedUnlockManager: AdvancedUnlockManager? = null
|
||||
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
|
||||
private var mAdvancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||
|
||||
var databaseFileUri: Uri? = null
|
||||
private set
|
||||
|
||||
// TODO Retrieve credential storage from app database
|
||||
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
|
||||
|
||||
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
||||
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
|
||||
private var allowOpenBiometricPrompt = false
|
||||
|
||||
private lateinit var cipherDatabaseAction : CipherDatabaseAction
|
||||
|
||||
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||
|
||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
|
||||
|
||||
// Only to fix multiple fingerprint menu #332
|
||||
private var mAllowAdvancedUnlockMenu = false
|
||||
private var mAddBiometricMenuInProgress = false
|
||||
|
||||
// Only keep connection when we request a device credential activity
|
||||
private var keepConnection = false
|
||||
|
||||
private var mDeviceCredentialResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
// To wait resume
|
||||
if (keepConnection) {
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded =
|
||||
result.resultCode == Activity.RESULT_OK
|
||||
}
|
||||
keepConnection = false
|
||||
}
|
||||
|
||||
private val menuProvider: MenuProvider = object: MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// biometric menu
|
||||
if (mAllowAdvancedUnlockMenu)
|
||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
deleteEncryptedDatabaseKey()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context)
|
||||
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mBuilderListener = context as BuilderListener
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + BuilderListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
|
||||
|
||||
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
|
||||
initAdvancedUnlockMode()
|
||||
}
|
||||
|
||||
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
|
||||
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
|
||||
onDatabaseLoaded(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val rootView = inflater.inflate(R.layout.fragment_advanced_unlock, container, false)
|
||||
|
||||
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
context?.let {
|
||||
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it)
|
||||
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
|
||||
}
|
||||
keepConnection = false
|
||||
}
|
||||
|
||||
private fun onDatabaseLoaded(databaseUri: Uri?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// To get device credential unlock result, only if same database uri
|
||||
if (databaseUri != null
|
||||
&& mAdvancedUnlockEnabled) {
|
||||
val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
|
||||
deviceCredentialAuthSucceeded?.let {
|
||||
if (databaseUri == databaseFileUri) {
|
||||
if (deviceCredentialAuthSucceeded == true) {
|
||||
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||
} else {
|
||||
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
|
||||
}
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
} ?: run {
|
||||
if (databaseUri != databaseFileUri) {
|
||||
connect(databaseUri)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlock availability and change the current mode depending of device's state
|
||||
*/
|
||||
private fun checkUnlockAvailability() {
|
||||
context?.let { context ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
allowOpenBiometricPrompt = true
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(context)) {
|
||||
// biometric not supported (by API level or hardware) so keep option hidden
|
||||
// or manually disable
|
||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context)
|
||||
if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
||||
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
|
||||
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
|
||||
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
||||
} else {
|
||||
// biometric is available but not configured, show icon but in disabled state with some information
|
||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||
} else {
|
||||
selectMode()
|
||||
}
|
||||
}
|
||||
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
if (AdvancedUnlockManager.isDeviceSecure(context)) {
|
||||
selectMode()
|
||||
} else {
|
||||
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun selectMode() {
|
||||
// Check if fingerprint well init (be called the first time the fingerprint is configured
|
||||
// and the activity still active)
|
||||
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
|
||||
advancedUnlockManager = AdvancedUnlockManager { requireActivity() }
|
||||
// callback for fingerprint findings
|
||||
advancedUnlockManager?.advancedUnlockCallback = this
|
||||
}
|
||||
// Recheck to change the mode
|
||||
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
|
||||
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
|
||||
} else {
|
||||
if (mBuilderListener?.conditionToStoreCredential() == true) {
|
||||
// listen for encryption
|
||||
toggleMode(Mode.STORE_CREDENTIAL)
|
||||
} else {
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
||||
// biometric available but no stored password found yet for this DB so show info don't listen
|
||||
toggleMode(if (containsCipher) {
|
||||
// listen for decryption
|
||||
Mode.EXTRACT_CREDENTIAL
|
||||
} else {
|
||||
// wait for typing
|
||||
Mode.WAIT_CREDENTIAL
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun toggleMode(newBiometricMode: Mode) {
|
||||
if (newBiometricMode != biometricMode) {
|
||||
biometricMode = newBiometricMode
|
||||
initAdvancedUnlockMode()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initNotAvailable() {
|
||||
showViews(false)
|
||||
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener(null)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun openBiometricSetting() {
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener {
|
||||
try {
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL))
|
||||
}
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
|
||||
@Suppress("DEPRECATION") context
|
||||
?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL))
|
||||
}
|
||||
else -> {
|
||||
context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
||||
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initSecurityUpdateRequired() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
||||
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initNotConfigured() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initKeyManagerNotAvailable() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
||||
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initWaitData() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.unavailable)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
context?.let { context ->
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener {
|
||||
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
||||
context.getString(R.string.credential_before_click_advanced_unlock_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (allowOpenBiometricPrompt) {
|
||||
if (cryptoPrompt.isDeviceCredentialOperation)
|
||||
keepConnection = true
|
||||
try {
|
||||
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
|
||||
mDeviceCredentialResultLauncher)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open advanced unlock prompt", e)
|
||||
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initEncryptData() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
advancedUnlockManager?.initEncryptData { cryptoPrompt ->
|
||||
// Set listener to open the biometric dialog and save credential
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
||||
}
|
||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initDecryptData() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.unlock)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
advancedUnlockManager?.let { unlockHelper ->
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
||||
cipherDatabase?.let {
|
||||
unlockHelper.initDecryptData(it.specParameters) { cryptoPrompt ->
|
||||
|
||||
// Set listener to open the biometric dialog and check credential
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
||||
}
|
||||
|
||||
// Auto open the biometric prompt
|
||||
if (mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt
|
||||
&& mAutoOpenPromptEnabled) {
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
||||
}
|
||||
}
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
} ?: throw UnknownDatabaseLocationException()
|
||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||
}
|
||||
|
||||
private fun initAdvancedUnlockMode() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mAllowAdvancedUnlockMenu = false
|
||||
try {
|
||||
when (biometricMode) {
|
||||
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
|
||||
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
|
||||
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
||||
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
||||
Mode.WAIT_CREDENTIAL -> initWaitData()
|
||||
Mode.STORE_CREDENTIAL -> initEncryptData()
|
||||
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onGenericException(e)
|
||||
}
|
||||
invalidateBiometricMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun invalidateBiometricMenu() {
|
||||
// Show fingerprint key deletion
|
||||
if (!mAddBiometricMenuInProgress) {
|
||||
mAddBiometricMenuInProgress = true
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
||||
mAllowAdvancedUnlockMenu = containsCipher
|
||||
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
|
||||
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
|
||||
mAddBiometricMenuInProgress = false
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun connect(databaseUri: Uri) {
|
||||
showViews(true)
|
||||
this.databaseFileUri = databaseUri
|
||||
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
|
||||
override fun onCipherDatabaseCleared() {
|
||||
advancedUnlockManager?.closeBiometricPrompt()
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
}
|
||||
cipherDatabaseAction.apply {
|
||||
reloadPreferences()
|
||||
cipherDatabaseListener?.let {
|
||||
registerDatabaseListener(it)
|
||||
}
|
||||
}
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun disconnect(hideViews: Boolean = true,
|
||||
closePrompt: Boolean = true) {
|
||||
this.databaseFileUri = null
|
||||
// Close the biometric prompt
|
||||
allowOpenBiometricPrompt = false
|
||||
if (closePrompt)
|
||||
advancedUnlockManager?.closeBiometricPrompt()
|
||||
cipherDatabaseListener?.let {
|
||||
cipherDatabaseAction.unregisterDatabaseListener(it)
|
||||
}
|
||||
biometricMode = Mode.BIOMETRIC_UNAVAILABLE
|
||||
if (hideViews) {
|
||||
showViews(false)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun deleteEncryptedDatabaseKey() {
|
||||
mAllowAdvancedUnlockMenu = false
|
||||
advancedUnlockManager?.closeBiometricPrompt()
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
} ?: checkUnlockAvailability()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
||||
setAdvancedUnlockedMessageView(errString.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onAuthenticationFailed() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onAuthenticationSucceeded() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
when (biometricMode) {
|
||||
Mode.BIOMETRIC_UNAVAILABLE -> {
|
||||
}
|
||||
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
|
||||
}
|
||||
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> {
|
||||
}
|
||||
Mode.KEY_MANAGER_UNAVAILABLE -> {
|
||||
}
|
||||
Mode.WAIT_CREDENTIAL -> {
|
||||
}
|
||||
Mode.STORE_CREDENTIAL -> {
|
||||
// newly store the entered password in encrypted way
|
||||
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
|
||||
advancedUnlockManager?.encryptData(credential)
|
||||
}
|
||||
}
|
||||
Mode.EXTRACT_CREDENTIAL -> {
|
||||
// retrieve the encrypted value from preferences
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
||||
cipherDatabase?.encryptedValue?.let { value ->
|
||||
advancedUnlockManager?.decryptData(value)
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
} ?: run {
|
||||
onAuthenticationError(-1, getString(R.string.error_database_uri_null))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
mBuilderListener?.onCredentialEncrypted(
|
||||
CipherEncryptDatabase().apply {
|
||||
this.databaseUri = databaseUri
|
||||
this.credentialStorage = credentialDatabaseStorage
|
||||
this.encryptedValue = encryptedValue
|
||||
this.specParameters = ivSpec
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleDecryptedResult(decryptedValue: ByteArray) {
|
||||
// Load database directly with password retrieve
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
mBuilderListener?.onCredentialDecrypted(
|
||||
CipherDecryptDatabase().apply {
|
||||
this.databaseUri = databaseUri
|
||||
this.credentialStorage = credentialDatabaseStorage
|
||||
this.decryptedValue = decryptedValue
|
||||
}
|
||||
)
|
||||
cipherDatabaseAction.resetCipherParameters(databaseUri)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onGenericException(e: Exception) {
|
||||
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
|
||||
setAdvancedUnlockedMessageView(errorMessage)
|
||||
}
|
||||
|
||||
private fun showViews(show: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (show) {
|
||||
if (mAdvancedUnlockInfoView?.visibility != View.VISIBLE)
|
||||
mAdvancedUnlockInfoView?.showByFading()
|
||||
}
|
||||
else {
|
||||
if (mAdvancedUnlockInfoView?.visibility == View.VISIBLE)
|
||||
mAdvancedUnlockInfoView?.hideByFading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mAdvancedUnlockInfoView?.setTitle(textId)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun setAdvancedUnlockedMessageView(textId: Int) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mAdvancedUnlockInfoView?.setMessage(textId)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mAdvancedUnlockInfoView?.setMessage(text)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
BIOMETRIC_UNAVAILABLE,
|
||||
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
|
||||
KEY_MANAGER_UNAVAILABLE,
|
||||
WAIT_CREDENTIAL,
|
||||
STORE_CREDENTIAL,
|
||||
EXTRACT_CREDENTIAL
|
||||
}
|
||||
|
||||
interface BuilderListener {
|
||||
fun retrieveCredentialForEncryption(): ByteArray
|
||||
fun conditionToStoreCredential(): Boolean
|
||||
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
|
||||
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (!keepConnection) {
|
||||
// If close prompt, bug "user not authenticated in Android R"
|
||||
disconnect(false)
|
||||
advancedUnlockManager = null
|
||||
}
|
||||
}
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
mAdvancedUnlockInfoView = null
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
disconnect()
|
||||
advancedUnlockManager = null
|
||||
mBuilderListener = null
|
||||
}
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mBuilderListener = null
|
||||
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = AdvancedUnlockFragment::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.*
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.security.KeyStore
|
||||
import java.security.UnrecoverableKeyException
|
||||
import java.util.concurrent.Executors
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) {
|
||||
|
||||
private var keyStore: KeyStore? = null
|
||||
private var keyGenerator: KeyGenerator? = null
|
||||
private var cipher: Cipher? = null
|
||||
|
||||
private var biometricPrompt: BiometricPrompt? = null
|
||||
private var authenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
advancedUnlockCallback?.onAuthenticationFailed()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
advancedUnlockCallback?.onAuthenticationError(errorCode, errString)
|
||||
}
|
||||
}
|
||||
|
||||
var advancedUnlockCallback: AdvancedUnlockCallback? = null
|
||||
|
||||
private var isKeyManagerInit = false
|
||||
|
||||
private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext())
|
||||
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext())
|
||||
|
||||
val isKeyManagerInitialized: Boolean
|
||||
get() {
|
||||
if (!isKeyManagerInit) {
|
||||
advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized"))
|
||||
}
|
||||
return isKeyManagerInit
|
||||
}
|
||||
|
||||
private fun isBiometricOperation(): Boolean {
|
||||
return biometricUnlockEnable || isDeviceCredentialBiometricOperation()
|
||||
}
|
||||
|
||||
// Since Android 30, device credential is also a biometric operation
|
||||
private fun isDeviceCredentialOperation(): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
&& deviceCredentialUnlockEnable
|
||||
}
|
||||
|
||||
private fun isDeviceCredentialBiometricOperation(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& deviceCredentialUnlockEnable
|
||||
}
|
||||
|
||||
init {
|
||||
if (isDeviceSecure(retrieveContext())
|
||||
&& (biometricUnlockEnable || deviceCredentialUnlockEnable)) {
|
||||
try {
|
||||
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
|
||||
this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE)
|
||||
this.cipher = Cipher.getInstance(
|
||||
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
|
||||
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
|
||||
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
||||
isKeyManagerInit = (keyStore != null
|
||||
&& keyGenerator != null
|
||||
&& cipher != null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize the keystore", e)
|
||||
isKeyManagerInit = false
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
} else {
|
||||
// really not much to do when no fingerprint support found
|
||||
isKeyManagerInit = false
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized private fun getSecretKey(): SecretKey? {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
// Create new key if needed
|
||||
keyStore?.let { keyStore ->
|
||||
keyStore.load(null)
|
||||
|
||||
try {
|
||||
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
|
||||
// Set the alias of the entry in Android KeyStore where the key will appear
|
||||
// and the constrains (purposes) in the constructor of the Builder
|
||||
keyGenerator?.init(
|
||||
KeyGenParameterSpec.Builder(
|
||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
|
||||
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
||||
.apply {
|
||||
// Require the user to authenticate with a fingerprint to authorize every use
|
||||
// of the key, don't use it for device credential because it's the user authentication
|
||||
if (biometricUnlockEnable) {
|
||||
setUserAuthenticationRequired(true)
|
||||
}
|
||||
// To store in the security chip
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
&& retrieveContext().packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
|
||||
setIsStrongBoxBacked(true)
|
||||
}
|
||||
}
|
||||
.build())
|
||||
keyGenerator?.generateKey()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create a key in keystore", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
|
||||
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
||||
initEncryptData(actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
@Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
getSecretKey()?.let { secretKey ->
|
||||
cipher?.let { cipher ->
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
|
||||
actionIfCypherInit.invoke(
|
||||
AdvancedUnlockCryptoPrompt(
|
||||
cipher,
|
||||
R.string.advanced_unlock_prompt_store_credential_title,
|
||||
R.string.advanced_unlock_prompt_store_credential_message,
|
||||
isDeviceCredentialOperation(), isBiometricOperation())
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
||||
initEncryptData(actionIfCypherInit, false)
|
||||
} else {
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun encryptData(value: ByteArray) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
|
||||
// passes updated iv spec on to callback so this can be stored for decryption
|
||||
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
|
||||
advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to encrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun initDecryptData(ivSpecValue: ByteArray,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
@Synchronized private fun initDecryptData(ivSpecValue: ByteArray,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean = true) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// important to restore spec here that was used for decryption
|
||||
val spec = IvParameterSpec(ivSpecValue)
|
||||
getSecretKey()?.let { secretKey ->
|
||||
cipher?.let { cipher ->
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
|
||||
actionIfCypherInit.invoke(
|
||||
AdvancedUnlockCryptoPrompt(
|
||||
cipher,
|
||||
R.string.advanced_unlock_prompt_extract_credential_title,
|
||||
null,
|
||||
isDeviceCredentialOperation(), isBiometricOperation())
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteKeystoreKey()
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
||||
} else {
|
||||
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
||||
}
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
||||
} else {
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun decryptData(encryptedValue: ByteArray) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// actual decryption here
|
||||
cipher?.doFinal(encryptedValue)?.let { decrypted ->
|
||||
advancedUnlockCallback?.handleDecryptedResult(decrypted)
|
||||
}
|
||||
} catch (badPaddingException: BadPaddingException) {
|
||||
Log.e(TAG, "Unable to decrypt data", badPaddingException)
|
||||
advancedUnlockCallback?.onInvalidKeyException(badPaddingException)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to decrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun deleteKeystoreKey() {
|
||||
try {
|
||||
keyStore?.load(null)
|
||||
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
||||
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
// Init advanced unlock prompt
|
||||
if (biometricPrompt == null) {
|
||||
biometricPrompt = BiometricPrompt(retrieveContext(),
|
||||
Executors.newSingleThreadExecutor(),
|
||||
authenticationCallback)
|
||||
}
|
||||
|
||||
val promptTitle = retrieveContext().getString(cryptoPrompt.promptTitleId)
|
||||
val promptDescription = cryptoPrompt.promptDescriptionId?.let { descriptionId ->
|
||||
retrieveContext().getString(descriptionId)
|
||||
} ?: ""
|
||||
|
||||
if (cryptoPrompt.isBiometricOperation) {
|
||||
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(promptTitle)
|
||||
if (promptDescription.isNotEmpty())
|
||||
setDescription(promptDescription)
|
||||
setConfirmationRequired(false)
|
||||
if (isDeviceCredentialBiometricOperation()) {
|
||||
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
||||
} else {
|
||||
setNegativeButtonText(retrieveContext().getString(android.R.string.cancel))
|
||||
}
|
||||
}.build()
|
||||
biometricPrompt?.authenticate(
|
||||
promptInfoExtractCredential,
|
||||
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
|
||||
}
|
||||
else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
|
||||
@Suppress("DEPRECATION")
|
||||
deviceCredentialResultLauncher.launch(
|
||||
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun closeBiometricPrompt() {
|
||||
biometricPrompt?.cancelAuthentication()
|
||||
}
|
||||
|
||||
interface AdvancedUnlockErrorCallback {
|
||||
fun onUnrecoverableKeyException(e: Exception)
|
||||
fun onInvalidKeyException(e: Exception)
|
||||
fun onGenericException(e: Exception)
|
||||
}
|
||||
|
||||
interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback {
|
||||
fun onAuthenticationSucceeded()
|
||||
fun onAuthenticationFailed()
|
||||
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
|
||||
fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
|
||||
fun handleDecryptedResult(decryptedValue: ByteArray)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = AdvancedUnlockManager::class.java.name
|
||||
|
||||
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
|
||||
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
|
||||
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun canAuthenticate(context: Context): Int {
|
||||
return try {
|
||||
BiometricManager.from(context).canAuthenticate(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
|
||||
} else {
|
||||
BIOMETRIC_STRONG
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||
try {
|
||||
BiometricManager.from(context).canAuthenticate(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||
} else {
|
||||
BIOMETRIC_WEAK
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isDeviceSecure(context: Context): Boolean {
|
||||
return ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
||||
?.isDeviceSecure ?: false
|
||||
}
|
||||
|
||||
fun biometricUnlockSupported(context: Context): Boolean {
|
||||
val biometricCanAuthenticate = try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||
try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
}
|
||||
return (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
|
||||
)
|
||||
}
|
||||
|
||||
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
||||
(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 {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entry key in keystore
|
||||
*/
|
||||
fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
|
||||
advancedCallback: AdvancedUnlockErrorCallback) {
|
||||
AdvancedUnlockManager{ fragmentActivity }.apply {
|
||||
advancedUnlockCallback = object : AdvancedUnlockCallback {
|
||||
override fun onAuthenticationSucceeded() {}
|
||||
|
||||
override fun onAuthenticationFailed() {}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
|
||||
|
||||
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {}
|
||||
|
||||
override fun handleDecryptedResult(decryptedValue: ByteArray) {}
|
||||
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
advancedCallback.onUnrecoverableKeyException(e)
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
advancedCallback.onInvalidKeyException(e)
|
||||
}
|
||||
|
||||
override fun onGenericException(e: Exception) {
|
||||
advancedCallback.onGenericException(e)
|
||||
}
|
||||
}
|
||||
deleteKeystoreKey()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
deleteEntryKeyInKeystoreForBiometric(
|
||||
activity,
|
||||
object : AdvancedUnlockErrorCallback {
|
||||
fun showException(e: Exception) {
|
||||
Toast.makeText(activity,
|
||||
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onGenericException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import javax.crypto.Cipher
|
||||
|
||||
data class DeviceUnlockCryptoPrompt(
|
||||
var type: DeviceUnlockCryptoPromptType,
|
||||
var cipher: Cipher,
|
||||
@StringRes var titleId: Int,
|
||||
@StringRes var descriptionId: Int? = null,
|
||||
var isDeviceCredentialOperation: Boolean,
|
||||
var isBiometricOperation: Boolean
|
||||
) {
|
||||
fun isOldCredentialOperation(): Boolean {
|
||||
return !isBiometricOperation && isDeviceCredentialOperation
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeviceUnlockCryptoPromptType {
|
||||
CREDENTIAL_ENCRYPTION, CREDENTIAL_DECRYPTION
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.view.DeviceUnlockView
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.DeviceUnlockPromptMode
|
||||
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
class DeviceUnlockFragment: Fragment() {
|
||||
|
||||
private var mDeviceUnlockView: DeviceUnlockView? = null
|
||||
|
||||
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by activityViewModels()
|
||||
|
||||
private var mBiometricPrompt: BiometricPrompt? = null
|
||||
|
||||
// Only to fix multiple fingerprint menu #332
|
||||
private var mAllowDeviceUnlockMenu = false
|
||||
|
||||
private var mDeviceCredentialResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
mDeviceUnlockViewModel.onAuthenticationSucceeded()
|
||||
} else {
|
||||
setAuthenticationFailed()
|
||||
}
|
||||
mDeviceUnlockViewModel.biometricPromptClosed()
|
||||
}
|
||||
|
||||
private var biometricAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
mDeviceUnlockViewModel.onAuthenticationSucceeded(result)
|
||||
mDeviceUnlockViewModel.biometricPromptClosed()
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
setAuthenticationFailed()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
setAuthenticationError(errorCode, errString)
|
||||
}
|
||||
}
|
||||
|
||||
private val menuProvider: MenuProvider = object: MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
// biometric menu
|
||||
if (mAllowDeviceUnlockMenu)
|
||||
menuInflater.inflate(R.menu.device_unlock, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_keystore_remove_key ->
|
||||
deleteEncryptedDatabaseKey()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val rootView = inflater.inflate(R.layout.fragment_device_unlock, container, false)
|
||||
|
||||
mDeviceUnlockView = rootView.findViewById(R.id.device_unlock_view)
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Init device unlock prompt
|
||||
mBiometricPrompt = BiometricPrompt(
|
||||
this@DeviceUnlockFragment,
|
||||
Executors.newSingleThreadExecutor(),
|
||||
biometricAuthenticationCallback
|
||||
)
|
||||
|
||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDeviceUnlockViewModel.uiState.collect { uiState ->
|
||||
// Change mode
|
||||
toggleDeviceCredentialMode(uiState.newDeviceUnlockMode)
|
||||
// Prompt
|
||||
manageDeviceCredentialPrompt(uiState.cryptoPromptState)
|
||||
// Advanced menu
|
||||
mAllowDeviceUnlockMenu = uiState.allowDeviceUnlockMenu
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelBiometricPrompt() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mBiometricPrompt?.cancelAuthentication()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
when (deviceUnlockMode) {
|
||||
DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode()
|
||||
DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode()
|
||||
DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode()
|
||||
DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode()
|
||||
DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode()
|
||||
DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode()
|
||||
DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mDeviceUnlockViewModel.setException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun manageDeviceCredentialPrompt(
|
||||
state: DeviceUnlockPromptMode
|
||||
) {
|
||||
mDeviceUnlockViewModel.cryptoPrompt?.let { prompt ->
|
||||
when (state) {
|
||||
DeviceUnlockPromptMode.SHOW -> {
|
||||
openPrompt(prompt)
|
||||
mDeviceUnlockViewModel.promptShown()
|
||||
}
|
||||
DeviceUnlockPromptMode.CLOSE -> {
|
||||
cancelBiometricPrompt()
|
||||
mDeviceUnlockViewModel.biometricPromptClosed()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
val promptTitle = getString(cryptoPrompt.titleId)
|
||||
val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId ->
|
||||
getString(descriptionId)
|
||||
} ?: ""
|
||||
|
||||
if (cryptoPrompt.isBiometricOperation) {
|
||||
mBiometricPrompt?.authenticate(
|
||||
BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(promptTitle)
|
||||
if (promptDescription.isNotEmpty())
|
||||
setDescription(promptDescription)
|
||||
setConfirmationRequired(false)
|
||||
if (isDeviceCredentialBiometricOperation(context)) {
|
||||
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
||||
} else {
|
||||
setNegativeButtonText(getString(android.R.string.cancel))
|
||||
}
|
||||
}.build(),
|
||||
BiometricPrompt.CryptoObject(cryptoPrompt.cipher)
|
||||
)
|
||||
} else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||
context?.let { context ->
|
||||
@Suppress("DEPRECATION")
|
||||
mDeviceCredentialResultLauncher?.launch(
|
||||
ContextCompat.getSystemService(
|
||||
context,
|
||||
KeyguardManager::class.java
|
||||
)?.createConfirmDeviceCredentialIntent(
|
||||
promptTitle,
|
||||
promptDescription
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open prompt", e)
|
||||
mDeviceUnlockViewModel.setException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNotAvailableMode() {
|
||||
showViews(false)
|
||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener(null)
|
||||
}
|
||||
|
||||
private fun openBiometricSetting() {
|
||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
|
||||
try {
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL))
|
||||
}
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
|
||||
@Suppress("DEPRECATION") context
|
||||
?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL))
|
||||
}
|
||||
else -> {
|
||||
context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
||||
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSecurityUpdateRequiredMode() {
|
||||
showViews(true)
|
||||
setDeviceUnlockedTitleView(R.string.biometric_security_update_required)
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
private fun setNotConfiguredMode() {
|
||||
showViews(true)
|
||||
setDeviceUnlockedTitleView(R.string.configure_biometric)
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
private fun setKeyManagerNotAvailableMode() {
|
||||
showViews(true)
|
||||
setDeviceUnlockedTitleView(R.string.keystore_not_accessible)
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
private fun setWaitCredentialMode() {
|
||||
showViews(true)
|
||||
setDeviceUnlockedTitleView(R.string.unavailable)
|
||||
context?.let { context ->
|
||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
|
||||
mDeviceUnlockViewModel.setException(SecurityException(
|
||||
context.getString(R.string.credential_before_click_device_unlock_button)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setStoreCredentialMode() {
|
||||
showViews(true)
|
||||
setDeviceUnlockedTitleView(R.string.unlock_and_link_biometric)
|
||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ ->
|
||||
mDeviceUnlockViewModel.showPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setExtractCredentialMode() {
|
||||
showViews(true)
|
||||
setDeviceUnlockedTitleView(R.string.unlock)
|
||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ ->
|
||||
mDeviceUnlockViewModel.showPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteEncryptedDatabaseKey() {
|
||||
mDeviceUnlockViewModel.deleteEncryptedDatabaseKey()
|
||||
}
|
||||
|
||||
private fun showViews(show: Boolean) {
|
||||
if (show) {
|
||||
if (mDeviceUnlockView?.visibility != View.VISIBLE)
|
||||
mDeviceUnlockView?.showByFading()
|
||||
}
|
||||
else {
|
||||
if (mDeviceUnlockView?.visibility == View.VISIBLE)
|
||||
mDeviceUnlockView?.hideByFading()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDeviceUnlockedTitleView(textId: Int) {
|
||||
mDeviceUnlockView?.setTitle(textId)
|
||||
}
|
||||
|
||||
private fun setAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
mDeviceUnlockViewModel.biometricPromptClosed()
|
||||
when (errorCode) {
|
||||
BiometricPrompt.ERROR_CANCELED,
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||
BiometricPrompt.ERROR_USER_CANCELED -> {
|
||||
// No operation
|
||||
Log.i(TAG, "$errString")
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
||||
mDeviceUnlockViewModel.setException(SecurityException(errString.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAuthenticationFailed() {
|
||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||
mDeviceUnlockViewModel.setException(
|
||||
SecurityException(getString(R.string.device_unlock_not_recognized))
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
cancelBiometricPrompt()
|
||||
mDeviceUnlockViewModel.clear()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
mDeviceUnlockView = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DeviceUnlockFragment::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.security.KeyStore
|
||||
import java.security.UnrecoverableKeyException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class DeviceUnlockManager(private var appContext: Context) {
|
||||
|
||||
private var keyStore: KeyStore? = null
|
||||
private var keyGenerator: KeyGenerator? = null
|
||||
private var cipher: Cipher? = null
|
||||
|
||||
private var biometricUnlockEnable = isBiometricUnlockEnable(appContext)
|
||||
private var deviceCredentialUnlockEnable = isDeviceCredentialUnlockEnable(appContext)
|
||||
|
||||
init {
|
||||
if (biometricUnlockEnable || deviceCredentialUnlockEnable) {
|
||||
if (isDeviceSecure(appContext)) {
|
||||
try {
|
||||
this.keyStore = KeyStore.getInstance(DEVICE_UNLOCK_KEYSTORE)
|
||||
this.keyGenerator = KeyGenerator.getInstance(
|
||||
DEVICE_UNLOCK_KEY_ALGORITHM,
|
||||
DEVICE_UNLOCK_KEYSTORE
|
||||
)
|
||||
this.cipher = Cipher.getInstance(
|
||||
DEVICE_UNLOCK_KEY_ALGORITHM + "/"
|
||||
+ DEVICE_UNLOCK_BLOCKS_MODES + "/"
|
||||
+ DEVICE_UNLOCK_ENCRYPTION_PADDING
|
||||
)
|
||||
if (keyStore == null) {
|
||||
throw SecurityException("Unable to initialize the keystore")
|
||||
}
|
||||
if (keyGenerator == null) {
|
||||
throw SecurityException("Unable to initialize the key generator")
|
||||
}
|
||||
if (cipher == null) {
|
||||
throw SecurityException("Unable to initialize the cipher")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize the device unlock manager", e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
throw SecurityException("Device not secure enough")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized private fun getSecretKey(): SecretKey? {
|
||||
try {
|
||||
// Create new key if needed
|
||||
keyStore?.let { keyStore ->
|
||||
keyStore.load(null)
|
||||
try {
|
||||
if (!keyStore.containsAlias(DEVICE_UNLOCK_KEYSTORE_KEY)) {
|
||||
// Set the alias of the entry in Android KeyStore where the key will appear
|
||||
// and the constrains (purposes) in the constructor of the Builder
|
||||
keyGenerator?.init(
|
||||
KeyGenParameterSpec.Builder(
|
||||
DEVICE_UNLOCK_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(DEVICE_UNLOCK_BLOCKS_MODES)
|
||||
.setEncryptionPaddings(DEVICE_UNLOCK_ENCRYPTION_PADDING)
|
||||
.apply {
|
||||
// Require the user to authenticate with a fingerprint to authorize every use
|
||||
// of the key, don't use it for device credential because it's the user authentication
|
||||
if (biometricUnlockEnable) {
|
||||
setUserAuthenticationRequired(true)
|
||||
}
|
||||
// To store in the security chip
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
&& appContext.packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
|
||||
setIsStrongBoxBacked(true)
|
||||
}
|
||||
}
|
||||
.build())
|
||||
keyGenerator?.generateKey()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create a key in keystore", e)
|
||||
throw e
|
||||
}
|
||||
return keyStore.getKey(DEVICE_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
||||
throw e
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Synchronized fun initEncryptData(
|
||||
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||
) {
|
||||
initEncryptData(true, actionIfCypherInit)
|
||||
}
|
||||
|
||||
@Synchronized private fun initEncryptData(
|
||||
firstLaunch: Boolean,
|
||||
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||
) {
|
||||
try {
|
||||
getSecretKey()?.let { secretKey ->
|
||||
cipher?.let { cipher ->
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
actionIfCypherInit.invoke(
|
||||
DeviceUnlockCryptoPrompt(
|
||||
type = DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION,
|
||||
cipher = cipher,
|
||||
titleId = R.string.device_unlock_prompt_store_credential_title,
|
||||
descriptionId = R.string.device_unlock_prompt_store_credential_message,
|
||||
isDeviceCredentialOperation = isDeviceCredentialOperation(
|
||||
deviceCredentialUnlockEnable
|
||||
),
|
||||
isBiometricOperation = isBiometricOperation(
|
||||
biometricUnlockEnable, deviceCredentialUnlockEnable
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||
throw unrecoverableKeyException
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(appContext)
|
||||
initEncryptData(false, actionIfCypherInit)
|
||||
} else {
|
||||
throw invalidKeyException
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun encryptData(
|
||||
value: ByteArray,
|
||||
cipher: Cipher?,
|
||||
handleEncryptedResult: (encryptedValue: ByteArray, ivSpec: ByteArray) -> Unit
|
||||
) {
|
||||
try {
|
||||
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
|
||||
// passes updated iv spec on to callback so this can be stored for decryption
|
||||
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
|
||||
handleEncryptedResult.invoke(encrypted, spec.iv)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to encrypt data", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun initDecryptData(
|
||||
ivSpecValue: ByteArray,
|
||||
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||
) {
|
||||
initDecryptData(ivSpecValue, true, actionIfCypherInit)
|
||||
}
|
||||
|
||||
@Synchronized private fun initDecryptData(
|
||||
ivSpecValue: ByteArray,
|
||||
firstLaunch: Boolean = true,
|
||||
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||
) {
|
||||
try {
|
||||
// important to restore spec here that was used for decryption
|
||||
val spec = IvParameterSpec(ivSpecValue)
|
||||
getSecretKey()?.let { secretKey ->
|
||||
cipher?.let { cipher ->
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
actionIfCypherInit.invoke(
|
||||
DeviceUnlockCryptoPrompt(
|
||||
type = DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION,
|
||||
cipher = cipher,
|
||||
titleId = R.string.device_unlock_prompt_extract_credential_title,
|
||||
descriptionId = null,
|
||||
isDeviceCredentialOperation = isDeviceCredentialOperation(
|
||||
deviceCredentialUnlockEnable
|
||||
),
|
||||
isBiometricOperation = isBiometricOperation(
|
||||
biometricUnlockEnable, deviceCredentialUnlockEnable
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteKeystoreKey()
|
||||
initDecryptData(ivSpecValue, false, actionIfCypherInit)
|
||||
} else {
|
||||
throw unrecoverableKeyException
|
||||
}
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(appContext)
|
||||
initDecryptData(ivSpecValue, false, actionIfCypherInit)
|
||||
} else {
|
||||
throw invalidKeyException
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun decryptData(
|
||||
encryptedValue: ByteArray,
|
||||
cipher: Cipher?,
|
||||
handleDecryptedResult: (decryptedValue: ByteArray) -> Unit
|
||||
) {
|
||||
try {
|
||||
// actual decryption here
|
||||
cipher?.doFinal(encryptedValue)?.let { decrypted ->
|
||||
handleDecryptedResult.invoke(decrypted)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to decrypt data", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun deleteKeystoreKey() {
|
||||
try {
|
||||
keyStore?.load(null)
|
||||
keyStore?.deleteEntry(DEVICE_UNLOCK_KEYSTORE_KEY)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = DeviceUnlockManager::class.java.name
|
||||
|
||||
private const val DEVICE_UNLOCK_KEYSTORE = "AndroidKeyStore"
|
||||
private const val DEVICE_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
|
||||
private const val DEVICE_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||
private const val DEVICE_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||
private const val DEVICE_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun canAuthenticate(context: Context): Int {
|
||||
return try {
|
||||
BiometricManager.from(context).canAuthenticate(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
|
||||
} else {
|
||||
BIOMETRIC_STRONG
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||
try {
|
||||
BiometricManager.from(context).canAuthenticate(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||
} else {
|
||||
BIOMETRIC_WEAK
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isDeviceSecure(context: Context): Boolean {
|
||||
return ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
||||
?.isDeviceSecure ?: false
|
||||
}
|
||||
|
||||
fun biometricUnlockSupported(context: Context): Boolean {
|
||||
val biometricCanAuthenticate = try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||
try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
}
|
||||
return (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
|
||||
)
|
||||
}
|
||||
|
||||
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
||||
(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 {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entry key in keystore
|
||||
*/
|
||||
fun deleteEntryKeyInKeystoreForBiometric(
|
||||
appContext: Context
|
||||
) {
|
||||
DeviceUnlockManager(appContext).apply {
|
||||
deleteKeystoreKey()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllEntryKeysInKeystoreForBiometric(appContext: Context) {
|
||||
try {
|
||||
deleteEntryKeyInKeystoreForBiometric(appContext)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(appContext,
|
||||
deviceUnlockError(e, appContext),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
} finally {
|
||||
CipherDatabaseAction.getInstance(appContext).deleteAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deviceUnlockError(error: Throwable, context: Context): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& (error is UnrecoverableKeyException
|
||||
|| error is KeyPermanentlyInvalidatedException)) {
|
||||
context.getString(R.string.device_unlock_invalid_key)
|
||||
} else
|
||||
error.cause?.localizedMessage
|
||||
?: error.localizedMessage
|
||||
?: error.toString()
|
||||
}
|
||||
|
||||
fun isBiometricUnlockEnable(appContext: Context) =
|
||||
PreferencesUtil.isBiometricUnlockEnable(appContext)
|
||||
|
||||
fun isDeviceCredentialUnlockEnable(appContext: Context) =
|
||||
PreferencesUtil.isDeviceCredentialUnlockEnable(appContext)
|
||||
|
||||
private fun isBiometricOperation(
|
||||
biometricUnlockEnable: Boolean,
|
||||
deviceCredentialUnlockEnable: Boolean
|
||||
): Boolean {
|
||||
return biometricUnlockEnable
|
||||
|| isDeviceCredentialBiometricOperation(deviceCredentialUnlockEnable)
|
||||
}
|
||||
|
||||
// Since Android 30, device credential is also a biometric operation
|
||||
private fun isDeviceCredentialOperation(
|
||||
deviceCredentialUnlockEnable: Boolean
|
||||
): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
&& deviceCredentialUnlockEnable
|
||||
}
|
||||
|
||||
private fun isDeviceCredentialBiometricOperation(
|
||||
deviceCredentialUnlockEnable: Boolean
|
||||
): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& deviceCredentialUnlockEnable
|
||||
}
|
||||
|
||||
fun isDeviceCredentialBiometricOperation(context: Context?): Boolean {
|
||||
if (context == null) {
|
||||
return false
|
||||
}
|
||||
return isDeviceCredentialBiometricOperation(
|
||||
isDeviceCredentialUnlockEnable(context)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
enum class DeviceUnlockMode {
|
||||
BIOMETRIC_UNAVAILABLE,
|
||||
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
|
||||
KEY_MANAGER_UNAVAILABLE,
|
||||
WAIT_CREDENTIAL,
|
||||
STORE_CREDENTIAL,
|
||||
EXTRACT_CREDENTIAL
|
||||
}
|
||||
@@ -254,6 +254,7 @@ class DatabaseTaskProvider(
|
||||
}
|
||||
|
||||
private fun initServiceConnection() {
|
||||
stopDialog()
|
||||
if (serviceConnection == null) {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
|
||||
@@ -59,8 +59,9 @@ object SearchHelper {
|
||||
&& !searchInfo.containsOnlyNullValues()) {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
searchInfo.toString(),
|
||||
MAX_SEARCH_ENTRY
|
||||
searchInfoString = searchInfo.toString(),
|
||||
searchInfoByDomain = searchInfo.isASearchByDomain(),
|
||||
max = MAX_SEARCH_ENTRY
|
||||
)?.let { searchGroup ->
|
||||
if (searchGroup.numberOfChildEntries > 0) {
|
||||
searchWithoutUI = true
|
||||
|
||||
@@ -89,8 +89,8 @@ class PasswordActivityEducation(activity: Activity)
|
||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||
return checkAndPerformedEducation(isEducationBiometricPerformed(activity),
|
||||
TapTarget.forView(educationView,
|
||||
activity.getString(R.string.education_advanced_unlock_title),
|
||||
activity.getString(R.string.education_advanced_unlock_summary))
|
||||
activity.getString(R.string.education_device_unlock_title),
|
||||
activity.getString(R.string.education_device_unlock_summary))
|
||||
.outerCircleColorInt(getCircleColor())
|
||||
.outerCircleAlpha(getCircleAlpha())
|
||||
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_fingerprint_24dp))
|
||||
|
||||
@@ -15,13 +15,13 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
class AdvancedUnlockNotificationService : NotificationService() {
|
||||
class DeviceUnlockNotificationService : NotificationService() {
|
||||
|
||||
private lateinit var mTempCipherDao: ArrayList<CipherDatabaseEntity>
|
||||
|
||||
private var mActionTaskBinder = AdvancedUnlockBinder()
|
||||
private var mActionTaskBinder = DeviceUnlockBinder()
|
||||
|
||||
inner class AdvancedUnlockBinder: Binder() {
|
||||
inner class DeviceUnlockBinder: Binder() {
|
||||
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
|
||||
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
|
||||
}
|
||||
@@ -48,11 +48,11 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
override val notificationId: Int = 593
|
||||
|
||||
override fun retrieveChannelId(): String {
|
||||
return CHANNEL_ADVANCED_UNLOCK_ID
|
||||
return CHANNEL_DEVICE_UNLOCK_ID
|
||||
}
|
||||
|
||||
override fun retrieveChannelName(): String {
|
||||
return getString(R.string.advanced_unlock)
|
||||
return getString(R.string.device_unlock)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -60,7 +60,7 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
mTempCipherDao = ArrayList()
|
||||
}
|
||||
|
||||
// It's simpler to use pendingIntent to perform REMOVE_ADVANCED_UNLOCK_KEY_ACTION
|
||||
// It's simpler to use pendingIntent to perform REMOVE_DEVICE_UNLOCK_KEY_ACTION
|
||||
// because can be directly broadcast to another module or app
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
@@ -68,7 +68,7 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
|
||||
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
|
||||
4577,
|
||||
Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION),
|
||||
Intent(REMOVE_DEVICE_UNLOCK_KEY_ACTION),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
@@ -81,28 +81,28 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
} else {
|
||||
R.drawable.notification_ic_device_unlock_24dp
|
||||
})
|
||||
setContentTitle(getString(R.string.advanced_unlock))
|
||||
setContentText(getString(R.string.advanced_unlock_tap_delete))
|
||||
setContentTitle(getString(R.string.device_unlock))
|
||||
setContentText(getString(R.string.device_unlock_tap_delete))
|
||||
setContentIntent(pendingDeleteIntent)
|
||||
// Unfortunately swipe is disabled in lollipop+
|
||||
setDeleteIntent(pendingDeleteIntent)
|
||||
}
|
||||
|
||||
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
|
||||
val notificationTimeoutMilliSecs = PreferencesUtil.getDeviceUnlockTimeout(this)
|
||||
// Not necessarily a foreground service
|
||||
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
||||
defineTimerJob(
|
||||
notificationBuilder,
|
||||
NotificationServiceType.ADVANCED_UNLOCK,
|
||||
NotificationServiceType.DEVICE_UNLOCK,
|
||||
notificationTimeoutMilliSecs
|
||||
) {
|
||||
sendBroadcast(Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION))
|
||||
sendBroadcast(Intent(REMOVE_DEVICE_UNLOCK_KEY_ACTION))
|
||||
}
|
||||
} else {
|
||||
startForegroundCompat(
|
||||
notificationId,
|
||||
notificationBuilder,
|
||||
NotificationServiceType.ADVANCED_UNLOCK
|
||||
NotificationServiceType.DEVICE_UNLOCK
|
||||
)
|
||||
}
|
||||
|
||||
@@ -119,11 +119,11 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
class AdvancedUnlockReceiver(var removeKeyAction: () -> Unit): BroadcastReceiver() {
|
||||
class DeviceUnlockReceiver(var removeKeyAction: () -> Unit): BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
intent.action?.let {
|
||||
when (it) {
|
||||
REMOVE_ADVANCED_UNLOCK_KEY_ACTION -> {
|
||||
REMOVE_DEVICE_UNLOCK_KEY_ACTION -> {
|
||||
removeKeyAction.invoke()
|
||||
}
|
||||
}
|
||||
@@ -132,13 +132,13 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ADVANCED_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
|
||||
const val REMOVE_ADVANCED_UNLOCK_KEY_ACTION = "com.kunzisoft.keepass.REMOVE_ADVANCED_UNLOCK_KEY"
|
||||
private const val CHANNEL_DEVICE_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
|
||||
const val REMOVE_DEVICE_UNLOCK_KEY_ACTION = "com.kunzisoft.keepass.REMOVE_DEVICE_UNLOCK_KEY"
|
||||
|
||||
// Only one service connection
|
||||
fun bindService(context: Context, serviceConnection: ServiceConnection, flags: Int) {
|
||||
context.bindService(Intent(context,
|
||||
AdvancedUnlockNotificationService::class.java),
|
||||
DeviceUnlockNotificationService::class.java),
|
||||
serviceConnection,
|
||||
flags)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
@@ -105,7 +106,7 @@ abstract class NotificationService : Service() {
|
||||
NotificationServiceType.ATTACHMENT -> FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
NotificationServiceType.CLIPBOARD -> foregroundServiceTimer
|
||||
NotificationServiceType.KEYBOARD -> foregroundServiceTimer
|
||||
NotificationServiceType.ADVANCED_UNLOCK -> foregroundServiceTimer
|
||||
NotificationServiceType.DEVICE_UNLOCK -> foregroundServiceTimer
|
||||
}
|
||||
startForeground(notificationId, builder.build(), foregroundType)
|
||||
} else {
|
||||
@@ -156,11 +157,21 @@ abstract class NotificationService : Service() {
|
||||
mReset = true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
override fun onTimeout(startId: Int, fgsType: Int) {
|
||||
super.onTimeout(startId, fgsType)
|
||||
Log.e(javaClass::class.simpleName, "The service took too long to execute")
|
||||
cancelNotification()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
protected fun cancelNotification() {
|
||||
mTimerJob?.cancel()
|
||||
mTimerJob = null
|
||||
notificationManager?.cancel(notificationId)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cancelNotification()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ enum class NotificationServiceType {
|
||||
ATTACHMENT,
|
||||
CLIPBOARD,
|
||||
KEYBOARD,
|
||||
ADVANCED_UNLOCK
|
||||
DEVICE_UNLOCK
|
||||
}
|
||||
@@ -23,15 +23,15 @@ import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
|
||||
class AdvancedUnlockSettingsActivity : SettingsActivity() {
|
||||
class DeviceUnlockSettingsActivity : SettingsActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
mTimeoutEnable = false
|
||||
setTitle(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
|
||||
setTitle(NestedSettingsFragment.Screen.DEVICE_UNLOCK)
|
||||
}
|
||||
|
||||
override fun retrieveMainFragment(): Fragment {
|
||||
return NestedSettingsFragment.newInstance(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
|
||||
return NestedSettingsFragment.newInstance(NestedSettingsFragment.Screen.DEVICE_UNLOCK)
|
||||
}
|
||||
}
|
||||
@@ -84,9 +84,9 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.settings_advanced_unlock_key))?.apply {
|
||||
findPreference<Preference>(getString(R.string.settings_device_unlock_key))?.apply {
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
|
||||
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DEVICE_UNLOCK)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||
@@ -66,8 +66,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
Screen.FORM_FILLING -> {
|
||||
onCreateFormFillingPreference(rootKey)
|
||||
}
|
||||
Screen.ADVANCED_UNLOCK -> {
|
||||
onCreateAdvancedUnlockPreferences(rootKey)
|
||||
Screen.DEVICE_UNLOCK -> {
|
||||
onCreateDeviceUnlockPreferences(rootKey)
|
||||
}
|
||||
Screen.APPEARANCE -> {
|
||||
onCreateAppearancePreferences(rootKey)
|
||||
@@ -240,18 +240,18 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCreateAdvancedUnlockPreferences(rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences_advanced_unlock, rootKey)
|
||||
private fun onCreateDeviceUnlockPreferences(rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences_device_unlock, rootKey)
|
||||
|
||||
activity?.let { activity ->
|
||||
|
||||
val biometricUnlockEnablePreference: TwoStatePreference? = findPreference(getString(R.string.biometric_unlock_enable_key))
|
||||
val deviceCredentialUnlockEnablePreference: TwoStatePreference? = findPreference(getString(R.string.device_credential_unlock_enable_key))
|
||||
val autoOpenPromptPreference: TwoStatePreference? = findPreference(getString(R.string.biometric_auto_open_prompt_key))
|
||||
val tempAdvancedUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
|
||||
val tempDeviceUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_device_unlock_enable_key))
|
||||
|
||||
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.biometricUnlockSupported(activity)
|
||||
DeviceUnlockManager.biometricUnlockSupported(activity)
|
||||
} else false
|
||||
biometricUnlockEnablePreference?.apply {
|
||||
// False if under Marshmallow
|
||||
@@ -272,7 +272,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
||||
biometricUnlockEnablePreference.isChecked = false
|
||||
autoOpenPromptPreference?.isEnabled = deviceCredentialChecked
|
||||
tempAdvancedUnlockPreference?.isEnabled = deviceCredentialChecked
|
||||
tempDeviceUnlockPreference?.isEnabled = deviceCredentialChecked
|
||||
}
|
||||
} else {
|
||||
if (deviceCredentialChecked) {
|
||||
@@ -286,7 +286,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
|
||||
biometricUnlockEnablePreference.isChecked = true
|
||||
autoOpenPromptPreference?.isEnabled = true
|
||||
tempAdvancedUnlockPreference?.isEnabled = true
|
||||
tempDeviceUnlockPreference?.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +296,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
|
||||
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.deviceCredentialUnlockSupported(activity)
|
||||
DeviceUnlockManager.deviceCredentialUnlockSupported(activity)
|
||||
} else false
|
||||
deviceCredentialUnlockEnablePreference?.apply {
|
||||
// Biometric unlock already checked
|
||||
@@ -319,7 +319,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
||||
deviceCredentialUnlockEnablePreference.isChecked = false
|
||||
autoOpenPromptPreference?.isEnabled = biometricChecked
|
||||
tempAdvancedUnlockPreference?.isEnabled = biometricChecked
|
||||
tempDeviceUnlockPreference?.isEnabled = biometricChecked
|
||||
}
|
||||
} else {
|
||||
if (biometricChecked) {
|
||||
@@ -333,7 +333,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
|
||||
deviceCredentialUnlockEnablePreference.isChecked = true
|
||||
autoOpenPromptPreference?.isEnabled = true
|
||||
tempAdvancedUnlockPreference?.isEnabled = true
|
||||
tempDeviceUnlockPreference?.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,13 +344,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
|
||||
autoOpenPromptPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|
||||
|| deviceCredentialUnlockEnablePreference?.isChecked == true
|
||||
tempAdvancedUnlockPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|
||||
tempDeviceUnlockPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|
||||
|| deviceCredentialUnlockEnablePreference?.isChecked == true
|
||||
|
||||
tempAdvancedUnlockPreference?.setOnPreferenceClickListener {
|
||||
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
|
||||
tempDeviceUnlockPreference?.setOnPreferenceClickListener {
|
||||
tempDeviceUnlockPreference.isChecked = !tempDeviceUnlockPreference.isChecked
|
||||
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
||||
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
|
||||
tempDeviceUnlockPreference.isChecked = !tempDeviceUnlockPreference.isChecked
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -366,8 +366,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.advanced_unlock_explanation_key))?.setOnPreferenceClickListener {
|
||||
context?.openUrl(R.string.advanced_unlock_explanation_url)
|
||||
findPreference<Preference>(getString(R.string.device_unlock_explanation_key))?.setOnPreferenceClickListener {
|
||||
context?.openUrl(R.string.device_unlock_explanation_url)
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -378,14 +378,14 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
validate: (()->Unit)? = null) {
|
||||
var message = ""
|
||||
if (keystoreWarning) {
|
||||
message += resources.getString(R.string.advanced_unlock_prompt_store_credential_message)
|
||||
message += "\n\n" + resources.getString(R.string.advanced_unlock_keystore_warning)
|
||||
message += resources.getString(R.string.device_unlock_prompt_store_credential_message)
|
||||
message += "\n\n" + resources.getString(R.string.device_unlock_keystore_warning)
|
||||
}
|
||||
if (keystoreWarning && deleteKeys) {
|
||||
message += "\n\n"
|
||||
}
|
||||
if (deleteKeys) {
|
||||
message += resources.getString(R.string.advanced_unlock_delete_all_key_warning)
|
||||
message += resources.getString(R.string.device_unlock_delete_all_key_warning)
|
||||
}
|
||||
warningAlertDialog = AlertDialog.Builder(activity)
|
||||
.setMessage(message)
|
||||
@@ -395,7 +395,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
validate?.invoke()
|
||||
warningAlertDialog?.setOnDismissListener(null)
|
||||
if (deleteKeys && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
||||
DeviceUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(resources.getString(android.R.string.cancel)
|
||||
@@ -509,7 +509,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
when (preference.key) {
|
||||
getString(R.string.app_timeout_key),
|
||||
getString(R.string.clipboard_timeout_key),
|
||||
getString(R.string.temp_advanced_unlock_timeout_key) -> {
|
||||
getString(R.string.temp_device_unlock_timeout_key) -> {
|
||||
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
else -> otherDialogFragment = true
|
||||
|
||||
@@ -30,7 +30,7 @@ import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFra
|
||||
abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
enum class Screen {
|
||||
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
|
||||
APPLICATION, FORM_FILLING, DEVICE_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
|
||||
}
|
||||
|
||||
fun getScreen(): Screen {
|
||||
@@ -66,7 +66,7 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
|
||||
val fragment: NestedSettingsFragment = when (key) {
|
||||
Screen.APPLICATION,
|
||||
Screen.FORM_FILLING,
|
||||
Screen.ADVANCED_UNLOCK,
|
||||
Screen.DEVICE_UNLOCK,
|
||||
Screen.APPEARANCE -> NestedAppSettingsFragment()
|
||||
Screen.DATABASE,
|
||||
Screen.DATABASE_SECURITY,
|
||||
@@ -83,7 +83,7 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
|
||||
return when (key) {
|
||||
Screen.APPLICATION -> resources.getString(R.string.menu_app_settings)
|
||||
Screen.FORM_FILLING -> resources.getString(R.string.menu_form_filling_settings)
|
||||
Screen.ADVANCED_UNLOCK -> resources.getString(R.string.menu_advanced_unlock_settings)
|
||||
Screen.DEVICE_UNLOCK -> resources.getString(R.string.menu_device_unlock_settings)
|
||||
Screen.APPEARANCE -> resources.getString(R.string.menu_appearance_settings)
|
||||
Screen.DATABASE -> resources.getString(R.string.menu_database_settings)
|
||||
Screen.DATABASE_SECURITY -> resources.getString(R.string.menu_security_settings)
|
||||
|
||||
@@ -29,7 +29,7 @@ import androidx.preference.PreferenceManager
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
@@ -460,10 +460,10 @@ object PreferencesUtil {
|
||||
?: TimeoutHelper.DEFAULT_TIMEOUT
|
||||
}
|
||||
|
||||
fun getAdvancedUnlockTimeout(context: Context): Long {
|
||||
fun getDeviceUnlockTimeout(context: Context): Long {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getString(context.getString(R.string.temp_advanced_unlock_timeout_key),
|
||||
context.getString(R.string.temp_advanced_unlock_timeout_default))?.toLong()
|
||||
return prefs.getString(context.getString(R.string.temp_device_unlock_timeout_key),
|
||||
context.getString(R.string.temp_device_unlock_timeout_default))?.toLong()
|
||||
?: TimeoutHelper.DEFAULT_TIMEOUT
|
||||
}
|
||||
|
||||
@@ -503,7 +503,7 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.enable_screenshot_mode_key_default))
|
||||
}
|
||||
|
||||
fun isAdvancedUnlockEnable(context: Context): Boolean {
|
||||
fun isDeviceUnlockEnable(context: Context): Boolean {
|
||||
return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context)
|
||||
}
|
||||
|
||||
@@ -512,7 +512,7 @@ object PreferencesUtil {
|
||||
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
||||
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
|
||||
&& (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.biometricUnlockSupported(context)
|
||||
DeviceUnlockManager.biometricUnlockSupported(context)
|
||||
} else {
|
||||
false
|
||||
})
|
||||
@@ -526,13 +526,13 @@ object PreferencesUtil {
|
||||
&& !isBiometricUnlockEnable(context)
|
||||
}
|
||||
|
||||
fun isTempAdvancedUnlockEnable(context: Context): Boolean {
|
||||
fun isTempDeviceUnlockEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.temp_advanced_unlock_enable_key),
|
||||
context.resources.getBoolean(R.bool.temp_advanced_unlock_enable_default))
|
||||
return prefs.getBoolean(context.getString(R.string.temp_device_unlock_enable_key),
|
||||
context.resources.getBoolean(R.bool.temp_device_unlock_enable_default))
|
||||
}
|
||||
|
||||
fun isAdvancedUnlockPromptAutoOpenEnable(context: Context): Boolean {
|
||||
fun isDeviceUnlockPromptAutoOpenEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.biometric_auto_open_prompt_key),
|
||||
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
|
||||
@@ -618,12 +618,6 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.allow_no_password_default))
|
||||
}
|
||||
|
||||
fun enableReadOnlyDatabase(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.enable_read_only_key),
|
||||
context.resources.getBoolean(R.bool.enable_read_only_default))
|
||||
}
|
||||
|
||||
fun deletePasswordAfterConnexionAttempt(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.delete_entered_password_key),
|
||||
@@ -804,7 +798,6 @@ object PreferencesUtil {
|
||||
when (name) {
|
||||
context.getString(R.string.allow_no_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.delete_entered_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.enable_read_only_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.enable_auto_save_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.enable_keep_screen_on_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.auto_focus_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
@@ -821,8 +814,8 @@ object PreferencesUtil {
|
||||
context.getString(R.string.biometric_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.device_credential_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.biometric_auto_open_prompt_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.temp_advanced_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.temp_advanced_unlock_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||
context.getString(R.string.temp_device_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.temp_device_unlock_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||
|
||||
context.getString(R.string.magic_keyboard_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
context.getString(R.string.clipboard_notifications_key) -> editor.putBoolean(name, value.toBoolean())
|
||||
|
||||
@@ -70,8 +70,12 @@ open class SettingsActivity
|
||||
// To apply navigation bar with background color
|
||||
/* TODO Settings nav bar
|
||||
setTransparentNavigationBar {
|
||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
||||
coordinatorLayout?.applyWindowInsets(EnumSet.of(
|
||||
WindowInsetPosition.TOP_MARGINS,
|
||||
WindowInsetPosition.BOTTOM_MARGINS,
|
||||
WindowInsetPosition.START_MARGINS,
|
||||
WindowInsetPosition.END_MARGINS,
|
||||
))
|
||||
}*/
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
|
||||
@@ -65,27 +65,19 @@ object TimeoutHelper {
|
||||
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||
val triggerTime = System.currentTimeMillis() + timeout
|
||||
Log.d(TAG, "TimeoutHelper start")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.AppLifecycleObserver
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||
@@ -76,27 +77,19 @@ class LockReceiver(private var lockAction: () -> Unit) : BroadcastReceiver() {
|
||||
// Launch the effective action after a small time
|
||||
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
|
||||
(context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
lockPendingIntent
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
lockPendingIntent
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
lockPendingIntent
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
lockPendingIntent
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -105,6 +98,7 @@ class LockReceiver(private var lockAction: () -> Unit) : BroadcastReceiver() {
|
||||
}
|
||||
LOCK_ACTION -> {
|
||||
lockAction.invoke()
|
||||
AppLifecycleObserver.lockBackgroundEvent = false
|
||||
if (PreferencesUtil.isKeyboardPreviousLockEnable(context)) {
|
||||
backToPreviousKeyboardAction?.invoke()
|
||||
} else {}
|
||||
|
||||
@@ -67,55 +67,53 @@ object UriUtil {
|
||||
readOnly: Boolean) {
|
||||
try {
|
||||
// try to persist read and write permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
contentResolver?.apply {
|
||||
var readPermissionAllowed = false
|
||||
var writePermissionAllowed = false
|
||||
// Check current permissions allowed
|
||||
persistedUriPermissions.find { uriPermission ->
|
||||
uriPermission.uri == uri
|
||||
}?.let { uriPermission ->
|
||||
Log.d(TAG, "Check URI permission : $uriPermission")
|
||||
if (uriPermission.isReadPermission) {
|
||||
readPermissionAllowed = true
|
||||
}
|
||||
if (uriPermission.isWritePermission) {
|
||||
writePermissionAllowed = true
|
||||
}
|
||||
contentResolver?.apply {
|
||||
var readPermissionAllowed = false
|
||||
var writePermissionAllowed = false
|
||||
// Check current permissions allowed
|
||||
persistedUriPermissions.find { uriPermission ->
|
||||
uriPermission.uri == uri
|
||||
}?.let { uriPermission ->
|
||||
Log.d(TAG, "Check URI permission : $uriPermission")
|
||||
if (uriPermission.isReadPermission) {
|
||||
readPermissionAllowed = true
|
||||
}
|
||||
|
||||
// Release permission
|
||||
if (release) {
|
||||
if (writePermissionAllowed) {
|
||||
Log.d(TAG, "Release write permission : $uri")
|
||||
val removeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
releasePersistableUriPermission(uri, removeFlags)
|
||||
}
|
||||
if (readPermissionAllowed) {
|
||||
Log.d(TAG, "Release read permission $uri")
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
releasePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
if (uriPermission.isWritePermission) {
|
||||
writePermissionAllowed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Take missing permission
|
||||
if (!readPermissionAllowed) {
|
||||
Log.d(TAG, "Take read permission $uri")
|
||||
// Release permission
|
||||
if (release) {
|
||||
if (writePermissionAllowed) {
|
||||
Log.d(TAG, "Release write permission : $uri")
|
||||
val removeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
releasePersistableUriPermission(uri, removeFlags)
|
||||
}
|
||||
if (readPermissionAllowed) {
|
||||
Log.d(TAG, "Release read permission $uri")
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
takePersistableUriPermission(uri, takeFlags)
|
||||
releasePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
if (readOnly) {
|
||||
if (writePermissionAllowed) {
|
||||
Log.d(TAG, "Release write permission $uri")
|
||||
val removeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
releasePersistableUriPermission(uri, removeFlags)
|
||||
}
|
||||
} else {
|
||||
if (!writePermissionAllowed) {
|
||||
Log.d(TAG, "Take write permission $uri")
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
takePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
}
|
||||
|
||||
// Take missing permission
|
||||
if (!readPermissionAllowed) {
|
||||
Log.d(TAG, "Take read permission $uri")
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
takePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
if (readOnly) {
|
||||
if (writePermissionAllowed) {
|
||||
Log.d(TAG, "Release write permission $uri")
|
||||
val removeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
releasePersistableUriPermission(uri, removeFlags)
|
||||
}
|
||||
} else {
|
||||
if (!writePermissionAllowed) {
|
||||
Log.d(TAG, "Take write permission $uri")
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
takePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,42 +138,38 @@ object UriUtil {
|
||||
}
|
||||
|
||||
fun Context.releaseAllUnnecessaryPermissionUris() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
applicationContext?.let { appContext ->
|
||||
val fileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(appContext)
|
||||
fileDatabaseHistoryAction.getDatabaseFileList { databaseFileList ->
|
||||
val listToNotRemove = mutableListOf<Uri>()
|
||||
databaseFileList.forEach {
|
||||
it.databaseUri?.let { databaseUri ->
|
||||
listToNotRemove.add(databaseUri)
|
||||
}
|
||||
it.keyFileUri?.let { keyFileUri ->
|
||||
listToNotRemove.add(keyFileUri)
|
||||
}
|
||||
applicationContext?.let { appContext ->
|
||||
val fileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(appContext)
|
||||
fileDatabaseHistoryAction.getDatabaseFileList { databaseFileList ->
|
||||
val listToNotRemove = mutableListOf<Uri>()
|
||||
databaseFileList.forEach {
|
||||
it.databaseUri?.let { databaseUri ->
|
||||
listToNotRemove.add(databaseUri)
|
||||
}
|
||||
// Remove URI permission for not database files
|
||||
val resolver = appContext.contentResolver
|
||||
resolver.persistedUriPermissions.forEach { uriPermission ->
|
||||
val uri = uriPermission.uri
|
||||
if (!listToNotRemove.contains(uri))
|
||||
resolver.releaseUriPermission(uri)
|
||||
it.keyFileUri?.let { keyFileUri ->
|
||||
listToNotRemove.add(keyFileUri)
|
||||
}
|
||||
}
|
||||
// Remove URI permission for not database files
|
||||
val resolver = appContext.contentResolver
|
||||
resolver.persistedUriPermissions.forEach { uriPermission ->
|
||||
val uri = uriPermission.uri
|
||||
if (!listToNotRemove.contains(uri))
|
||||
resolver.releaseUriPermission(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Intent.getUri(key: String): Uri? {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
val clipData = this.clipData
|
||||
if (clipData != null) {
|
||||
if (clipData.description.label == key) {
|
||||
if (clipData.itemCount == 1) {
|
||||
val clipItem = clipData.getItemAt(0)
|
||||
if (clipItem != null) {
|
||||
return clipItem.uri
|
||||
}
|
||||
val clipData = this.clipData
|
||||
if (clipData != null) {
|
||||
if (clipData.description.label == key) {
|
||||
if (clipData.itemCount == 1) {
|
||||
val clipItem = clipData.getItemAt(0)
|
||||
if (clipItem != null) {
|
||||
return clipItem.uri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,27 +25,26 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0)
|
||||
class DeviceUnlockView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0)
|
||||
: LinearLayout(context, attrs, defStyle) {
|
||||
|
||||
private var biometricButtonView: Button? = null
|
||||
|
||||
init {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_advanced_unlock, this)
|
||||
inflater?.inflate(R.layout.view_device_unlock, this)
|
||||
|
||||
biometricButtonView = findViewById(R.id.biometric_button)
|
||||
}
|
||||
|
||||
fun setIconViewClickListener(listener: OnClickListener?) {
|
||||
fun setDeviceUnlockButtonViewClickListener(listener: OnClickListener?) {
|
||||
biometricButtonView?.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
@@ -60,14 +59,4 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
||||
fun setTitle(@StringRes textId: Int) {
|
||||
title = context.getString(textId)
|
||||
}
|
||||
|
||||
fun setMessage(text: CharSequence) {
|
||||
if (text.isNotEmpty())
|
||||
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun setMessage(@StringRes textId: Int) {
|
||||
Toast.makeText(context, textId, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -53,9 +53,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
private var checkboxHardwareView: CompoundButton
|
||||
private var hardwareKeySelectionView: HardwareKeySelectionView
|
||||
|
||||
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
||||
var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
||||
var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
||||
var onConditionToStoreCredentialChanged: ((CredentialStorage, verified: Boolean) -> Unit)? = null
|
||||
var onValidateListener: (() -> Unit)? = null
|
||||
|
||||
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
|
||||
@@ -103,24 +101,33 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
handled
|
||||
}
|
||||
|
||||
checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
|
||||
onPasswordChecked?.onCheckedChanged(view, checked)
|
||||
checkboxPasswordView.setOnCheckedChangeListener { _, _ ->
|
||||
onConditionToStoreCredentialChanged?.invoke(
|
||||
mCredentialStorage,
|
||||
conditionToStoreCredential()
|
||||
)
|
||||
}
|
||||
checkboxKeyFileView.setOnCheckedChangeListener { view, checked ->
|
||||
checkboxKeyFileView.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) {
|
||||
if (keyFileSelectionView.uri == null) {
|
||||
checkboxKeyFileView.isChecked = false
|
||||
}
|
||||
}
|
||||
onKeyFileChecked?.onCheckedChanged(view, checked)
|
||||
onConditionToStoreCredentialChanged?.invoke(
|
||||
mCredentialStorage,
|
||||
conditionToStoreCredential()
|
||||
)
|
||||
}
|
||||
checkboxHardwareView.setOnCheckedChangeListener { view, checked ->
|
||||
checkboxHardwareView.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) {
|
||||
if (hardwareKeySelectionView.hardwareKey == null) {
|
||||
checkboxHardwareView.isChecked = false
|
||||
}
|
||||
}
|
||||
onHardwareKeyChecked?.onCheckedChanged(view, checked)
|
||||
onConditionToStoreCredentialChanged?.invoke(
|
||||
mCredentialStorage,
|
||||
conditionToStoreCredential()
|
||||
)
|
||||
}
|
||||
|
||||
hardwareKeySelectionView.selectionListener = { _ ->
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.Spannable
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
@@ -104,18 +103,14 @@ class PasswordTextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
id = passwordProgressViewId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(LEFT_OF, actionImageButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
it.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
}
|
||||
mPasswordEntropyView.apply {
|
||||
id = passwordEntropyViewId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(ALIGN_RIGHT, passwordProgressViewId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(ALIGN_END, passwordProgressViewId)
|
||||
}
|
||||
it.addRule(ALIGN_END, passwordProgressViewId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
private var searchUsername: CompoundButton
|
||||
private var searchPassword: CompoundButton
|
||||
private var searchURL: CompoundButton
|
||||
private var searchByURLDomain: Boolean = false
|
||||
private var searchExpired: CompoundButton
|
||||
private var searchNotes: CompoundButton
|
||||
private var searchOther: CompoundButton
|
||||
@@ -50,6 +51,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
this.searchInUsernames = searchUsername.isChecked
|
||||
this.searchInPasswords = searchPassword.isChecked
|
||||
this.searchInUrls = searchURL.isChecked
|
||||
this.searchByDomain = searchByURLDomain
|
||||
this.searchInExpired = searchExpired.isChecked
|
||||
this.searchInNotes = searchNotes.isChecked
|
||||
this.searchInOther = searchOther.isChecked
|
||||
@@ -70,6 +72,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
||||
searchUsername.isChecked = value.searchInUsernames
|
||||
searchPassword.isChecked = value.searchInPasswords
|
||||
searchURL.isChecked = value.searchInUrls
|
||||
searchByURLDomain = value.searchByDomain
|
||||
searchExpired.isChecked = value.searchInExpired
|
||||
searchNotes.isChecked = value.searchInNotes
|
||||
searchOther.isChecked = value.searchInOther
|
||||
|
||||
@@ -18,8 +18,14 @@ import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.element.template.*
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateAttributeOption
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateAttributeType
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngine.Companion.addTemplateDecorator
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
@@ -608,9 +614,8 @@ abstract class TemplateAbstractView<
|
||||
getViewFieldByName(oldField.name)?.view?.let { viewToReplace ->
|
||||
val oldValue = getCustomField(oldField.name).protectedValue.toString()
|
||||
|
||||
val parentGroup = viewToReplace.parent as ViewGroup
|
||||
val indexInParent = parentGroup.indexOfChild(viewToReplace)
|
||||
parentGroup.removeView(viewToReplace)
|
||||
val parentGroup = viewToReplace.parent as? ViewGroup?
|
||||
parentGroup?.removeView(viewToReplace)
|
||||
|
||||
val newCustomFieldWithValue = if (keepOldValue)
|
||||
Field(newField.name,
|
||||
@@ -624,7 +629,9 @@ abstract class TemplateAbstractView<
|
||||
|
||||
val newCustomView = buildViewForCustomField(newCustomFieldWithValue)
|
||||
newCustomView?.let {
|
||||
parentGroup.addView(newCustomView, indexInParent)
|
||||
parentGroup?.indexOfChild(viewToReplace)?.let { indexInParent ->
|
||||
parentGroup.addView(newCustomView, indexInParent)
|
||||
}
|
||||
mViewFields.add(
|
||||
oldPosition,
|
||||
ViewField(
|
||||
|
||||
@@ -123,6 +123,9 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||
setMaxChars(templateAttribute.options.getNumberChars())
|
||||
setMaxLines(templateAttribute.options.getNumberLines())
|
||||
setActionClick(templateAttribute, field, this)
|
||||
if (field.protectedValue.isProtected) {
|
||||
textDirection = TEXT_DIRECTION_LTR
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
@@ -62,6 +63,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
||||
// Here the value is often empty
|
||||
|
||||
if (field.protectedValue.isProtected) {
|
||||
textDirection = TEXT_DIRECTION_LTR
|
||||
if (mFirstTimeAskAllowCopyProtectedFields) {
|
||||
setCopyButtonState(TextFieldView.ButtonState.DEACTIVATE)
|
||||
setCopyButtonClickListener { _, _ ->
|
||||
@@ -175,6 +177,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
||||
otpElement.type.name,
|
||||
ProtectedString(false, otpElement.token)))
|
||||
}
|
||||
textDirection = TEXT_DIRECTION_LTR
|
||||
mLastOtpTokenView = this
|
||||
mOtpRunnable = Runnable {
|
||||
if (otpElement.shouldRefreshToken()) {
|
||||
|
||||
@@ -51,9 +51,7 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
imeOptions = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||
importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
}
|
||||
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
maxLines = 1
|
||||
}
|
||||
private var actionImageButton = AppCompatImageButton(
|
||||
@@ -70,11 +68,9 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
it.addRule(ALIGN_PARENT_RIGHT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
visibility = View.GONE
|
||||
visibility = GONE
|
||||
contentDescription = context.getString(R.string.menu_edit)
|
||||
}
|
||||
|
||||
@@ -91,9 +87,7 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
id = labelViewId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(LEFT_OF, actionImageButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
it.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
}
|
||||
valueView.apply {
|
||||
@@ -192,7 +186,7 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
actionImageButton.setImageDrawable(ContextCompat.getDrawable(context, it))
|
||||
}
|
||||
actionImageButton.setOnClickListener(onActionClickListener)
|
||||
actionImageButton.visibility = if (onActionClickListener == null) View.GONE else View.VISIBLE
|
||||
actionImageButton.visibility = if (onActionClickListener == null) GONE else VISIBLE
|
||||
}
|
||||
|
||||
override var isFieldVisible: Boolean
|
||||
|
||||
@@ -63,13 +63,12 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
4f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.marginStart = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
4f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
it.marginStart = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
4f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
|
||||
}
|
||||
}
|
||||
protected val valueView = AppCompatTextView(context).apply {
|
||||
@@ -88,13 +87,11 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
8f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.marginStart = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
8f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
it.marginStart = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
8f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
setTextIsSelectable(true)
|
||||
}
|
||||
@@ -128,9 +125,7 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
id = copyButtonId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(ALIGN_PARENT_RIGHT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
}
|
||||
showButton.apply {
|
||||
@@ -138,14 +133,14 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
if (copyButton.isVisible) {
|
||||
it.addRule(LEFT_OF, copyButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(START_OF, copyButtonId)
|
||||
}
|
||||
|
||||
it.addRule(START_OF, copyButtonId)
|
||||
|
||||
} else {
|
||||
it.addRule(ALIGN_PARENT_RIGHT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,18 +148,14 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
id = labelViewId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(LEFT_OF, showButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(START_OF, showButtonId)
|
||||
}
|
||||
it.addRule(START_OF, showButtonId)
|
||||
}
|
||||
}
|
||||
valueView.apply {
|
||||
id = valueViewId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(LEFT_OF, showButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(START_OF, showButtonId)
|
||||
}
|
||||
it.addRule(START_OF, showButtonId)
|
||||
it.addRule(BELOW, labelViewId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -51,9 +56,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
|
||||
imeOptions = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||
importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
}
|
||||
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_white_24dp)
|
||||
?.apply {
|
||||
mutate().colorFilter = BlendModeColorFilterCompat
|
||||
@@ -65,14 +68,12 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
|
||||
drawable,
|
||||
null
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
drawable,
|
||||
null
|
||||
)
|
||||
}
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
drawable,
|
||||
null
|
||||
)
|
||||
isFocusable = false
|
||||
inputType = InputType.TYPE_NULL
|
||||
maxLines = 1
|
||||
@@ -94,9 +95,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
it.addRule(ALIGN_PARENT_RIGHT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
visibility = View.GONE
|
||||
contentDescription = context.getString(R.string.menu_edit)
|
||||
@@ -132,18 +131,14 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
|
||||
id = labelViewId
|
||||
layoutParams = (layoutParams as LayoutParams?).also {
|
||||
it?.addRule(LEFT_OF, actionImageButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it?.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
it?.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
}
|
||||
valueSpinnerView.apply {
|
||||
id = valueViewId
|
||||
layoutParams = (layoutParams as LayoutParams?).also {
|
||||
it?.addRule(LEFT_OF, actionImageButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it?.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
it?.addRule(START_OF, actionImageButtonId)
|
||||
it?.addRule(BELOW, labelViewId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.animation.AnimatorSet
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
@@ -58,13 +57,14 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.view.updatePaddingRelative
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.helper.getLocalizedMessage
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import java.util.EnumSet
|
||||
|
||||
|
||||
/**
|
||||
@@ -225,14 +225,16 @@ fun View.showByFading() {
|
||||
}
|
||||
}
|
||||
|
||||
fun View.updateLockPaddingLeft() {
|
||||
updatePadding(resources.getDimensionPixelSize(
|
||||
if (PreferencesUtil.showLockDatabaseButton(context)) {
|
||||
R.dimen.lock_button_size
|
||||
} else {
|
||||
R.dimen.hidden_lock_button_size
|
||||
}
|
||||
))
|
||||
fun View.updateLockPaddingStart() {
|
||||
resources.getDimensionPixelSize(
|
||||
if (PreferencesUtil.showLockDatabaseButton(context)) {
|
||||
R.dimen.lock_button_size
|
||||
} else {
|
||||
R.dimen.hidden_lock_button_size
|
||||
}
|
||||
).let { lockPadding ->
|
||||
updatePaddingRelative(lockPadding)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
|
||||
@@ -301,10 +303,9 @@ fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
|
||||
// Only in portrait
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
|
||||
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
|
||||
if (applyToStatusBar) {
|
||||
@@ -320,7 +321,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
|
||||
/**
|
||||
* Apply a margin to a view to fix the window inset
|
||||
*/
|
||||
fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.BOTTOM) {
|
||||
fun View.applyWindowInsets(positions: EnumSet<WindowInsetPosition>) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
|
||||
var consumed = false
|
||||
|
||||
@@ -336,52 +337,78 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
|
||||
}
|
||||
}
|
||||
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
when (position) {
|
||||
WindowInsetPosition.TOP -> {
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()
|
||||
or WindowInsetsCompat.Type.displayCutout()
|
||||
or WindowInsetsCompat.Type.ime())
|
||||
|
||||
val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
|
||||
val wantTopMargins = positions.contains(WindowInsetPosition.TOP_MARGINS)
|
||||
val wantBottomMargins = positions.contains(WindowInsetPosition.BOTTOM_MARGINS)
|
||||
val wantStartMargins = positions.contains(WindowInsetPosition.START_MARGINS)
|
||||
val wantEndMargins = positions.contains(WindowInsetPosition.END_MARGINS)
|
||||
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams
|
||||
&& (wantTopMargins || wantBottomMargins || wantStartMargins || wantEndMargins)) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
if (wantTopMargins) {
|
||||
topMargin = insets.top
|
||||
}
|
||||
if (wantBottomMargins) {
|
||||
bottomMargin = insets.bottom
|
||||
}
|
||||
if (wantStartMargins) {
|
||||
if (isRtl) {
|
||||
rightMargin = insets.right
|
||||
} else {
|
||||
leftMargin = insets.left
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowInsetPosition.LEGIT_TOP -> {
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowInsetPosition.BOTTOM -> {
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowInsetPosition.BOTTOM_IME -> {
|
||||
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = if (imeHeight > 1) 0 else insets.bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowInsetPosition.TOP_BOTTOM_IME -> {
|
||||
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
bottomMargin = if (imeHeight > 1) imeHeight else 0
|
||||
if (wantEndMargins) {
|
||||
if (isRtl) {
|
||||
leftMargin = insets.left
|
||||
} else {
|
||||
rightMargin = insets.right
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val wantTopPadding = positions.contains(WindowInsetPosition.TOP_PADDING)
|
||||
val wantBottomPadding = positions.contains(WindowInsetPosition.BOTTOM_PADDING)
|
||||
val wantStartPadding = positions.contains(WindowInsetPosition.START_PADDING)
|
||||
val wantEndPadding = positions.contains(WindowInsetPosition.END_PADDING)
|
||||
|
||||
if (wantTopPadding || wantBottomPadding || wantStartPadding || wantEndPadding) {
|
||||
val topPadding = if (wantTopPadding) insets.top else 0
|
||||
val bottomPadding = if (wantBottomPadding) insets.bottom else 0
|
||||
var leftPadding = 0
|
||||
var rightPadding = 0
|
||||
|
||||
if (wantStartPadding) {
|
||||
if (isRtl) {
|
||||
rightPadding = insets.right
|
||||
} else {
|
||||
leftPadding = insets.left
|
||||
}
|
||||
}
|
||||
if (wantEndPadding) {
|
||||
if (isRtl) {
|
||||
leftPadding = insets.left
|
||||
} else {
|
||||
rightPadding = insets.right
|
||||
}
|
||||
}
|
||||
|
||||
setPadding(leftPadding, topPadding, rightPadding, bottomPadding)
|
||||
}
|
||||
|
||||
// If any of the children consumed the insets, return an appropriate value
|
||||
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
enum class WindowInsetPosition {
|
||||
TOP, BOTTOM, LEGIT_TOP, BOTTOM_IME, TOP_BOTTOM_IME
|
||||
TOP_MARGINS, BOTTOM_MARGINS, START_MARGINS, END_MARGINS,
|
||||
TOP_PADDING, BOTTOM_PADDING, START_PADDING, END_PADDING,
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.kunzisoft.keepass.viewmodels
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class AdvancedUnlockViewModel : ViewModel() {
|
||||
|
||||
var allowAutoOpenBiometricPrompt : Boolean = true
|
||||
var deviceCredentialAuthSucceeded: Boolean? = null
|
||||
|
||||
val onInitAdvancedUnlockModeRequested : LiveData<Void?> get() = _onInitAdvancedUnlockModeRequested
|
||||
private val _onInitAdvancedUnlockModeRequested = SingleLiveEvent<Void?>()
|
||||
|
||||
val onUnlockAvailabilityCheckRequested : LiveData<Void?> get() = _onUnlockAvailabilityCheckRequested
|
||||
private val _onUnlockAvailabilityCheckRequested = SingleLiveEvent<Void?>()
|
||||
|
||||
val onDatabaseFileLoaded : LiveData<Uri?> get() = _onDatabaseFileLoaded
|
||||
private val _onDatabaseFileLoaded = SingleLiveEvent<Uri?>()
|
||||
|
||||
fun initAdvancedUnlockMode() {
|
||||
_onInitAdvancedUnlockModeRequested.call()
|
||||
}
|
||||
|
||||
fun checkUnlockAvailability() {
|
||||
_onUnlockAvailabilityCheckRequested.call()
|
||||
}
|
||||
|
||||
fun databaseFileLoaded(databaseUri: Uri?) {
|
||||
_onDatabaseFileLoaded.value = databaseUri
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@ class DatabaseFileViewModel(application: Application) : AndroidViewModel(applica
|
||||
|
||||
fun loadDatabaseFile(databaseUri: Uri) {
|
||||
mFileDatabaseHistoryAction?.getDatabaseFile(databaseUri) { databaseFileRetrieved ->
|
||||
mDatabaseFileLoaded.value = databaseFileRetrieved
|
||||
databaseFileRetrieved?.let {
|
||||
mDatabaseFileLoaded.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
package com.kunzisoft.keepass.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.kunzisoft.keepass.app.AppLifecycleObserver
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPrompt
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPromptType
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockMode
|
||||
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
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.crypto.Cipher
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
class DeviceUnlockViewModel(application: Application): AndroidViewModel(application) {
|
||||
private var cipherDatabase: CipherEncryptDatabase? = null
|
||||
|
||||
private var isConditionToStoreCredentialVerified: Boolean = false
|
||||
|
||||
private var deviceUnlockManager: DeviceUnlockManager? = null
|
||||
private var databaseUri: Uri? = null
|
||||
|
||||
private var mCipherJob: Job? = null
|
||||
|
||||
private var deviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE
|
||||
var cryptoPrompt: DeviceUnlockCryptoPrompt? = null
|
||||
private set
|
||||
private var isAutoOpenBiometricPromptAllowed = true
|
||||
private var cryptoPromptShowPending: Boolean = false
|
||||
|
||||
// TODO Retrieve credential storage from app database
|
||||
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
|
||||
|
||||
val cipherDatabaseAction = CipherDatabaseAction.getInstance(getApplication())
|
||||
private var cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
|
||||
override fun onCipherDatabaseRetrieved(
|
||||
databaseUri: Uri,
|
||||
cipherDatabase: CipherEncryptDatabase?
|
||||
) {
|
||||
if (databaseUri == this@DeviceUnlockViewModel.databaseUri) {
|
||||
cipherDatabase?.let {
|
||||
this@DeviceUnlockViewModel.cipherDatabase = it
|
||||
checkUnlockAvailability()
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
}
|
||||
override fun onCipherDatabaseAddedOrUpdated(cipherDatabase: CipherEncryptDatabase) {
|
||||
if (cipherDatabase.databaseUri == this@DeviceUnlockViewModel.databaseUri) {
|
||||
this@DeviceUnlockViewModel.cipherDatabase = cipherDatabase
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
}
|
||||
override fun onCipherDatabaseDeleted(databaseUri: Uri) {
|
||||
if (databaseUri == this@DeviceUnlockViewModel.databaseUri) {
|
||||
this@DeviceUnlockViewModel.cipherDatabase = null
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
}
|
||||
override fun onAllCipherDatabasesDeleted() {
|
||||
this@DeviceUnlockViewModel.cipherDatabase = null
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
override fun onCipherDatabaseCleared() {
|
||||
this@DeviceUnlockViewModel.cipherDatabase = null
|
||||
closeBiometricPrompt()
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow(DeviceUnlockState())
|
||||
val uiState: StateFlow<DeviceUnlockState> = _uiState
|
||||
|
||||
init {
|
||||
AppLifecycleObserver.appJustLaunched
|
||||
.onEach {
|
||||
isAutoOpenBiometricPromptAllowed = true
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
cipherDatabaseAction.registerDatabaseListener(cipherDatabaseListener)
|
||||
}
|
||||
|
||||
private fun cancelAndLaunchCipherJob(
|
||||
coroutineExceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, e ->
|
||||
setException(e)
|
||||
},
|
||||
block: suspend () -> Unit
|
||||
) {
|
||||
mCipherJob?.cancel()
|
||||
mCipherJob = viewModelScope.launch(coroutineExceptionHandler) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkConditionToStoreCredential(condition: Boolean) {
|
||||
isConditionToStoreCredentialVerified = condition
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlock availability and change the current mode depending of device's state
|
||||
*/
|
||||
fun checkUnlockAvailability() {
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(getApplication())) {
|
||||
// biometric not supported (by API level or hardware) so keep option hidden
|
||||
// or manually disable
|
||||
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(getApplication())
|
||||
if (!PreferencesUtil.isDeviceUnlockEnable(getApplication())
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
||||
changeMode(DeviceUnlockMode.BIOMETRIC_UNAVAILABLE)
|
||||
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
|
||||
changeMode(DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
||||
} else {
|
||||
// biometric is available but not configured, show icon but in disabled state with some information
|
||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||
changeMode(DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||
} else {
|
||||
changeMode()
|
||||
}
|
||||
}
|
||||
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(getApplication())) {
|
||||
if (DeviceUnlockManager.isDeviceSecure(getApplication())) {
|
||||
changeMode()
|
||||
} else {
|
||||
changeMode(DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeMode() {
|
||||
try {
|
||||
if (isConditionToStoreCredentialVerified) {
|
||||
// listen for encryption
|
||||
changeMode(DeviceUnlockMode.STORE_CREDENTIAL)
|
||||
} else if (cipherDatabase != null) {
|
||||
// biometric available but no stored password found yet for this DB
|
||||
// listen for decryption
|
||||
changeMode(DeviceUnlockMode.EXTRACT_CREDENTIAL)
|
||||
} else {
|
||||
// wait for typing
|
||||
changeMode(DeviceUnlockMode.WAIT_CREDENTIAL)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
changeMode(DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE)
|
||||
setException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeMode(deviceUnlockMode: DeviceUnlockMode) {
|
||||
this.deviceUnlockMode = deviceUnlockMode
|
||||
when (deviceUnlockMode) {
|
||||
DeviceUnlockMode.STORE_CREDENTIAL -> initEncryptData()
|
||||
DeviceUnlockMode.EXTRACT_CREDENTIAL -> initDecryptData()
|
||||
else -> {}
|
||||
}
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
newDeviceUnlockMode = deviceUnlockMode,
|
||||
allowDeviceUnlockMenu = cipherDatabase != null
|
||||
&& deviceUnlockMode != DeviceUnlockMode.BIOMETRIC_UNAVAILABLE
|
||||
&& deviceUnlockMode != DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectDatabase(databaseUri: Uri) {
|
||||
this.databaseUri = databaseUri
|
||||
cipherDatabaseAction.getCipherDatabase(databaseUri)
|
||||
}
|
||||
|
||||
private fun showPendingIfNecessary() {
|
||||
// Reassign prompt state to open again if necessary
|
||||
if (cryptoPrompt?.isOldCredentialOperation() != true
|
||||
&& uiState.value.cryptoPromptState == DeviceUnlockPromptMode.IDLE_SHOW) {
|
||||
cryptoPromptShowPending = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnectDatabase() {
|
||||
this.databaseUri = null
|
||||
this.cipherDatabase = null
|
||||
clearPrompt()
|
||||
changeMode(DeviceUnlockMode.BIOMETRIC_UNAVAILABLE)
|
||||
}
|
||||
|
||||
fun connect(databaseUri: Uri?) {
|
||||
Log.d(TAG, "Connect to device unlock")
|
||||
// To get device credential unlock result, only if same database uri
|
||||
if (databaseUri != null
|
||||
&& PreferencesUtil.isDeviceUnlockEnable(getApplication())) {
|
||||
if (databaseUri != this.databaseUri) {
|
||||
connectDatabase(databaseUri)
|
||||
}
|
||||
} else {
|
||||
disconnectDatabase()
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
Log.d(TAG, "Disconnect from device unlock")
|
||||
showPendingIfNecessary()
|
||||
disconnectDatabase()
|
||||
}
|
||||
|
||||
fun onAuthenticationSucceeded() {
|
||||
cryptoPrompt?.let { prompt ->
|
||||
when (prompt.type) {
|
||||
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
|
||||
retrieveCredentialForEncryption( prompt.cipher)
|
||||
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
|
||||
decryptCredential( prompt.cipher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult
|
||||
) {
|
||||
cryptoPrompt?.type?.let { type ->
|
||||
when (type) {
|
||||
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
|
||||
retrieveCredentialForEncryption(result.cryptoObject?.cipher)
|
||||
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
|
||||
decryptCredential(result.cryptoObject?.cipher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrieveCredentialForEncryption(cipher: Cipher?) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
credentialRequiredCipher = cipher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun encryptCredential(
|
||||
credential: ByteArray,
|
||||
cipher: Cipher?
|
||||
) {
|
||||
cancelAndLaunchCipherJob {
|
||||
deviceUnlockManager?.encryptData(
|
||||
value = credential,
|
||||
cipher = cipher,
|
||||
handleEncryptedResult = { encryptedValue, ivSpec ->
|
||||
databaseUri?.let { databaseUri ->
|
||||
onCredentialEncrypted(
|
||||
CipherEncryptDatabase().apply {
|
||||
this.databaseUri = databaseUri
|
||||
this.credentialStorage = credentialDatabaseStorage
|
||||
this.encryptedValue = encryptedValue
|
||||
this.specParameters = ivSpec
|
||||
}
|
||||
)
|
||||
} ?: throw UnknownDatabaseLocationException()
|
||||
}
|
||||
)
|
||||
}
|
||||
// Reinit credential storage request
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
credentialRequiredCipher = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptCredential(cipher: Cipher?) {
|
||||
// retrieve the encrypted value from preferences
|
||||
cancelAndLaunchCipherJob {
|
||||
databaseUri?.let { databaseUri ->
|
||||
cipherDatabase?.encryptedValue?.let { encryptedCredential ->
|
||||
deviceUnlockManager?.decryptData(
|
||||
encryptedValue = encryptedCredential,
|
||||
cipher = cipher,
|
||||
handleDecryptedResult = { decryptedValue ->
|
||||
// Load database directly with password retrieve
|
||||
onCredentialDecrypted(
|
||||
CipherDecryptDatabase().apply {
|
||||
this.databaseUri = databaseUri
|
||||
this.credentialStorage = credentialDatabaseStorage
|
||||
this.decryptedValue = decryptedValue
|
||||
}
|
||||
)
|
||||
cipherDatabaseAction.resetCipherParameters(databaseUri)
|
||||
}
|
||||
)
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
} ?: run {
|
||||
throw UnknownDatabaseLocationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherEncryptDatabase = cipherEncryptDatabase
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeCredentialEncrypted() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherEncryptDatabase = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherDecryptDatabase = cipherDecryptDatabase
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeCredentialDecrypted() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherDecryptDatabase = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPromptRequested(
|
||||
cryptoPrompt: DeviceUnlockCryptoPrompt,
|
||||
autoOpen: Boolean = false
|
||||
) {
|
||||
this@DeviceUnlockViewModel.cryptoPrompt = cryptoPrompt
|
||||
if (cryptoPromptShowPending
|
||||
|| (autoOpen && PreferencesUtil.isDeviceUnlockPromptAutoOpenEnable(getApplication())))
|
||||
showPrompt()
|
||||
}
|
||||
|
||||
fun showPrompt() {
|
||||
AppLifecycleObserver.lockBackgroundEvent = true
|
||||
isAutoOpenBiometricPromptAllowed = false
|
||||
cryptoPromptShowPending = false
|
||||
if (cryptoPrompt == null) {
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cryptoPromptState = DeviceUnlockPromptMode.SHOW
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun promptShown() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cryptoPromptState = DeviceUnlockPromptMode.IDLE_SHOW
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setException(value: Throwable?) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
exception = value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun exceptionShown() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
exception = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEncryptData() {
|
||||
cancelAndLaunchCipherJob {
|
||||
deviceUnlockManager = DeviceUnlockManager(getApplication())
|
||||
deviceUnlockManager?.initEncryptData { cryptoPrompt ->
|
||||
onPromptRequested(cryptoPrompt)
|
||||
} ?: throw Exception("Device unlock manager not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDecryptData() {
|
||||
cancelAndLaunchCipherJob {
|
||||
cipherDatabase?.let { cipherDb ->
|
||||
deviceUnlockManager = DeviceUnlockManager(getApplication())
|
||||
deviceUnlockManager?.initDecryptData(cipherDb.specParameters) { cryptoPrompt ->
|
||||
onPromptRequested(
|
||||
cryptoPrompt,
|
||||
autoOpen = isAutoOpenBiometricPromptAllowed
|
||||
)
|
||||
} ?: throw Exception("Device unlock manager not initialized")
|
||||
} ?: throw Exception("Cipher database not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteEncryptedDatabaseKey() {
|
||||
closeBiometricPrompt()
|
||||
databaseUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri)
|
||||
} ?: run {
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
allowDeviceUnlockMenu = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun closeBiometricPrompt() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cryptoPromptState = DeviceUnlockPromptMode.CLOSE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun biometricPromptClosed() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cryptoPromptState = DeviceUnlockPromptMode.IDLE_CLOSE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearPrompt() {
|
||||
cryptoPrompt = null
|
||||
deviceUnlockManager = null
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
if (cryptoPrompt?.isOldCredentialOperation() != true) {
|
||||
clearPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
cipherDatabaseAction.unregisterDatabaseListener(cipherDatabaseListener)
|
||||
clearPrompt()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DeviceUnlockViewModel::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeviceUnlockPromptMode {
|
||||
IDLE_CLOSE, IDLE_SHOW, SHOW, CLOSE
|
||||
}
|
||||
|
||||
data class DeviceUnlockState(
|
||||
val newDeviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE,
|
||||
val allowDeviceUnlockMenu: Boolean = false,
|
||||
val credentialRequiredCipher: Cipher? = null,
|
||||
val cipherEncryptDatabase: CipherEncryptDatabase? = null,
|
||||
val cipherDecryptDatabase: CipherDecryptDatabase? = null,
|
||||
val cryptoPromptState: DeviceUnlockPromptMode = DeviceUnlockPromptMode.IDLE_CLOSE,
|
||||
val autoOpenPrompt: Boolean = false,
|
||||
val exception: Throwable? = null
|
||||
)
|
||||
13
app/src/main/res/drawable-ldrtl/ic_arrow_back_white_24dp.xml
Normal file
13
app/src/main/res/drawable-ldrtl/ic_arrow_back_white_24dp.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.78885484"
|
||||
android:pathData="M10,19Q7.5,19 5.5,17.5Q4,16 4,13.5Q4,11 5.5,9.5Q7.5,8 10,8L16,8L13.5,5.5L15,4L20,9L15,14L13.5,12.5L16,10L10,10Q8.5,10 7,11Q6,12 6,13.5Q6,15 7,16Q8.5,17 10,17L17,17L17,19L10,19Z" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6 -1.41,-1.41z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="@color/green"
|
||||
android:pathData="M14,7l-5,5 5,5V7z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@drawable/ic_arrow_left_white_24dp"
|
||||
android:fromDegrees="180"
|
||||
android:toDegrees="180"
|
||||
android:visible="true" />
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
<com.kunzisoft.keepass.view.DeviceUnlockView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/advanced_unlock_view"
|
||||
android:id="@+id/device_unlock_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
@@ -19,10 +19,11 @@
|
||||
-->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/activity_entry_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:filterTouchesWhenObscured="true">
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
@@ -58,6 +59,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_collapseMode="parallax"
|
||||
android:layoutDirection="ltr"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
-->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fitsSystemWindows="true"
|
||||
android:id="@+id/icon_picker_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
android:filterTouchesWhenObscured="true">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/icon_picker_coordinator"
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
-->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fitsSystemWindows="true"
|
||||
android:id="@+id/key_generator_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
android:filterTouchesWhenObscured="true">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/key_generator_coordinator"
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
@@ -180,7 +181,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface">
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fragment_advanced_unlock_container_view"
|
||||
android:id="@+id/fragment_device_unlock_container_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
@@ -196,7 +197,7 @@
|
||||
style="@style/KeepassDXStyle.Button.Primary"
|
||||
app:icon="@drawable/ic_lock_open_white_24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/fragment_advanced_unlock_container_view"
|
||||
app:layout_constraintStart_toEndOf="@+id/fragment_device_unlock_container_view"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
-->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
android:importantForAccessibility="no"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textPassword|textMultiLine"
|
||||
android:textDirection="ltr"
|
||||
android:maxLines="3"
|
||||
android:hint="@string/hint_conf_pass"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
@@ -122,8 +122,7 @@
|
||||
android:inputType="textPassword"
|
||||
android:importantForAccessibility="no"
|
||||
android:importantForAutofill="no"
|
||||
android:hint="@string/otp_secret"
|
||||
tools:targetApi="jelly_bean" />
|
||||
android:hint="@string/otp_secret" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -178,8 +177,7 @@
|
||||
tools:text="30"
|
||||
android:maxLength="3"
|
||||
android:digits="0123456789"
|
||||
android:imeOptions="actionNext"
|
||||
tools:targetApi="jelly_bean" />
|
||||
android:imeOptions="actionNext" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/setup_otp_counter_label"
|
||||
@@ -198,8 +196,7 @@
|
||||
android:importantForAutofill="no"
|
||||
android:hint="@string/otp_counter"
|
||||
tools:text="1"
|
||||
android:imeOptions="actionNext"
|
||||
tools:targetApi="jelly_bean" />
|
||||
android:imeOptions="actionNext" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
@@ -228,8 +225,7 @@
|
||||
tools:text="6"
|
||||
android:maxLength="2"
|
||||
android:digits="0123456789"
|
||||
android:imeOptions="actionNext"
|
||||
tools:targetApi="jelly_bean" />
|
||||
android:imeOptions="actionNext" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -237,4 +233,4 @@
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_arrow_right_white_24dp"
|
||||
app:tint="?attr/colorOnSurface"
|
||||
android:importantForAccessibility="no"
|
||||
tools:targetApi="jelly_bean" />
|
||||
android:importantForAccessibility="no" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -125,8 +125,7 @@
|
||||
android:paddingStart="8dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:paddingVertical="4dp">
|
||||
android:paddingRight="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/node_otp_token"
|
||||
@@ -179,4 +178,4 @@
|
||||
tools:text="Database / Group A / Group B" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -61,11 +61,11 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@+id/node_icon"
|
||||
android:layout_marginEnd="-32dp"
|
||||
android:layout_marginRight="-32dp"
|
||||
android:layout_toStartOf="@+id/node_icon"
|
||||
android:layout_toLeftOf="@+id/node_icon"
|
||||
tools:text="3" />
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
tools:text="123" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="8dp"
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layoutDirection="ltr"
|
||||
app:layout_constraintTop_toBottomOf="@+id/switch_element">
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
android:minHeight="48dp"
|
||||
android:hint="@string/password"
|
||||
android:inputType="textPassword"
|
||||
android:textDirection="ltr"
|
||||
android:importantForAutofill="no"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
android:importantForAccessibility="no"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textPassword|textMultiLine"
|
||||
android:textDirection="ltr"
|
||||
android:maxLines="3"
|
||||
tools:ignore="TextFields" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
@@ -18,39 +18,39 @@
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
--><resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<string name="homepage">الصفحة الرئيسة</string>
|
||||
<string name="accept">قبول</string>
|
||||
<string name="add_group">إضافة مجموعة</string>
|
||||
<string name="encryption">التشفير</string>
|
||||
<string name="encryption_algorithm">خوارزمية التشفير</string>
|
||||
<string name="accept">اقبل</string>
|
||||
<string name="add_group">أضف مجموعة</string>
|
||||
<string name="encryption">التعمية</string>
|
||||
<string name="encryption_algorithm">خوارزمية التعمية</string>
|
||||
<string name="application">التطبيق</string>
|
||||
<string name="brackets">الأقواس</string>
|
||||
<string name="extended_ASCII">ASCII ممتد</string>
|
||||
<string name="allow">سماح</string>
|
||||
<string name="allow">اسمح</string>
|
||||
<string name="clipboard_cleared">مُسِحت الحافظة</string>
|
||||
<string name="clipboard_error_title">خطأ في الحافظة</string>
|
||||
<string name="clipboard_error_clear">تعذَّر مسح الحافظة</string>
|
||||
<string name="database">قاعدة البيانات</string>
|
||||
<string name="decrypting_db">يفك تشفير محتوى قاعدة البيانات…</string>
|
||||
<string name="decrypting_db">يفك تعمية محتوى قاعدة البيانات…</string>
|
||||
<string name="digits">أرقام</string>
|
||||
<string name="entry_cancel">إلغاء</string>
|
||||
<string name="entry_cancel">ألغِ</string>
|
||||
<string name="entry_notes">ملاحظات</string>
|
||||
<string name="entry_confpassword">تأكيد كلمة السر</string>
|
||||
<string name="entry_confpassword">أكّد كلمة السر</string>
|
||||
<string name="entry_created">أُنشئ</string>
|
||||
<string name="entry_modified">معدل</string>
|
||||
<string name="entry_modified">مُعدل</string>
|
||||
<string name="entry_not_found">تعذر العثور على بيانات المُدخلة.</string>
|
||||
<string name="entry_password">كلمة السر</string>
|
||||
<string name="save">حفظ</string>
|
||||
<string name="save">احفظ</string>
|
||||
<string name="entry_title">العنوان</string>
|
||||
<string name="entry_url">رابط</string>
|
||||
<string name="entry_user_name">اسم المستخدم</string>
|
||||
<string name="error_file_not_create">تعذر إنشاء الملف</string>
|
||||
<string name="error_file_not_create">تعذر إنشاء الملف.</string>
|
||||
<string name="error_invalid_path">تأكد أن المسار صحيح.</string>
|
||||
<string name="error_no_name">ادخل اسمًا.</string>
|
||||
<string name="error_pass_match">كلمتا السر غير متطابقتين.</string>
|
||||
<string name="field_name">اسم الحقل</string>
|
||||
<string name="field_value">قيمة الحقل</string>
|
||||
<string name="generate_password">توليد كلمة سر</string>
|
||||
<string name="hint_conf_pass">تأكيد كلمة السر</string>
|
||||
<string name="generate_password">ولّد كلمة سر</string>
|
||||
<string name="hint_conf_pass">أكّد كلمة السر</string>
|
||||
<string name="hint_group_name">اسم المجموعة</string>
|
||||
<string name="hint_length">الطول</string>
|
||||
<string name="hint_pass">كلمة السر</string>
|
||||
@@ -61,15 +61,15 @@
|
||||
<string name="list_size_summary">حجم النص في قائمة العناصر</string>
|
||||
<string name="loading_database">يحمل قاعدة البيانات…</string>
|
||||
<string name="lowercase">حروف صغيرة</string>
|
||||
<string name="hide_password_summary">إخفاء كلمات السر بشكل افتراضي</string>
|
||||
<string name="hide_password_summary">أخفِ كلمات السر (***) افتراضيًا</string>
|
||||
<string name="about">عن التطبيق</string>
|
||||
<string name="menu_change_key_settings">تغيير المفتاح الرئيسي</string>
|
||||
<string name="settings">الإعدادات</string>
|
||||
<string name="menu_app_settings">إعدادات التطبيق</string>
|
||||
<string name="menu_database_settings">إعدادات قاعدة البيانات</string>
|
||||
<string name="menu_delete">حذف</string>
|
||||
<string name="menu_delete">احذف</string>
|
||||
<string name="menu_donate">التبرع</string>
|
||||
<string name="menu_edit">تعديل</string>
|
||||
<string name="menu_edit">عدّل</string>
|
||||
<string name="menu_lock">اقفل قاعدة البيانات</string>
|
||||
<string name="menu_open">فتح</string>
|
||||
<string name="menu_search">البحث</string>
|
||||
@@ -77,10 +77,10 @@
|
||||
<string name="never">أبداً</string>
|
||||
<string name="no_results">لا توجد نتائج للبحث</string>
|
||||
<string name="no_url_handler">ثبت متصفح لزيارة هذا الرابط.</string>
|
||||
<string name="progress_create">إنشاء قاعدة بيانات جديدة …</string>
|
||||
<string name="progress_create">يُنشئ قاعدة بيانات جديدة …</string>
|
||||
<string name="protection">الحماية</string>
|
||||
<string name="read_only">محمي من التعديل</string>
|
||||
<string name="content_description_remove_from_list">إزالة</string>
|
||||
<string name="content_description_remove_from_list">أزل</string>
|
||||
<string name="root">الجذر</string>
|
||||
<string name="memory_usage">استخدام الذاكرة</string>
|
||||
<string name="parallelism">التَّوازِي</string>
|
||||
@@ -101,26 +101,25 @@
|
||||
<string name="warning_no_encryption_key">أمتأكد أنك لا تريد استخدام أي مفتاح لتشفير ؟</string>
|
||||
<string name="version_label">الإصدار %1$s</string>
|
||||
<string name="education_new_node_title">أضف عناصر إلى قاعدة البيانات</string>
|
||||
<string name="education_entry_new_field_title">إضافة حقول مخصصة</string>
|
||||
<string name="education_field_copy_title">نسخ حقل</string>
|
||||
<string name="education_lock_title">تأمين قاعدة البيانات</string>
|
||||
<string name="education_entry_new_field_title">أضف حقول مخصّصة</string>
|
||||
<string name="education_field_copy_title">انسخ حقل</string>
|
||||
<string name="education_lock_title">اقفل قاعدة البيانات</string>
|
||||
<string name="feedback">أرسل انطباعاتك</string>
|
||||
<string name="about_description">\"KeePassDX\" هو تطبيق أندرويد لمدير كلمات المرور كي باس \"KeePass\"</string>
|
||||
<string name="about_description">تطبيق أندرويد لمدير كلمات السر KeePass.</string>
|
||||
<string name="add_entry">أضف مدخل</string>
|
||||
<string name="edit_entry">تحرير مدخل</string>
|
||||
<string name="edit_entry">عدّل مدخل</string>
|
||||
<string name="key_derivation_function">وظيفة اشتقاق المفتاح</string>
|
||||
<string name="app_timeout">المهلة</string>
|
||||
<string name="app_timeout_summary">مدة الخمول قبل قفل قاعدة البيانات</string>
|
||||
<string name="file_manager_install_description">مدير الملفات الذي يمكنه القيام بالإجراءين ACTION_CREATE_DOCUMENT و ACTION_OPEN_DOCUMENT ضروري لانشاء, وفتح وحفض قواعد البيانات.</string>
|
||||
<string name="file_manager_install_description">مدير الملفات الذي يمكنه القيام بالإجراءين ACTION_CREATE_DOCUMENT و ACTION_OPEN_DOCUMENT ضروري لانشاء، وفتح وحفظ قواعد البيانات.</string>
|
||||
<string name="clipboard_error">بعض الأجهزة لا تسمح للتطبيقات باستعمال الحافظة.</string>
|
||||
<string name="clipboard_timeout">مهلة الحافظة</string>
|
||||
<string name="clipboard_timeout_summary">مدة التخزين في الحافظة(إذا كان جهازك يدعمها)</string>
|
||||
<string name="clipboard_timeout_summary">مدة التخزين في الحافظة (إذا كان جهازك يدعمها)</string>
|
||||
<string name="select_to_copy">اختر لنسخ %1$s إلى الحافظة</string>
|
||||
<string name="retrieving_db_key">يجلب مفتاح قاعدة البيانات…</string>
|
||||
<string name="default_checkbox">استخدامها كقاعدة بيانات افتراضية</string>
|
||||
<string name="html_about_licence">KeePassDX © %1$d كونزيسوفت <strong>مفتوح المصدر</strong> و <strong>بدون اعلانات</strong>.
|
||||
\n يوزع كما هو، بدون ضمان, تحت ترخيص <strong>GPLv3</strong>.</string>
|
||||
<string name="entry_accessed">نُفذ إليه</string>
|
||||
<string name="html_about_licence">KeePassDX © %1$d كونزيسوفت <strong>مفتوح المصدر</strong> و <strong>بدون إعلانات</strong>. \n يوزع كما هو، دون ضمان، تحت ترخيص <strong>GPLv3</strong>.</string>
|
||||
<string name="entry_accessed">وُصِل إليه</string>
|
||||
<string name="entry_expires">تنتهي صلاحيته في</string>
|
||||
<string name="entry_keyfile">ملف المفتاح</string>
|
||||
<string name="error_arc4">تشفير دفق Arcfour غير مدعوم.</string>
|
||||
@@ -129,7 +128,7 @@
|
||||
<string name="error_nokeyfile">اختر ملف مفتاح.</string>
|
||||
<string name="error_out_of_memory">لا ذاكرة لتحميل قاعدة البيانات كاملة.</string>
|
||||
<string name="error_load_database">تعذر تحميل قاعدة البيانات.</string>
|
||||
<string name="error_load_database_KDF_memory">لا يمكن تحميل المفتاح، حاول تقليل \"الذاكرة المستخدمة\" من قبل KDF.</string>
|
||||
<string name="error_load_database_KDF_memory">تعذر تحميل المفتاح. حاول تقليل \"الذاكرة المستخدمة\" من قِبل KDF.</string>
|
||||
<string name="error_pass_gen_type">يجب تحديد نوع واحد على الأقل لتوليد كلمة السر.</string>
|
||||
<string name="error_rounds_too_large">\"جولات التحويل\" كثيرة جداً. الإعداد إلى 2147483648.</string>
|
||||
<string name="error_string_key">يجب أن يكون لكل سلسلة اسم حقل.</string>
|
||||
@@ -141,22 +140,22 @@
|
||||
<string name="invalid_db_sig">تعذر تمييز نسق قاعدة البيانات.</string>
|
||||
<string name="keyfile_is_empty">ملف المفتاح فارغ.</string>
|
||||
<string name="list_entries_show_username_title">أظهر أسماء المستخدمين</string>
|
||||
<string name="list_entries_show_username_summary">اعرض اسماء المستخدمين في قوائم المدخلات</string>
|
||||
<string name="hint_generated_password">كلمة السر الموَلدة</string>
|
||||
<string name="hint_keyfile">الملف المفتاحي</string>
|
||||
<string name="hide_password_title">اخفاء كلمات السر</string>
|
||||
<string name="list_entries_show_username_summary">يعرض اسماء المستخدمين في قوائم المدخلات</string>
|
||||
<string name="hint_generated_password">كلمة السر مولّدة</string>
|
||||
<string name="hint_keyfile">ملف المفتاح</string>
|
||||
<string name="hide_password_title">أخفِ كلمات السر</string>
|
||||
<string name="copy_field">نُسخة من %1$s</string>
|
||||
<string name="menu_copy">نسخ</string>
|
||||
<string name="menu_move">نقل</string>
|
||||
<string name="menu_paste">لصق</string>
|
||||
<string name="menu_cancel">الغاء</string>
|
||||
<string name="menu_hide_password">اخفاء كلمة السر</string>
|
||||
<string name="menu_showpass">اظهار كلمة السر</string>
|
||||
<string name="menu_cancel">ألغِ</string>
|
||||
<string name="menu_hide_password">أخفِ كلمة السر</string>
|
||||
<string name="menu_showpass">أظهر كلمة السر</string>
|
||||
<string name="menu_url">الانتقال الى الرابط</string>
|
||||
<string name="menu_file_selection_read_only">محمي من التعديل</string>
|
||||
<string name="menu_open_file_read_and_write">قابل للتعديل</string>
|
||||
<string name="select_database_file">فتح مخزن موجود</string>
|
||||
<string name="create_keepass_file">انشاء مخزن جديد</string>
|
||||
<string name="select_database_file">افتح مخزن موجود</string>
|
||||
<string name="create_keepass_file">أنشئ مخزن جديد</string>
|
||||
<string name="progress_title">قيد العمل…</string>
|
||||
<string name="read_only_warning">KeePassDX يحتاج صلاحية الكتابة من اجل تعديل قاعدة البيانات.</string>
|
||||
<string name="encryption_explanation">خوارزمية تشفير جميع البيانات</string>
|
||||
@@ -166,7 +165,7 @@
|
||||
<string name="unavailable">غير متوفر</string>
|
||||
<string name="menu_appearance_settings">المظهر</string>
|
||||
<string name="general">عام</string>
|
||||
<string name="autofill">ملأ تلقائي</string>
|
||||
<string name="autofill">الملء التلقائي</string>
|
||||
<string name="autofill_sign_in_prompt">سجل باستخدام KeePassDX</string>
|
||||
<string name="set_autofill_service_title">تعيين خدمة الملأ التلقائي الافتراضية</string>
|
||||
<string name="password_size_title">حجم كلمة السر المولدة</string>
|
||||
@@ -178,14 +177,14 @@
|
||||
<string name="clipboard_warning">اذا فشل الحذف التلقائي من الحافظة ,احذف تأريخه يدويا.</string>
|
||||
<string name="lock_database_screen_off_title">قفل الشاشة</string>
|
||||
<string name="lock_database_screen_off_summary">اقفل قاعدة البيانات بعد بضع ثوانٍ بمجرد إيقاف تشغيل الشاشة</string>
|
||||
<string name="biometric_delete_all_key_title">حذف مفاتيح التشفير</string>
|
||||
<string name="biometric_delete_all_key_title">احذف مفاتيح التعمية</string>
|
||||
<string name="unavailable_feature_text">لا يمكن بدأ هذه الميزة .</string>
|
||||
<string name="unavailable_feature_version">هذا الجهاز يعمل بأندرويد %1$s لكن يحتاج نسخة %2$s على الأقل.</string>
|
||||
<string name="file_name">اسم الملف</string>
|
||||
<string name="path">مسار</string>
|
||||
<string name="database_history">تأريخ</string>
|
||||
<string name="clipboard_notifications_summary">أظهر اشعارات الحافظة لنسخ الحقول عند عرض مدخل</string>
|
||||
<string name="advanced_unlock">فتح الجهاز</string>
|
||||
<string name="device_unlock">فتح الجهاز</string>
|
||||
<string name="biometric_unlock_enable_title">فحص البصمة</string>
|
||||
<string name="biometric_unlock_enable_summary">يتيح لك مسح بياناتك الحيوية لفتح قاعدة البيانات</string>
|
||||
<string name="monospace_font_fields_enable_summary">غير خط الحقول لتوضيح المحارف</string>
|
||||
@@ -213,15 +212,13 @@
|
||||
<string name="keyboard_keys_category">مفاتيح</string>
|
||||
<string name="keyboard_key_vibrate_title">إهتزاز عند اللمس</string>
|
||||
<string name="keyboard_key_sound_title">صوت عند اللمس</string>
|
||||
<string name="allow_no_password_title">إسمح بدون المفتاح الرئيسي</string>
|
||||
<string name="enable_read_only_title">محمي من التعديل</string>
|
||||
<string name="enable_read_only_summary">افتح قاعدة البيانات في وضع القراءة افتراضيا</string>
|
||||
<string name="allow_no_password_title">اسمح بدون المفتاح الرئيسي</string>
|
||||
<string name="enable_education_screens_title">تلميحات تعليمية</string>
|
||||
<string name="reset_education_screens_summary">أعد عرض كل المعلومات التعليمية</string>
|
||||
<string name="reset_education_screens_text">إعادة تعيين الشاشات التلميحات</string>
|
||||
<string name="education_create_database_title">أنشئ قاعدة بيانات</string>
|
||||
<string name="education_create_database_summary">أنشئ ملف إدارة كلمات السر.</string>
|
||||
<string name="education_select_database_title">إفتح قاعدة بيانات</string>
|
||||
<string name="education_select_database_title">افتح قاعدة بيانات موجودة بالفعل</string>
|
||||
<string name="sort_recycle_bin_bottom">سلة المحذوفات في الأسفل</string>
|
||||
<string name="sort_db">ترتيب طبيعي</string>
|
||||
<string name="sort_last_access_time">الوصول</string>
|
||||
@@ -234,10 +231,10 @@
|
||||
<string name="keyboard_notification_entry_content_title">%1$s متوفر على Magikeyboard</string>
|
||||
<string name="keyboard_notification_entry_content_text">%1$s</string>
|
||||
<string name="reset_education_screens_title">إعادة تعيين التلميحات التعليمية</string>
|
||||
<string name="education_search_title">البحث من خلال الإدخالات</string>
|
||||
<string name="education_search_title">ابحث من خلال المدخلات</string>
|
||||
<string name="content_description_open_file">افتح الملف</string>
|
||||
<string name="content_description_add_entry">إضافة مدخلة</string>
|
||||
<string name="content_description_add_group">إضافة مجموعة</string>
|
||||
<string name="content_description_add_entry">أضف مدخل</string>
|
||||
<string name="content_description_add_group">أضف مجموعة</string>
|
||||
<string name="content_description_file_information">معلومات الملف</string>
|
||||
<string name="entry_password_generator">مولد كلمة السر</string>
|
||||
<string name="content_description_background">الخلفية</string>
|
||||
@@ -250,22 +247,22 @@
|
||||
<string name="do_not_kill_app">لا تقتل التطبيق…</string>
|
||||
<string name="content_description_node_children">العقد الفرعية</string>
|
||||
<string name="content_description_add_node">أضف عقدة</string>
|
||||
<string name="content_description_entry_icon">ايقونة المدخل</string>
|
||||
<string name="content_description_entry_icon">أيقونة المدخل</string>
|
||||
<string name="content_description_password_length">طول كلمة السر</string>
|
||||
<string name="entry_add_field">أضف حقل</string>
|
||||
<string name="content_description_remove_field">أزل حقل</string>
|
||||
<string name="error_move_entry_here">يتعذر نقل مدخل إلى هنا.</string>
|
||||
<string name="error_copy_entry_here">يتعذر نسخ مدخال إلى هنا.</string>
|
||||
<string name="list_groups_show_number_entries_title">عرض عدد المدخلات</string>
|
||||
<string name="list_groups_show_number_entries_summary">عرض عدد المدخلات في المجموعة</string>
|
||||
<string name="content_description_update_from_list">تحديث</string>
|
||||
<string name="error_move_entry_here">لا يمكنك نقل مدخل هنا.</string>
|
||||
<string name="error_copy_entry_here">لا يمكنك نسخ مدخل هنا.</string>
|
||||
<string name="list_groups_show_number_entries_title">أظهر عدد المدخلات</string>
|
||||
<string name="list_groups_show_number_entries_summary">يعرض عدد المدخلات في المجموعة</string>
|
||||
<string name="content_description_update_from_list">حدِّث</string>
|
||||
<string name="content_description_keyboard_close_fields">أغلق الحقول</string>
|
||||
<string name="error_create_database_file">لا يمكن انشاء قاعدة بيانات بكلمة السر وملف المفتاح الحاليين.</string>
|
||||
<string name="menu_advanced_unlock_settings">فك قفل الجهاز</string>
|
||||
<string name="error_create_database_file">تعذر إنشاء قاعدة بيانات بكلمة السر وملف المفتاح الحاليين.</string>
|
||||
<string name="menu_device_unlock_settings">فك قفل الجهاز</string>
|
||||
<string name="entry_attachments">مرفقات</string>
|
||||
<string name="entry_history">السجل</string>
|
||||
<string name="entry_add_attachment">أضف مرفقا</string>
|
||||
<string name="discard">إلغاء</string>
|
||||
<string name="entry_history">التاريخ</string>
|
||||
<string name="entry_add_attachment">أضف مرفقًا</string>
|
||||
<string name="discard">تجاهل</string>
|
||||
<string name="discard_changes">تجاهل التغييرات؟</string>
|
||||
<string name="validate">تأكيد</string>
|
||||
<string name="security">الأمان</string>
|
||||
@@ -273,27 +270,27 @@
|
||||
<string name="error_otp_period">يجب ان تكون المدة بين %1$d و%2$d ثانية.</string>
|
||||
<string name="error_otp_secret_key">المفتاح السري يجب ان يكون بصيغة Base32.</string>
|
||||
<string name="error_save_database">تعذر حفظ قاعدة البيانات.</string>
|
||||
<string name="error_create_database">لا يمكن إنشاء ملف قاعدة البيانات.</string>
|
||||
<string name="error_copy_group_here">لا يمكن نسخ مجموعة هنا.</string>
|
||||
<string name="error_create_database">تعذر إنشاء ملف قاعدة البيانات.</string>
|
||||
<string name="error_copy_group_here">لا يمكنك نسخ مجموعة هنا.</string>
|
||||
<string name="error_label_exists">هذه التسمية موجودة بالفعل.</string>
|
||||
<string name="otp_period">المدة (ثواني)</string>
|
||||
<string name="otp_algorithm">الخوارزمية</string>
|
||||
<string name="otp_digits">أرقام</string>
|
||||
<string name="otp_counter">العداد</string>
|
||||
<string name="entry_setup_otp">عيّن كلمة مرور لمرة واحدة</string>
|
||||
<string name="entry_setup_otp">عيّن كلمة سر لمرة واحدة</string>
|
||||
<string name="entry_UUID">UUID</string>
|
||||
<string name="html_about_contribution">من أجل <strong>حماية خصوصيتا</strong>٫<strong> إصلاح العلل</strong>٫ <strong>إضافة مميزات</strong> <strong>وجعلنا نشطاء دائما</strong>٫ نحن نعتمد على <strong>مساهمتك</strong>.</string>
|
||||
<string name="content_description_keyfile_checkbox">خانة تأشير الملف المفتاحي</string>
|
||||
<string name="html_about_contribution">لكي <strong>نحافظ على حريتنا</strong>، و<strong>نصلح الأخطاء</strong>، و<strong>نضيف ميزات</strong>، و<strong>نبقى دائمًا نشطين</strong>، فإننا نعتمد على <strong>مساهمتكم</strong>.</string>
|
||||
<string name="content_description_keyfile_checkbox">خانة تأشير ملف المفتاح</string>
|
||||
<string name="content_description_password_checkbox">خانة تأشير كلمة السر</string>
|
||||
<string name="content_description_add_item">أضف عنصر</string>
|
||||
<string name="warning_permanently_delete_nodes">حذف العقد المحددة نهائيا؟</string>
|
||||
<string name="filter">مرشح</string>
|
||||
<string name="command_execution">ينفذ الأمر…</string>
|
||||
<string name="hide_broken_locations_title">اِخفي روابط قواعد البيانات المعطلة</string>
|
||||
<string name="hide_broken_locations_title">أخفِ روابط قواعد البيانات المعطوبة</string>
|
||||
<string name="show_recent_files_summary">أظهر موقع قواعد البيانات الأخيرة</string>
|
||||
<string name="show_recent_files_title">أظهر الملفات الأخيرة</string>
|
||||
<string name="remember_keyfile_locations_summary">تعقب موقع الملفات المفتاحية لقاعدة البيانات</string>
|
||||
<string name="remember_keyfile_locations_title">تذكر موقع الملف المفتاحي</string>
|
||||
<string name="remember_keyfile_locations_title">تذكر موقع ملف المفتاح</string>
|
||||
<string name="remember_database_locations_summary">تعقب موقع قاعدة البيانات</string>
|
||||
<string name="remember_database_locations_title">تذكر موقع تخزين قاعدة البيانات</string>
|
||||
<string name="contains_duplicate_uuid_procedure">للمتابعة هل تريد حل المشكلة بتوليد UUID للعناصر المكررة ؟</string>
|
||||
@@ -308,48 +305,48 @@
|
||||
<string name="creating_database">ينشئ قاعدة البيانات…</string>
|
||||
<string name="error_string_type">لا يطابق هذا النص العنصر المطلوب.</string>
|
||||
<string name="error_otp_counter">على العداد أن يكون ما بين %1$d و %2$d.</string>
|
||||
<string name="entry_otp">كلمة مرور لمرة واحدة</string>
|
||||
<string name="otp_type">نوع كلمة المرور لمرة واحدة</string>
|
||||
<string name="entry_otp">كلمة سر لمرة واحدة</string>
|
||||
<string name="otp_type">نوع كلمة السر لمرة واحدة (OTP)</string>
|
||||
<string name="error_disallow_no_credentials">عين اعتماد واحد على الأقل.</string>
|
||||
<string name="contribution">ساهم</string>
|
||||
<string name="contact">الإتصال بنا</string>
|
||||
<string name="contact">التواصل</string>
|
||||
<string name="biometric">البصمة</string>
|
||||
<string name="warning_empty_keyfile_explanation">يجب ألا تغير محتوى ملف المفتاح، في أحسن الحالات يجب أن يحتوي بيانات مولدة عشوائيا.</string>
|
||||
<string name="warning_empty_keyfile">من غير المستحسن اضافة ملف مفتاح فارغ.</string>
|
||||
<string name="warning_sure_remove_data">أزل هذه البيانات عل أي حال؟</string>
|
||||
<string name="warning_sure_add_file">أأضف الملف على أي حال؟</string>
|
||||
<string name="warning_replace_file">رفعُ هذا الملف سيستبدل الموجود مسبقا.</string>
|
||||
<string name="warning_replace_file">رفع هذا الملف سيستبدل الموجود مسبقًا.</string>
|
||||
<string name="warning_database_link_revoked">أبطلَ مدير الملفات الوصول للملف</string>
|
||||
<string name="warning_database_read_only">أعط صلاحية الكتابة من أجل حفظ قاعدة البيانات</string>
|
||||
<string name="warning_password_encoding">تجنب استخدام المحارف غير الموجودة في ترميز قاعدة البيانات (تحوَّل المحارف غير الموجودة لنفس الحرف).</string>
|
||||
<string name="hide_broken_locations_summary">إخف الروابط المعطلة في قائمة قواعد البيانات الحديثة</string>
|
||||
<string name="hide_broken_locations_summary">أخفِ الروابط المعطوبة في قائمة قواعد البيانات الحديثة</string>
|
||||
<string name="auto_focus_search_summary">افتح البحث عند فتح قاعدة البيانات</string>
|
||||
<string name="content_description_credentials_information">معلومات بيانات الاعتماد</string>
|
||||
<string name="max_history_items_title">العدد الأقصى</string>
|
||||
<string name="recycle_bin_group_title">مجموعة سلة المحذوفات</string>
|
||||
<string name="recycle_bin_summary">أُنقل المجموعات والمدخلات لسلة المحذوفات قبل حذفها</string>
|
||||
<string name="database_data_remove_unlinked_attachments_summary">أزِل المرفقات غير المرتبطة بإدخال في قاعدة البيانات</string>
|
||||
<string name="database_data_remove_unlinked_attachments_summary">أزل المرفقات غير المرتبطة بمدخل في قاعدة البيانات</string>
|
||||
<string name="database_data_remove_unlinked_attachments_title">أزل البيانات غير المرتبطة</string>
|
||||
<string name="database_data_compression_summary">ضغط البيانات يقلص من حجم قاعدة البيانات</string>
|
||||
<string name="database_data_compression_title">ضغط البيانات</string>
|
||||
<string name="data">البيانات</string>
|
||||
<string name="unavailable_feature_hardware">تعذر العثور على ماسح البصمة.</string>
|
||||
<string name="biometric_delete_all_key_summary">احذف كل مفاتيح التشفير المرتبطة بفتح الجهاز</string>
|
||||
<string name="advanced_unlock_explanation_summary">استخدم إلغاء القفل الجهاز لفتح قاعدة البيانات بسهولة</string>
|
||||
<string name="biometric_delete_all_key_summary">احذف كل مفاتيح التعمية المرتبطة بفتح الجهاز</string>
|
||||
<string name="device_unlock_explanation_summary">استخدم إلغاء القفل الجهاز لفتح قاعدة البيانات بسهولة</string>
|
||||
<string name="lock_database_show_button_summary">يعرض زر القَفل في الواجهة</string>
|
||||
<string name="lock_database_show_button_title">اعرض زر القَفل</string>
|
||||
<string name="lock_database_show_button_title">أظهر زر القفل</string>
|
||||
<string name="lock_database_back_root_summary">قفل قاعدة البيانات عند النقر على زر الرجوع في الشاشة الرئيسية</string>
|
||||
<string name="lock_database_back_root_title">اضغط على \"رجوع\" للإقفال</string>
|
||||
<string name="clipboard_explanation_summary">انسخ حقول الإدخال باستخدام الحافظة</string>
|
||||
<string name="clipboard_explanation_summary">انسخ حقول المدخل باستخدام الحافظة</string>
|
||||
<string name="database_opened">قاعدة البيانات مفتوحة</string>
|
||||
<string name="autofill_preference_title">إعدادات الملء التلقائي</string>
|
||||
<string name="education_entry_edit_title">حرر المدخلة</string>
|
||||
<string name="education_advanced_unlock_summary">لفتح قاعدة البيانات بسرعة اربط كلمة المرور بالبصمة.</string>
|
||||
<string name="education_search_summary">لإيجاد كلمة المرور، أدخل العنوان أو اسم المستخدم أو محتوى أحد الحقول.</string>
|
||||
<string name="education_entry_edit_title">عدّل المدخل</string>
|
||||
<string name="education_device_unlock_summary">اربط كلمة سر ببياناتك البيومترية الممسوحة ضوئيًا أو بيانات اعتماد جهازك لفتح قفل قاعدة بياناتك بسرعة.</string>
|
||||
<string name="education_search_summary">لإيجاد كلمة السر، أدخل العنوان أو اسم المستخدم أو محتوى أحد الحقول.</string>
|
||||
<string name="education_new_node_summary">المدخلات لإدارة معرفاتك الرقمية.
|
||||
\n
|
||||
\nالمجموعات (المجلدات) لتنظيم المدخلات في قاعدة البيانات.</string>
|
||||
<string name="education_select_database_summary">لمتابعة استخدام قاعدة البيانات السابقة، افتحها من مدير الملفات.</string>
|
||||
<string name="education_select_database_summary">افتح ملف قاعدة البيانات السابق من متصفح ملفاتك لمتابعة استخدامه.</string>
|
||||
<string name="enable_auto_save_database_summary">حفظ قاعدة البيانات بعد كل إجراء (في وضع \"التعديل\")</string>
|
||||
<string name="enable_auto_save_database_title">الحفظ التلقائي لقاعدة البيانات</string>
|
||||
<string name="delete_entered_password_title">احذف كلمة السر</string>
|
||||
@@ -357,15 +354,12 @@
|
||||
<string name="autofill_web_domain_blocklist_title">قائمة النطاقات المحظورة</string>
|
||||
<string name="autofill_application_id_blocklist_summary">منع الملء التلقائي للتطبيقات الموجودة في القائمة</string>
|
||||
<string name="autofill_application_id_blocklist_title">قائمة التطبيقات المحظورة</string>
|
||||
<string name="content_description_repeat_toggle_password_visibility">بدِّل ظهور كلمة السر</string>
|
||||
<string name="hide_expired_entries_summary">لن تعرض المدخلات منتهية الصلاحية</string>
|
||||
<string name="education_read_only_summary">تغيير وضع الافتتاح للجلسة.
|
||||
\n
|
||||
\nيمنع \"محمي ضد الكتابة\" التغييرات غير المقصودة في قاعدة البيانات.
|
||||
\n\"قابل للتعديل\" يتيح لك إضافة أو حذف أو تعديل جميع العناصر كما تريد.</string>
|
||||
<string name="education_read_only_title">احمي قاعدة البيانات من التعديل</string>
|
||||
<string name="education_unlock_title">افتح قاعدة البيانات</string>
|
||||
<string name="education_add_attachment_summary">أضف مرفقا للمدخلة لحفظ بيانات اضافية.</string>
|
||||
<string name="content_description_repeat_toggle_password_visibility">أعد تبديل ظهور كلمة السر</string>
|
||||
<string name="hide_expired_entries_summary">لا يتم عرض المدخلات منتهية الصلاحية</string>
|
||||
<string name="education_read_only_summary">غيّر وضع الافتتاح للجلسة. \n \nيمنع \"محمي ضد الكتابة\" التغييرات غير المقصودة في قاعدة البيانات. \n\"قابل للتعديل\" يتيح لك إضافة أو حذف أو تعديل جميع العناصر كما تريد.</string>
|
||||
<string name="education_read_only_title">احمِ قاعدة بياناتك من التعديل</string>
|
||||
<string name="education_unlock_title">افتح قاعدة بياناتك</string>
|
||||
<string name="education_add_attachment_summary">ارفع مرفقًا إلى مدخلك لحفظ البيانات الخارجية الهامة.</string>
|
||||
<string name="education_add_attachment_title">أضف مرفقا</string>
|
||||
<string name="autofill_block">احظر الملء التلقائي</string>
|
||||
<string name="keyboard_previous_database_credentials_title">شاشة بيانات اعتماد قاعدة البيانات</string>
|
||||
@@ -402,7 +396,7 @@
|
||||
<string name="education_generate_password_title">أنشئ كلمة سر قوية</string>
|
||||
<string name="save_mode">وضع الحفظ</string>
|
||||
<string name="search_mode">وضع البحث</string>
|
||||
<string name="version">النسخة</string>
|
||||
<string name="version">النُسخة</string>
|
||||
<string name="template_group_name">النماذج</string>
|
||||
<string name="holder">الحامل</string>
|
||||
<string name="number">الرقم</string>
|
||||
@@ -410,7 +404,7 @@
|
||||
<string name="personal_identification_number">PIN</string>
|
||||
<string name="id_card">بطاقة الهوية</string>
|
||||
<string name="type">النوع</string>
|
||||
<string name="cryptocurrency">محفظة عملات مشفرة</string>
|
||||
<string name="cryptocurrency">محفظة عملات التعموية</string>
|
||||
<string name="public_key">المفتاح العمومي</string>
|
||||
<string name="private_key">المفتاح الخاص</string>
|
||||
<string name="account">الحساب</string>
|
||||
@@ -418,20 +412,20 @@
|
||||
<string name="bank_name">اسم المصرف</string>
|
||||
<string name="secure_note">ملاحظة آمنة</string>
|
||||
<string name="error_word_reserved">هذه الكلمة محجوزة ولا يمكن استخدامها.</string>
|
||||
<string name="error_field_name_already_exists">اسم الحقل موجود سلفًا.</string>
|
||||
<string name="error_file_to_big">الملف الذي ترفعه كبير.</string>
|
||||
<string name="error_field_name_already_exists">اسم الحقل موجود بالفعل.</string>
|
||||
<string name="error_file_to_big">الملف الذي تحاول رفعه كبير جدًا.</string>
|
||||
<string name="error_upload_file">حدث خطأ أثناء رفع الملف.</string>
|
||||
<string name="error_duplicate_file">بيانات الملف موجودة سلفًا.</string>
|
||||
<string name="error_duplicate_file">بيانات الملف موجودة بالفعل.</string>
|
||||
<string name="error_remove_file">حدث خطأ أثناء إزالة بيانات الملف.</string>
|
||||
<string name="error_start_database_action">حدث خطأ أثناء تنفيذ إجراء على قاعدة البيانات.</string>
|
||||
<string name="content_description_otp_information">معلومات كلمة المرور لمرة واحدة</string>
|
||||
<string name="content_description_otp_information">معلومات كلمة السر لمرة واحدة</string>
|
||||
<string name="membership">العضوية</string>
|
||||
<string name="name">الاسم</string>
|
||||
<string name="email">البريد الإلكتروني</string>
|
||||
<string name="email_address">عنوان البريد الإلكتروني</string>
|
||||
<string name="ssid">SSID</string>
|
||||
<string name="debit_credit_card">بطاقة السحب الفوري / الإئتمان</string>
|
||||
<string name="error_registration_read_only">لا يمكن حفظ عنصر في قاعدة بيانات مفتوحة للقراءة فقط</string>
|
||||
<string name="error_registration_read_only">لا يمكن حفظ عنصر في قاعدة بيانات مفتوحة للقراءة فقط.</string>
|
||||
<string name="otp_secret">الرمز السري</string>
|
||||
<string name="place_of_issue">مكان المشكلة</string>
|
||||
<string name="date_of_issue">تاريخ المشكلة</string>
|
||||
@@ -452,8 +446,8 @@
|
||||
<string name="properties">الخصائص</string>
|
||||
<string name="token">الرمز</string>
|
||||
<string name="seed">البذرة</string>
|
||||
<string name="error_database_uri_null">يتعذر استرداد مسار قاعدة البيانات.</string>
|
||||
<string name="error_rebuild_list">يتعذر إعادة بناء القائمة بشكل صحيح.</string>
|
||||
<string name="error_database_uri_null">لا يمكن استرداد URI قاعدة البيانات.</string>
|
||||
<string name="error_rebuild_list">تعذر إعادة بناء القائمة بشكل صحيح.</string>
|
||||
<string name="menu_keystore_remove_key">احذف رمز فك القفل الجهاز</string>
|
||||
<string name="menu_form_filling_settings">ملء النموذج</string>
|
||||
<string name="menu_reload_database">أعد تحميل البيانات</string>
|
||||
@@ -463,17 +457,17 @@
|
||||
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
|
||||
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string>
|
||||
<string name="export_app_properties_summary">أنشئ ملفًا لتصدير إعدادات التطبيق</string>
|
||||
<string name="error_import_app_properties">خطأ أثناء استيراد إعدادات التطبيق</string>
|
||||
<string name="error_export_app_properties">خطأ أثناء تصدير إعدادات التطبيق</string>
|
||||
<string name="error_import_app_properties">خطأ أثناء استيراد إعدادات التطبيق.</string>
|
||||
<string name="error_export_app_properties">خطأ أثناء تصدير إعدادات التطبيق.</string>
|
||||
<string name="warning_database_info_changed">غُيِّرت معلومات قاعدة البيانات من خارج هذا التطبيق.</string>
|
||||
<string name="warning_database_info_changed_options">ادمج البيانات أو استبدل التعديلات الخارجية بحفظ قاعدة البيانات أو أعد تحميلها لجلب آخر التغييرات.</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">اكتب كلمة السر، وأنقر هذا الزر.</string>
|
||||
<string name="credential_before_click_device_unlock_button">اكتب كلمة السر، وأنقر هذا الزر.</string>
|
||||
<string name="device_credential">بيانات الاعتماد للجهاز</string>
|
||||
<string name="advanced_unlock_tap_delete">انقر لحذف مفاتيح فتح الجهاز</string>
|
||||
<string name="device_unlock_tap_delete">انقر لحذف مفاتيح فتح الجهاز</string>
|
||||
<string name="keyboard_auto_go_action_title">إجراء اللمس التلقائي</string>
|
||||
<string name="keyboard_previous_fill_in_title">العودة إلى الوراء</string>
|
||||
<string name="keyboard_previous_lock_title">اقفل قاعدة البيانات</string>
|
||||
<string name="education_advanced_unlock_title">فتح قاعدة بيانات الجهاز</string>
|
||||
<string name="education_device_unlock_title">فتح قاعدة بيانات الجهاز</string>
|
||||
<string name="hint_icon_name">اسم الأيقونة</string>
|
||||
<string name="autofill_manual_selection_title">اختيار يدوي</string>
|
||||
<string name="description_app_properties">خصائص KeePassDX لإدارة إعدادات التطبيقات</string>
|
||||
@@ -482,7 +476,7 @@
|
||||
<string name="autofill_ask_to_save_data_title">اسأل لحفظ البيانات</string>
|
||||
<string name="content_description_database_color">لون قاعدة البيانات</string>
|
||||
<string name="menu_merge_from">ادمج من…</string>
|
||||
<string name="show_uuid_summary">يعرض \"المعرف العام\" المرتبط بمُدخل او بمجموعة</string>
|
||||
<string name="show_uuid_summary">يعرض UUID المرتبط بمدخل أو بمجموعة</string>
|
||||
<string name="expired">انتهت</string>
|
||||
<string name="tags">الوسوم</string>
|
||||
<string name="menu_merge_database">ادمج البيانات</string>
|
||||
@@ -491,10 +485,10 @@
|
||||
<string name="warning_file_too_big">يفترض بقاعدة البيانات أن تحوي ملفات صغيرة الحجم ( كمفاتيح PGP).
|
||||
\n
|
||||
\nبرفع هذا الملف قد يزداد حجم قاعدة البيانات ويضعف أداءها.</string>
|
||||
<string name="error_move_group_here">يتعذر نقل المجموعة إلى هنا.</string>
|
||||
<string name="error_move_group_here">لا يمكنك نقل مجموعة هنا.</string>
|
||||
<string name="menu_save_copy_to">احفظ نسخة إلى…</string>
|
||||
<string name="searchable">يمكن البحث عنه</string>
|
||||
<string name="custom_data">بيانات مخصصة</string>
|
||||
<string name="custom_data">بيانات مخصّصة</string>
|
||||
<string name="case_sensitive">حساسة لحالة الأحرف</string>
|
||||
<string name="regex">تعابير نمطية</string>
|
||||
<string name="enable_keep_screen_on_title">أبقِ الشاشة شغّالة</string>
|
||||
@@ -510,42 +504,42 @@
|
||||
<string name="autofill_close_database_summary">أغلق قاعدة البيانات بعد الملء التلقائي</string>
|
||||
<string name="autofill_ask_to_save_data_summary">اسأل عن حفظ البيانات عند ملئك لنموذج</string>
|
||||
<string name="templates_group_uuid_title">مجموعة القوالب</string>
|
||||
<string name="advanced_unlock_timeout">انتهت مهلة فتح الجهاز</string>
|
||||
<string name="temp_advanced_unlock_timeout_summary">مهلة استخدام فتح الجهاز قبل حذف محتواها</string>
|
||||
<string name="advanced_unlock_delete_all_key_warning">أتريد حذف كل مفاتيح التشفير المرتبطة بفتح الجهاز؟</string>
|
||||
<string name="device_unlock_timeout">انتهت مهلة فتح الجهاز</string>
|
||||
<string name="temp_device_unlock_timeout_summary">مهلة استخدام فتح الجهاز قبل حذف محتواها</string>
|
||||
<string name="device_unlock_delete_all_key_warning">أتريد حذف كل مفاتيح التعمية المرتبطة بفتح الجهاز؟</string>
|
||||
<string name="templates">القوالب</string>
|
||||
<string name="templates_group_enable_title">استخدام القوالب</string>
|
||||
<string name="notification">الإشعارات</string>
|
||||
<string name="temp_advanced_unlock_enable_summary">لا تقم بتخزين أي محتوى مشفر لاستخدام إلغاء قفل الجهاز</string>
|
||||
<string name="temp_advanced_unlock_timeout_title">انتهاء صلاحية فتح الحهاز</string>
|
||||
<string name="hide_expired_entries_title">إخفاء الإدخالات منتهية الصلاحية</string>
|
||||
<string name="content_description_hardware_key_checkbox">خانة إختيار مفتاح الجهاز</string>
|
||||
<string name="content_description_passphrase_word_count">عدد عبارات المرور</string>
|
||||
<string name="temp_device_unlock_enable_summary">لا تقم بتخزين أي محتوى مشفر لاستخدام إلغاء قفل الجهاز</string>
|
||||
<string name="temp_device_unlock_timeout_title">انتهاء صلاحية فتح الحهاز</string>
|
||||
<string name="hide_expired_entries_title">أخفِ المدخلات منتهية الصلاحية</string>
|
||||
<string name="content_description_hardware_key_checkbox">خانة إختيار مفتاح العتاد</string>
|
||||
<string name="content_description_passphrase_word_count">عدد عبارات السر</string>
|
||||
<string name="content_description_entry_background_color">لون خلفية المدخل</string>
|
||||
<string name="passphrase">عبارة المرور</string>
|
||||
<string name="colorize_password_title">تلوين كلمات المرور</string>
|
||||
<string name="passphrase">عبارة السر</string>
|
||||
<string name="colorize_password_title">لوّن كلمات السر</string>
|
||||
<string name="permission">الإذن</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">تعذر تهيئة موجه إلغاء قفل الجهاز.</string>
|
||||
<string name="device_unlock_prompt_not_initialized">تعذر تهيئة موجه إلغاء قفل الجهاز.</string>
|
||||
<string name="biometric_security_update_required">مطلوب تحديث أمان المقاييس الحيوية.</string>
|
||||
<string name="advanced_unlock_not_recognized">تعذر التعرف على طباعة فتح الجهاز</string>
|
||||
<string name="temp_advanced_unlock_enable_title">فتح جهاز مؤقت</string>
|
||||
<string name="device_unlock_not_recognized">تعذر التعرف على طباعة فتح الجهاز</string>
|
||||
<string name="temp_device_unlock_enable_title">فتح جهاز مؤقت</string>
|
||||
<string name="autofill_inline_suggestions_title">اقتراحات مضمنة</string>
|
||||
<string name="education_setup_OTP_summary">قم بإعداد إدارة كلمات المرور لمرة واحدة (HOTP / TOTP) لإنشاء رمز مميز مطلوب للمصادقة الثنائية (2FA).</string>
|
||||
<string name="education_setup_OTP_summary">أعِدّ إدارة كلمات السر لمرة واحدة (HOTP / TOTP) لإنشاء رمز مميز مطلوب لاستيثاق الثنائي (2FA).</string>
|
||||
<string name="education_field_copy_summary">يمكن لصق الحقول المنسوخة في أي مكان.
|
||||
\n
|
||||
\nاستخدم طريقة ملء النموذج التي تفضلها.</string>
|
||||
<string name="html_text_dev_feature_work_hard">نحن نعمل بجد لإصدار هذه الميزة بسرعة.</string>
|
||||
<string name="autofill_inline_suggestions_summary">حاول عرض اقتراحات الملء التلقائي مباشرة من لوحة مفاتيح متوافقة</string>
|
||||
<string name="delete_entered_password_summary">يحذف كلمة المرور التي تم إدخالها بعد محاولة الاتصال بقاعدة البيانات</string>
|
||||
<string name="education_lock_summary">اقفل قاعدةبياناتك بسرعة، يمكنك إعداد التطبيق لقفلها بعد فترة، وعند إيقاف تشغيل الشاشة.</string>
|
||||
<string name="delete_entered_password_summary">يحذف كلمة السر التي أُدخلت بعد محاولة الاتصال بقاعدة البيانات</string>
|
||||
<string name="education_lock_summary">اقفل قاعدة بياناتك بسرعة، يمكنك إعداد التطبيق لقفلها بعد فترة، وعند إيقاف تشغيل الشاشة.</string>
|
||||
<string name="education_sort_title">فرز العنصر</string>
|
||||
<string name="contribute">ساهِم</string>
|
||||
<string name="upload_attachment">رفع %1$s</string>
|
||||
<string name="download_canceled">ألغيت!</string>
|
||||
<string name="unit_kibibyte">كيلو بايت</string>
|
||||
<string name="unit_mebibyte">ميغا بايت</string>
|
||||
<string name="unit_gibibyte">جيجابت</string>
|
||||
<string name="entropy">إنتروبيا: %1$s بت</string>
|
||||
<string name="upload_attachment">ارفع %1$s</string>
|
||||
<string name="download_canceled">أُلغِيَ!</string>
|
||||
<string name="unit_kibibyte">ك.بايت</string>
|
||||
<string name="unit_mebibyte">م.بايت</string>
|
||||
<string name="unit_gibibyte">ج.بايت</string>
|
||||
<string name="entropy">الانتروبيا: %1$s بت</string>
|
||||
<string name="entropy_high">الانتروبيا: مرتفع</string>
|
||||
<string name="entropy_calculate">الانتروبيا: احسب…</string>
|
||||
<string name="exclude_ambiguous_chars">استبعاد الأحرف الغامضة</string>
|
||||
@@ -557,7 +551,7 @@
|
||||
<string name="title_case">حالة العنوان</string>
|
||||
<string name="character_count">عدد الأحرف: %1$d</string>
|
||||
<string name="style_choose_summary">السمة المستخدمة في التطبيق</string>
|
||||
<string name="show_entry_colors_summary">يعرض ألوان المقدمة والخلفية لإدخال</string>
|
||||
<string name="show_entry_colors_summary">يعرض ألوان المقدمة والخلفية للمدخل</string>
|
||||
<string name="icon_pack_choose_summary">حزمة الأيقونات المستخدمة في التطبيق</string>
|
||||
<string name="show_entry_colors_title">ألوان الدخول</string>
|
||||
<string name="device_credential_unlock_enable_title">فتح بيانات اعتماد الجهاز</string>
|
||||
@@ -569,21 +563,21 @@
|
||||
<string name="download_finalization">جارِ الانتهاء…</string>
|
||||
<string name="download_complete">مكتمل!</string>
|
||||
<string name="unit_byte">B</string>
|
||||
<string name="icon_section_custom">مُخصص</string>
|
||||
<string name="icon_section_custom">مخصّص</string>
|
||||
<string name="content_description_entry_foreground_color">لون مقدمة الدخول</string>
|
||||
<string name="keyboard_previous_fill_in_summary">العودة تلقائيًا إلى لوحة المفاتيح السابقة بعد تنفيذ \"إجراء المفتاح التلقائي\"</string>
|
||||
<string name="download_attachment">تثبيت %1$s</string>
|
||||
<string name="download_attachment">ثبِّت %1$s</string>
|
||||
<string name="html_about_privacy"><strong> لا يتم استرداد أي بيانات مستخدم</strong>، هذا التطبيق لا يتصل بأي خادم، ويعمل محليًا فقط ويحترم خصوصية المستخدمين تمامًا.</string>
|
||||
<string name="error_cancel_by_user">ألغى المستخدم.</string>
|
||||
<string name="show_otp_token_title">إظهار رمز \"الاقتران لمرة واحدة\" OTP</string>
|
||||
<string name="show_otp_token_summary">إظهار رموز\"الاقتران لمرة واحدة\" في قائمة المدخلات</string>
|
||||
<string name="error_cancel_by_user">أُلغِيَ بواسطة المستخدم.</string>
|
||||
<string name="show_otp_token_title">أظهر رمز OTP</string>
|
||||
<string name="show_otp_token_summary">يعرض رموز OTP في قائمة المدخلات</string>
|
||||
<string name="warning_database_already_opened">قاعدة البيانات مفتوحة بالفعل، أغلقها أولاً لفتح قاعدة البيانات الجديدة</string>
|
||||
<string name="warning_database_info_reloaded">ستؤدي إعادة تحميل قاعدة البيانات إلى حذف البيانات المعدلة محليًا.</string>
|
||||
<string name="templates_group_enable_summary">استخدم القوالب الديناميكية لملء حقول الإدخال</string>
|
||||
<string name="templates_group_enable_summary">استخدم القوالب الديناميكية لملء حقول المدخل</string>
|
||||
<string name="keyboard_auto_go_action_summary">إجراء مفتاح \"Go\" بعد الضغط على مفتاح \"Field\"</string>
|
||||
<string name="allow_no_password_summary">يسمح بالنقر فوق الزر \"فتح\" إذا لم يتم تحديد بيانات اعتماد</string>
|
||||
<string name="education_generate_password_summary">أنشئ كلمة مرور قوية لربطها بإدخالك، وحددها بسهولة وفقًا لمعايير النموذج ولا تنس كلمة المرور الآمنة.</string>
|
||||
<string name="education_setup_OTP_title">قم بإعداد OTP</string>
|
||||
<string name="education_generate_password_summary">أنشئ كلمة سر قوية لربطها بإدخالك، وحددها بسهولة وفقًا لمعايير النموذج ولا تنسَ كلمة السر الآمنة.</string>
|
||||
<string name="education_setup_OTP_title">أعِدّ OTP</string>
|
||||
<string name="style_brightness_title">سطوع السمة</string>
|
||||
<string name="word_separator">الفاصل</string>
|
||||
<string name="screenshot_mode_banner_text">وضع لقطة الشاشة</string>
|
||||
@@ -597,59 +591,58 @@
|
||||
<string name="kdf_explanation">لإنشاء مفتاح خوارزمية التشفير، يتحول المفتاح الرئيسي باستخدام وظيفة اشتقاق مفتاح مملح عشوائيًا.</string>
|
||||
<string name="html_text_dev_feature_buy_pro">بشراء الإصدار <strong> pro </strong>،</string>
|
||||
<string name="auto_type">كتابة تلقائيًا</string>
|
||||
<string name="hardware_key">مفتاح الجهاز</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_title">رابط لفتح الجهاز</string>
|
||||
<string name="hardware_key">مفتاح العتاد</string>
|
||||
<string name="device_unlock_prompt_store_credential_title">رابط لفتح الجهاز</string>
|
||||
<string name="backspace">فراغ للخلف</string>
|
||||
<string name="enter">دخول</string>
|
||||
<string name="education_sort_summary">اختر كيفية فرز الإدخالات والمجموعات.</string>
|
||||
<string name="education_sort_summary">اختر كيفية فرز المدخلات والمجموعات.</string>
|
||||
<string name="html_text_feature_generosity">هذا <strong> النمط المرئي</strong> متاح بفضل كرمك.</string>
|
||||
<string name="info">المعلومات</string>
|
||||
<string name="waiting_challenge_response">في انتظار استجابة التحدي…</string>
|
||||
<string name="bank_identifier_code">SWIFT / BIC</string>
|
||||
<string name="international_bank_account_number">IBAN</string>
|
||||
<string name="error_no_hardware_key">حدد مفتاح الجهاز.</string>
|
||||
<string name="colorize_password_summary">تلوين أحرف كلمة المرور حسب النوع</string>
|
||||
<string name="enable_keep_screen_on_summary">استمر في تشغيل الشاشة عند مشاهدة إدخال أو تعديله</string>
|
||||
<string name="error_no_hardware_key">حدّد مفتاح العتاد.</string>
|
||||
<string name="colorize_password_summary">لوّن أحرف كلمة السر حسب النوع</string>
|
||||
<string name="enable_keep_screen_on_summary">استمر في تشغيل الشاشة عند مشاهدة مدخل أو تعديله</string>
|
||||
<string name="enable_screenshot_mode_title">وضع لقطة الشاشة</string>
|
||||
<string name="navigation_drawer_open">درج التنقل مفتوح</string>
|
||||
<string name="waiting_challenge_request">في انتظار طلب التحدي…</string>
|
||||
<string name="navigation_drawer_close">درج التنقل مقفول</string>
|
||||
<string name="error_XML_malformed">XML تالف.</string>
|
||||
<string name="error_otp_type">لم يتم التعرف على نوع OTP الحالي من خلال هذا النموذج، وقد لا يؤدي التحقق من صحته إلى إنشاء الرمز المميز بشكل صحيح.</string>
|
||||
<string name="error_challenge_already_requested">التحدي مطلوب بالفعل</string>
|
||||
<string name="error_challenge_already_requested">التحدي طُلَب بالفعل.</string>
|
||||
<string name="error_response_already_provided">تقدم الرد بالفعل.</string>
|
||||
<string name="error_no_response_from_challenge">غير قادر على الحصول على رد من التحدي.</string>
|
||||
<string name="error_driver_required">مطلوب تعريف لـ%1$s.</string>
|
||||
<string name="error_unable_merge_database_kdb">غير قادر على الدمج مع ملف قاعدة بيانات kdb</string>
|
||||
<string name="error_unable_merge_database_kdb">غير قادر على الدمج مع ملف قاعدة بيانات kdb.</string>
|
||||
<string name="error_location_unknown">موقع قاعدة البيانات غير معروف، لا يمكن تنفيذ إجراء قاعدة البيانات.</string>
|
||||
<string name="menu_advanced_unlock_settings_summary">القياس الحيوي، بيانات اعتماد الجهاز</string>
|
||||
<string name="menu_device_unlock_settings_summary">القياس الحيوي، بيانات اعتماد الجهاز</string>
|
||||
<string name="menu_database_settings_summary">البيانات الوصفية، سلة المحذوفات، القوالب، التاريخ</string>
|
||||
<string name="menu_security_settings_summary">التشفير، وظيفة اشتقاق المفتاح</string>
|
||||
<string name="error_hardware_key_unsupported">مفتاح الجهاز غير مدعوم.</string>
|
||||
<string name="error_hardware_key_unsupported">مفتاح العتاد غير مدعوم.</string>
|
||||
<string name="master_key_settings_summary">التغيير والتجديد</string>
|
||||
<string name="error_empty_key">لا يمكن أن يكون المفتاح فارغًا.</string>
|
||||
<string name="corrupted_file">ملف تالف.</string>
|
||||
<string name="warning_keyfile_integrity">لا يتم ضمان تجزئة الملف لأن Android يمكنه تغيير بياناته بسرعة. قم بتغيير امتداد الملف إلى bin. من أجل التكامل الصحيح.</string>
|
||||
<string name="invalid_db_same_uuid">%1$s بنفس UUID %2$s موجود بالفعل.</string>
|
||||
<string name="remember_hardware_key_title">تذكر مفاتيح الأجهزة</string>
|
||||
<string name="remember_hardware_key_title">تذكر مفاتيح العتاد</string>
|
||||
<string name="warning_exact_alarm">لم تسمح للتطبيق باستخدام منبه دقيق. نتيجة لذلك، لن يتم تنفيذ الميزات التي تتطلب مؤقتًا في وقت محدد.</string>
|
||||
<string name="remember_hardware_key_summary">يتتبع مفاتيح الأجهزة المستخدمة</string>
|
||||
<string name="remember_hardware_key_summary">يتتبع مفاتيح العتاد المستخدمة</string>
|
||||
<string name="warning_database_notification_permission">يسمح لك إذن الإشعار بعرض حالة قاعدة البيانات وقفلها باستخدام زر يسهل الوصول إليه.
|
||||
\n
|
||||
\nإذا لم تنشط هذا الإذن، فلن تكون قاعدة البيانات المفتوحة في الخلفية مرئية إذا كان هناك تطبيق آخر في المقدمة.</string>
|
||||
<string name="warning_copy_permission">مطلوب إذن الإشعار لاستخدام ميزة إشعار الحافظة.</string>
|
||||
<string name="merge_success">اكتمل الدمج بنجاح</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_message">ما زلت بحاجة إلى تذكر بيانات الاعتماد الرئيسية في مخزنك إذا كنت تستخدم التعرف على فتح الجهاز.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">التعرف على فتح الجهاز</string>
|
||||
<string name="device_unlock_prompt_store_credential_message">ما زلت بحاجة إلى تذكر بيانات الاعتماد الرئيسية في مخزنك إذا كنت تستخدم التعرّف على فتح الجهاز.</string>
|
||||
<string name="device_unlock_prompt_extract_credential_title">التعرف على فتح الجهاز</string>
|
||||
<string name="later">لاحقًا</string>
|
||||
<string name="configure">تضبيط</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">استخراج بيانات اعتماد قاعدة البيانات مع بيانات فتح الجهاز</string>
|
||||
<string name="device_unlock_prompt_extract_credential_message">استخراج بيانات اعتماد قاعدة البيانات مع بيانات فتح الجهاز</string>
|
||||
<string name="ask">إسأل</string>
|
||||
<string name="configure_biometric">لم تسجل بيانات اعتماد المقاييس الحيوية أو الجهاز.</string>
|
||||
<string name="show_uuid_title">إظهار \"المعرف العام المميز\" UUID</string>
|
||||
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
|
||||
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
|
||||
<string name="advanced_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
|
||||
<string name="advanced_unlock_scanning_error">خطأ في فتح الجهاز: %1$s</string>
|
||||
<string name="device_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
|
||||
<string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string>
|
||||
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
|
||||
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>
|
||||
@@ -657,37 +650,33 @@
|
||||
<string name="unlock">فتح</string>
|
||||
<string name="menu_app_settings_summary">البحث، القفل، التاريخ، الخصائص</string>
|
||||
<string name="menu_form_filling_settings_summary">لوحة المفاتيح، الملء التلقائي، الحافظة</string>
|
||||
<string name="advanced_unlock_keystore_warning">ستقوم هذه الميزة بتخزين بيانات الاعتماد المشفرة في KeyStore الآمن بجهازك.
|
||||
<string name="device_unlock_keystore_warning">ستقوم هذه الميزة بتخزين بيانات الاعتماد المشفرة في KeyStore الآمن بجهازك.
|
||||
\n
|
||||
\nاعتمادًا على تطبيق API الأصلي لنظام التشغيل، قد لا يعمل بكامل طاقته.
|
||||
\n
|
||||
\nتحقق من توافق وأمن KeyStore مع الشركة المصنعة لجهازك ومنشئ ROM الذي تستخدمه.</string>
|
||||
<string name="keyboard_selection_entry_summary">عند عرض إدخال في KeePassDX، عبئ Magikeyboard بهذا الإدخال</string>
|
||||
<string name="keyboard_selection_entry_summary">عند عرض مدخل في KeePassDX، عبئ Magikeyboard بهذا المدخل</string>
|
||||
<string name="enable_screenshot_mode_summary">اسمح لتطبيقات الطرف الثالث بتسجيل أو التقاط لقطات شاشة للتطبيق</string>
|
||||
<string name="keyboard_save_search_info_summary">حاول حفظ المعلومات المشتركة عند إجراء اختيار إدخال يدوي لاستخدامات مستقبلية أسهل</string>
|
||||
<string name="education_entry_edit_summary">تحرير الإدخال الخاص بك مع الحقول المخصصة. يمكن الرجوع إلى بيانات التجمع بين حقول الإدخال المختلفة.</string>
|
||||
<string name="education_validate_entry_title">تحقق من صحة الإدخال</string>
|
||||
<string name="education_validate_entry_summary">تذكر التحقق من صحة الإدخال الخاص بك وحفظ قاعدة البيانات الخاصة بك.
|
||||
\n
|
||||
\nإذا تم تنشيط القفل التلقائي ونسيت أنك تجري تعديلاً، فإنك تخاطر بفقدان بياناتك.</string>
|
||||
<string name="education_entry_new_field_summary">قم بتسجيل حقل إضافي، أضف قيمة وقم بحمايته بشكل اختياري.</string>
|
||||
<string name="education_unlock_summary">أدخل كلمة المرور و/أو ملف المفتاح لفتح قاعدة بياناتك.
|
||||
\n
|
||||
\nقم بعمل نسخة احتياطية من ملف قاعدة البيانات في مكان آمن بعد كل تغيير.</string>
|
||||
<string name="keyboard_save_search_info_summary">حاول حفظ المعلومات المشتركة عند إجراء اختيار مدخل يدوي لاستخدامات مستقبلية أسهل</string>
|
||||
<string name="education_entry_edit_summary">عدّل إدخالك مع الحقول المخصّصة. يمكن الرجوع إلى بيانات التجمع بين حقول مدخل المختلفة.</string>
|
||||
<string name="education_validate_entry_title">تحقق من صحة المدخل</string>
|
||||
<string name="education_validate_entry_summary">تذكر التحقق من صحة إدخالك وحفظ قاعدة بياناتك. \n \nإذا القفل التلقائي مُنشّط ونسيت أنك تجري تعديلاً، فإنك تخاطر بفقدان بياناتك.</string>
|
||||
<string name="education_entry_new_field_summary">سجِّل حقلاً إضافيًا، أضف قيمة، واحمها اختياريًا.</string>
|
||||
<string name="education_unlock_summary">أدخل كلمة السر و/أو ملف المفتاح لفتح قاعدة بياناتك. \n \nاحتفظ بنسخة احتياطية من ملف قاعدة البيانات في مكان آمن بعد كل تغيير.</string>
|
||||
<string name="html_text_dev_feature_thanks">شكرا جزيلا لمساهمتك.</string>
|
||||
<string name="at_least_one_char">على الأقل حرف واحد من كل منهما</string>
|
||||
<string name="html_text_dev_feature_upgrade">تذكر أن تحافظ على تحديث تطبيقك عن طريق تثبيت إصدارات جديدة.</string>
|
||||
<string name="download">تثبيت</string>
|
||||
<string name="html_text_ad_free">على عكس العديد من تطبيقات إدارة كلمات المرور ، فإن هذا التطبيق <strong>بدون إعلانات</strong>، و <strong>برنامج حر متروك الحقوق</strong> ولا يجمع البيانات الشخصية على خوادمه، بغض النظر عن الإصدار الذي تستخدمه.</string>
|
||||
<string name="html_text_ad_free">على عكس العديد من تطبيقات إدارة كلمات السر، هذا التطبيق <strong>خالٍ من الإعلانات</strong>، و<strong>برنامج حر متروك الحقوق</strong> ولا يجمع بيانات شخصية على خوادمه، بغض النظر عن الإصدار الذي تستخدمه.</string>
|
||||
<string name="download_initialization">جارِ التهيئة…</string>
|
||||
<string name="download_progression">قيد التقدم: %1$d%%</string>
|
||||
<string name="html_text_buy_pro">بشراء الإصدار الاحترافي، ستتمتع بإمكانية الوصول إلى هذا <strong> النمط المرئي</strong> وستساعد بشكل خاص في <strong> تنفيذ مشروعات المجتمع. </strong></string>
|
||||
<string name="html_text_donation">من أجل الحفاظ على حريتنا ولكي نكون نشيطين دائمًا، فإننا نعتمد على <strong> مساهمتك.</strong></string>
|
||||
<string name="html_text_donation">من خلال <strong>المساهمة</strong> في المشروع <i>(مالياً أو برمجياً أو ترجمة)</i>، ستساعده على الاستمرار في الحياة والازدهار، وستكون مؤهلاً أيضاً لإجراء فتح <strong>السمة</strong>.</string>
|
||||
<string name="html_text_dev_feature_encourage">أنت تشجع المطورين على إنشاء <strong> ميزات جديدة</strong> و <strong> إصلاح الخلل</strong> وفقًا لملاحظاتك.</string>
|
||||
<string name="style_name_forest">غابة</string>
|
||||
<string name="style_name_simple">بسيط</string>
|
||||
<string name="style_name_moon">قمر</string>
|
||||
<string name="style_name_divine">إلهي</string>
|
||||
<string name="style_name_divine">فائق</string>
|
||||
<string name="style_name_classic">كلاسيكي</string>
|
||||
<string name="style_name_dark">داكن</string>
|
||||
<string name="style_name_reply">رد</string>
|
||||
@@ -696,4 +685,12 @@
|
||||
<string name="style_name_kunzite">الكونزيت</string>
|
||||
<string name="style_name_follow_system">اتبع النظام</string>
|
||||
<string name="style_name_light">فاتح</string>
|
||||
</resources>
|
||||
<string name="hide_templates_summary">لا يتم عرض القوالب</string>
|
||||
<string name="generate_keyfile">ولّد ملف مفتاح</string>
|
||||
<string name="nodes">العُقد</string>
|
||||
<string name="recursive_number_entries_title">عدد متكرر من المدخلات</string>
|
||||
<string name="recursive_number_entries_summary">يحسب بشكل متكرر عدد المدخلات في المجموعة</string>
|
||||
<string name="warning_large_keyfile">لا يُنصح بإضافة ملف مفتاحي كبير، فقد يؤدي هذا إلى منع فتح قاعدة البيانات.</string>
|
||||
<string name="hide_templates_title">أخفِ القوالب</string>
|
||||
<string name="error_otp_secret_length">يجب أن يتكوّن المفتاح السري من %1$d أحرف على الأقل.</string>
|
||||
</resources>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<string name="error_rounds_too_large">\"Transformasiya mərhələləri\" çox yüksəkdir. 2147483648-ə təyin edin.</string>
|
||||
<string name="error_save_database">Malumat bazasını yadda saxlamaq mümkün olmadı.</string>
|
||||
<string name="error_otp_secret_key">Məxfi söz Base32 formatında olmalıdır.</string>
|
||||
<string name="error_challenge_already_requested">Doğrulama artıq istənilib</string>
|
||||
<string name="error_challenge_already_requested">Doğrulama artıq istənilib.</string>
|
||||
<string name="error_response_already_provided">Artıq cavab verilib.</string>
|
||||
<string name="error_location_unknown">Məlumat bazasının yeri məlum deyil, məlumat bazası funksiyası yerinə yetirilə bilməz.</string>
|
||||
<string name="error_empty_key">Açar bölməsi boş saxlanıla bilməz.</string>
|
||||
@@ -129,8 +129,8 @@
|
||||
<string name="copy_field">%1$s nüsxələndi</string>
|
||||
<string name="menu_form_filling_settings">Forum doldurma</string>
|
||||
<string name="menu_form_filling_settings_summary">Klaviatura, avtomatik douldurma, mübadilə buferi</string>
|
||||
<string name="menu_advanced_unlock_settings">Cihaz kilidini aç</string>
|
||||
<string name="menu_advanced_unlock_settings_summary">Biometrik, cihaz şəxsiyyətini təyin edən məlumatlar</string>
|
||||
<string name="menu_device_unlock_settings">Cihaz kilidini aç</string>
|
||||
<string name="menu_device_unlock_settings_summary">Biometrik, cihaz şəxsiyyətini təyin edən məlumatlar</string>
|
||||
<string name="menu_security_settings_summary">Şifrələmə, açar yaratma funskiyası</string>
|
||||
<string name="menu_master_key_settings">Ana açar parametrləri</string>
|
||||
<string name="settings">Parametrlər</string>
|
||||
@@ -183,8 +183,8 @@
|
||||
<string name="underline">Altdan xətt</string>
|
||||
<string name="uppercase">Böyük hərf</string>
|
||||
<string name="warning">Xəbərdarlıq</string>
|
||||
<string name="error_import_app_properties">Tətbiqin parametrlərinin idxalı zamanı xəta baş verdi</string>
|
||||
<string name="error_export_app_properties">Tətbiqin parametrləri idxal edilən zaman xəta baş verdi</string>
|
||||
<string name="error_import_app_properties">Tətbiqin parametrlərinin idxalı zamanı xəta baş verdi.</string>
|
||||
<string name="error_export_app_properties">Tətbiqin parametrləri idxal edilən zaman xəta baş verdi.</string>
|
||||
<string name="root">Kök</string>
|
||||
<string name="sort_menu">Çeşidlə</string>
|
||||
<string name="sort_username">İstifadəçi adı</string>
|
||||
@@ -197,9 +197,8 @@
|
||||
<string name="ask">Soruş</string>
|
||||
<string name="configure">Kofiqurasiya et</string>
|
||||
<string name="keystore_not_accessible">Açar ehtiyyatı düzgün formada başladılmadı.</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_title">Cihaz kilidini açma linki</string>
|
||||
<string name="device_unlock_prompt_store_credential_title">Cihaz kilidini açma linki</string>
|
||||
<string name="database_history">Tarixçə</string>
|
||||
<string name="advanced_unlock_scanning_error">Cihaz kilidini açma xətası: %1$s</string>
|
||||
<string name="warning_database_info_reloaded">Məlumat bazasını yenidən yükləmək lokal olaraq modifikasiya olunmuş faylları siləcəkdir.</string>
|
||||
<string name="warning_database_revoked">Fayla giriş fayl meneceri tərəfindən ləğv edildi, məlumat bazasını bağlayın və onu olduğu yerdən yenidən açın.</string>
|
||||
<string name="warning_exact_alarm">Siz tətəbiqin zəngli saatdan istifadə etməsinə icazə verməmisiniz. Nəticədə, taymer tələb edən funksiyalar dəqiq bir zamanda işləməyəckdir.</string>
|
||||
@@ -210,11 +209,11 @@
|
||||
<string name="configure_biometric">Biometrik və ya cihaz şəxsiyyəti ilə doğrulama məlumatları tapılmadı.</string>
|
||||
<string name="biometric_security_update_required">Biometrik təhlükəsizlik yenilənməsi lazımdır.</string>
|
||||
<string name="unlock_and_link_biometric">Cihaz kilid açma linki</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">Cihaz kilidini tanıma</string>
|
||||
<string name="device_unlock_prompt_extract_credential_title">Cihaz kilidini tanıma</string>
|
||||
<string name="encrypted_value_stored">Şifrələnmiş şifrə ehtiyyata alındı</string>
|
||||
<string name="advanced_unlock_not_recognized">Cihaz kilidini açmaq üçün barmaq izi tanınmadı</string>
|
||||
<string name="device_unlock_not_recognized">Cihaz kilidini açmaq üçün barmaq izi tanınmadı</string>
|
||||
<string name="unavailable">Mövcud deyil</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Şifrəni yazın və sonra bu düyməyə basın.</string>
|
||||
<string name="credential_before_click_device_unlock_button">Şifrəni yazın və sonra bu düyməyə basın.</string>
|
||||
<string name="properties">Xüsusiyyətlər</string>
|
||||
<string name="menu_appearance_settings">Görünüş</string>
|
||||
<string name="menu_appearance_settings_summary">Tema, rəng və atributlar</string>
|
||||
@@ -226,11 +225,11 @@
|
||||
<string name="autofill_sign_in_prompt">KeePassDX ile giriş edin</string>
|
||||
<string name="education_entry_new_field_title">Xüsusi bölmələr əlavə edin</string>
|
||||
<string name="education_add_attachment_title">Qoşma əlavə edin</string>
|
||||
<string name="advanced_unlock_explanation_summary">Məlumat bazasını daha asan açmaq üçün cihazın kilid açma funksiyasından istifadə edin</string>
|
||||
<string name="device_unlock_explanation_summary">Məlumat bazasını daha asan açmaq üçün cihazın kilid açma funksiyasından istifadə edin</string>
|
||||
<string name="device_credential_unlock_enable_summary">Məlumat bazasını açmaq üçün cihaz şəxsiyyət məlumatlarından istifadə etməyə imkan verir</string>
|
||||
<string name="education_add_attachment_summary">Önəmli xarici məlumatları yadda saxlamaq üçün şifrənizə (qeyd) qoşma əlavə edin.</string>
|
||||
<string name="temp_advanced_unlock_enable_title">Müvəqqəti kilid açma</string>
|
||||
<string name="temp_advanced_unlock_timeout_title">Cihazın kilid açma müddəti bitdi</string>
|
||||
<string name="temp_device_unlock_enable_title">Müvəqqəti kilid açma</string>
|
||||
<string name="temp_device_unlock_timeout_title">Cihazın kilid açma müddəti bitdi</string>
|
||||
<string name="unavailable_feature_version">Bu cihazda Andoird %1$s versiyası var, lakin %2$svə ya daha sonrakı versiya lazımdır.</string>
|
||||
<string name="path">Yol</string>
|
||||
<string name="unavailable_feature_hardware">Lazımi aparat-təchizat tapılmadı.</string>
|
||||
@@ -256,7 +255,7 @@
|
||||
<string name="device_credential_unlock_enable_title">Cihaz şəxsiyyət məlumatları ilə kilid açma</string>
|
||||
<string name="biometric_auto_open_prompt_title">Avtomatik açma istəyi</string>
|
||||
<string name="biometric_auto_open_prompt_summary">Məlumat bazası ondan istifadə ediləcək şəkildə quraşdırılıbsa, cihaz kilidini avtomatik olaraq tələb et</string>
|
||||
<string name="temp_advanced_unlock_enable_summary">Cihaz kilidini açmaq üçün hər hansısa şifrələnmiş məzmunu saxlamayın</string>
|
||||
<string name="temp_device_unlock_enable_summary">Cihaz kilidini açmaq üçün hər hansısa şifrələnmiş məzmunu saxlamayın</string>
|
||||
<string name="biometric_delete_all_key_title">Şifrələnmiş açarları silin</string>
|
||||
<string name="unavailable_feature_text">Bu funksiya işləmədi.</string>
|
||||
<string name="file_name">Fayl adı</string>
|
||||
@@ -342,7 +341,7 @@
|
||||
<string name="clipboard_notifications_summary">Şifrəni göstərərkən sahələri (bölmə) nüsxələmək üçün mübadilə buferi bildirişlərini göstər</string>
|
||||
<string name="clipboard_warning">Əgər mübadilə buferinin avtomatik silinməsi uğursuz olarsa, onun tarixçəsini əllə silin.</string>
|
||||
<string name="html_text_ad_free">Digər bir çox şifrə menecerlerindən fərqli olaraq, bu tətbiq <strong>reklamsız</strong>,<strong>azad lisenziyaya sahibdir</strong> və hansı versiyanı istifadə etdiyinizdən asılı olmayaraq, şəxsi məlumatlarınızı öz serverlərində toplamır.</string>
|
||||
<string name="advanced_unlock_keystore_warning">Bu xüsusiyyət, şifrələnən şəxsiyyəti təsdiq edən məlumatları cihazın təhlükəsiz açar bazasında saxlayacaqdır.\n\nƏməliyyat sisteminin yerli APİ-nin tətbiqindən asılı olaraq, tam olaraq funksional olmaya bilər.\n\nCihazın istehsalçısı və istifadə etdiyiniz ROM-un tərtibatçısı ilə açar bazasının uyğunluğu və təhlükəsizliyini yoxlayın.</string>
|
||||
<string name="device_unlock_keystore_warning">Bu xüsusiyyət, şifrələnən şəxsiyyəti təsdiq edən məlumatları cihazın təhlükəsiz açar bazasında saxlayacaqdır.\n\nƏməliyyat sisteminin yerli APİ-nin tətbiqindən asılı olaraq, tam olaraq funksional olmaya bilər.\n\nCihazın istehsalçısı və istifadə etdiyiniz ROM-un tərtibatçısı ilə açar bazasının uyğunluğu və təhlükəsizliyini yoxlayın.</string>
|
||||
<string name="autofill_inline_suggestions_title">Cümlə daxili təkliflər</string>
|
||||
<string name="autofill_inline_suggestions_summary">Birbaşa uyğun olan klaviaturadan avtomatik doldurma təkliflərini göstərməyə çalış</string>
|
||||
<string name="autofill_manual_selection_title">Əllə seçim</string>
|
||||
@@ -350,7 +349,6 @@
|
||||
<string name="autofill_save_search_info_title">Axtarış məlumatlarını yadda saxla</string>
|
||||
<string name="autofill_inline_suggestions_keyboard">Avtomatik doldurma təklifləri əlavə edildi.</string>
|
||||
<string name="allow_no_password_title">Ana açar olmamasına icazə ver</string>
|
||||
<string name="enable_read_only_summary">Məlumat bazasını standart olaraq yazma-qorumalı (dəyişməz) aç</string>
|
||||
<string name="enable_auto_save_database_title">Məlumat bazasını avtomatik olaraq yadda saxla</string>
|
||||
<string name="reset_education_screens_summary">Bütün təlim məlumatlarını yenidən göstər</string>
|
||||
<string name="reset_education_screens_text">Təlim ipuclarını sıfırlamaq</string>
|
||||
@@ -415,7 +413,7 @@
|
||||
<string name="entry_url">URL</string>
|
||||
<string name="entry_user_name">İstifadəçi adı</string>
|
||||
<string name="error_arc4">Arcfour axın şifrəsi dəstəklənmir.</string>
|
||||
<string name="error_file_not_create">Faylı yaratmaq mümkün olmadə</string>
|
||||
<string name="error_file_not_create">Faylı yaratmaq mümkün olmadə.</string>
|
||||
<string name="error_can_not_handle_uri">Bu URİ-nin KeePassDX-də istifadəsi mümkün olmadı.</string>
|
||||
<string name="case_sensitive">Böyük/kiçik hərf həssaslığı</string>
|
||||
<string name="error_invalid_db">Məlumat bazasını oxumaq mümkün olmadı.</string>
|
||||
@@ -434,7 +432,7 @@
|
||||
<string name="error_otp_digits">Jeton %1$d ilə %2$d arası rəqəmlərdən ibarət olmalıdır.</string>
|
||||
<string name="error_otp_type">Mövcud OTP növü bu form tərəfindən tanınmır, onun doğrulaması artıq düzgün şəkildə jeton yaratmaya bilər.</string>
|
||||
<string name="error_string_type">Bu mətn istənilən məlumat (fayl) ilə uyğunlaşmır.</string>
|
||||
<string name="error_registration_read_only">Dəyişməz (yalnız oxuna bilən) məlumat bazasında yeni bir məlumatın yadda saxlanılmasına icazə verilmir</string>
|
||||
<string name="error_registration_read_only">Dəyişməz (yalnız oxuna bilən) məlumat bazasında yeni bir məlumatın yadda saxlanılmasına icazə verilmir.</string>
|
||||
<string name="error_field_name_already_exists">Sahə (bölmə) adı artıq mövcuddur.</string>
|
||||
<string name="error_database_uri_null">Mləlumat bazası URİ-sini geri qaytarmaq olmur.</string>
|
||||
<string name="error_rebuild_list">Listi düzgün şəkildə yenidən hazırlamaq mümkün deyil.</string>
|
||||
@@ -444,7 +442,7 @@
|
||||
<string name="error_remove_file">Fayl məlumatlarını silərkən xəta baş verdi.</string>
|
||||
<string name="error_start_database_action">Məlumat bazasında prosses həyata keçirilərkən xəta baş verdi.</string>
|
||||
<string name="error_no_response_from_challenge">Doğrulama istəyindən cavab almaq mümkün deyil.</string>
|
||||
<string name="error_unable_merge_database_kdb">kdb məlumat bazası faylı ilə birləşdirmə etmək mümkün deyil</string>
|
||||
<string name="error_unable_merge_database_kdb">kdb məlumat bazası faylı ilə birləşdirmə etmək mümkün deyil.</string>
|
||||
<string name="invalid_credentials">Şəxsiyyəti təsdiq edən məlumatları oxumaq mümkün olmadı.</string>
|
||||
<string name="invalid_algorithm">Yanlış alqoritma.</string>
|
||||
<string name="invalid_db_same_uuid">%1$s eyni UUİD dəyərinə sahib %2$s artıq mövcuddur.</string>
|
||||
@@ -514,10 +512,10 @@
|
||||
<string name="warning_database_link_revoked">Fayla giriş icazəsi fayl meneceri tərəfindən rədd edildi</string>
|
||||
<string name="warning_database_already_opened">Məlumat bazası artıq açıqdır, yenisi açmaq üçün öncə onu bağlayın</string>
|
||||
<string name="warning_empty_password">Şifrə qoruması olmadan davam edilsinmi?</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_message">Cihazın kilid açma funksiyasından istifadə etsəniz belə, kassanın əsas şəxsiyyət təyin etmə məlumatlarını yenə də xatırlamaq lazımdır.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Cihazın kilid açma funksiyası ilə məlumat bazasının şəxsiyyətini tədiq edən məlumatlarını əldə edin</string>
|
||||
<string name="advanced_unlock_invalid_key">Cihazın kilid açma açarı oxunmadı. Zəhmət olmazsa, onu silin və kilid açma prossesini təkrarlayın.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Cihaz kilid açma istəyini başlatmaq mümkün deyil.</string>
|
||||
<string name="device_unlock_prompt_store_credential_message">Cihazın kilid açma funksiyasından istifadə etsəniz belə, kassanın əsas şəxsiyyət təyin etmə məlumatlarını yenə də xatırlamaq lazımdır.</string>
|
||||
<string name="device_unlock_prompt_extract_credential_message">Cihazın kilid açma funksiyası ilə məlumat bazasının şəxsiyyətini tədiq edən məlumatlarını əldə edin</string>
|
||||
<string name="device_unlock_invalid_key">Cihazın kilid açma açarı oxunmadı. Zəhmət olmazsa, onu silin və kilid açma prossesini təkrarlayın.</string>
|
||||
<string name="device_unlock_prompt_not_initialized">Cihaz kilid açma istəyini başlatmaq mümkün deyil.</string>
|
||||
<string name="autofill_explanation_summary">Digər tətbiqlərdə formları (anket) daha sürətli doldurmaq üçün avtomatik doldurma funksiyasını aktiv edin</string>
|
||||
<string name="autofill_select_entry">Şifrə seç .…</string>
|
||||
<string name="set_autofill_service_title">Standart avtomatik doldurma xidmətini təyin edin</string>
|
||||
@@ -536,12 +534,12 @@
|
||||
<string name="lock_database_show_button_summary">İstifadəçi interfeysində kilid düyməsini göstərin</string>
|
||||
<string name="content">Məzmun</string>
|
||||
<string name="unlock">Kilidi aç</string>
|
||||
<string name="advanced_unlock">Cihaz kilidini aç</string>
|
||||
<string name="advanced_unlock_tap_delete">Cihaz kilid açma açarlarını silmək üçün toxunun</string>
|
||||
<string name="temp_advanced_unlock_timeout_summary">Məzmununu silmədən öncə cihazın kilidini açma istifadə müddəti</string>
|
||||
<string name="advanced_unlock_timeout">Cihaz kilidi açma taymout müddəti</string>
|
||||
<string name="device_unlock">Cihaz kilidini aç</string>
|
||||
<string name="device_unlock_tap_delete">Cihaz kilid açma açarlarını silmək üçün toxunun</string>
|
||||
<string name="temp_device_unlock_timeout_summary">Məzmununu silmədən öncə cihazın kilidini açma istifadə müddəti</string>
|
||||
<string name="device_unlock_timeout">Cihaz kilidi açma taymout müddəti</string>
|
||||
<string name="biometric_delete_all_key_summary">Cihaz kilid açma tanıması ilə bağlı bütün şifrəli açarları silin</string>
|
||||
<string name="advanced_unlock_delete_all_key_warning">Cihaz kilid açma tanıması ilə bağlı bütün şifrəli açarlar silinsinmi?</string>
|
||||
<string name="device_unlock_delete_all_key_warning">Cihaz kilid açma tanıması ilə bağlı bütün şifrəli açarlar silinsinmi?</string>
|
||||
<string name="database_data_remove_unlinked_attachments_summary">Məlumat bazasında olan, lakin şifrəyə bağlı olmayan qoşmaları silin</string>
|
||||
<string name="max_history_items_summary">Hər şifrə başına düşən keçmiş məlumatların (fayl) sayını limitləyin</string>
|
||||
<string name="monospace_font_fields_enable_title">Sahə (bölmə) yazı tipi</string>
|
||||
@@ -573,7 +571,6 @@
|
||||
<string name="allow_no_password_summary">Əgər şəxsiyyəti təsdiq edən məlumatlar seçilməyibsə, \"Aç\" düyməsinin sıxılmasına icazə ver</string>
|
||||
<string name="delete_entered_password_title">Şifrəni sil</string>
|
||||
<string name="delete_entered_password_summary">Məlumat bazasına bağlantı cəhdindən sonra daxil edilmiş şifrəni sil</string>
|
||||
<string name="enable_read_only_title">Yazma qorumalı</string>
|
||||
<string name="enable_auto_save_database_summary">Hər önəmli prossesdən sonra məlumat bazasını yadda saxla (\"Modifikasiya edilə bilən\" modda keçərlidir)</string>
|
||||
<string name="enable_keep_screen_on_title">Ekranı açıq saxla</string>
|
||||
<string name="enable_keep_screen_on_summary">Şifrəyə baxarkən və ya redaktə edərkən ekranı açıq saxla</string>
|
||||
@@ -599,8 +596,8 @@
|
||||
<string name="education_select_database_summary">İstifadəyə davam etmək üçün fayl brauzerinizdən əvvəlki məlumat bazası faylını açın.</string>
|
||||
<string name="education_new_node_summary">Şifrələr rəqəmsal şəxsiyyəti təsdiq edən məlumatları idarə etməyinizə kömək edir.\n\nQruplar (~qovluqlar) məlumat bazasındakı şifrələri düzəltdir.</string>
|
||||
<string name="education_search_title">Şifrələrdə axtarış edin</string>
|
||||
<string name="education_advanced_unlock_title">Cihaz maəlumat bazası kilidini açma</string>
|
||||
<string name="education_advanced_unlock_summary">Məlumat bazanızı daha sürətli açmaq üçün şifrənizi skan edilmiş biometriya və ya cihaz şəxsiyyəti təsdiq edən məlumatlarla əlaqələndirin.</string>
|
||||
<string name="education_device_unlock_title">Cihaz maəlumat bazası kilidini açma</string>
|
||||
<string name="education_device_unlock_summary">Məlumat bazanızı daha sürətli açmaq üçün şifrənizi skan edilmiş biometriya və ya cihaz şəxsiyyəti təsdiq edən məlumatlarla əlaqələndirin.</string>
|
||||
<string name="education_entry_edit_summary">Şifrəyə özəl sahələrlə (bölmə) düzəliş edin. Ümumi məlumatlara müxtəlif şifrə sahələri arasında istinad edilə bilər.</string>
|
||||
<string name="education_sort_title">Məlumatları (fayl) sıralayın</string>
|
||||
<string name="education_sort_summary">Şifrə və qrupların necə sıralandığını seçin.</string>
|
||||
@@ -659,4 +656,4 @@
|
||||
<string name="keyboard_previous_lock_summary">Məlumat bazasını kilidlədikdən sonra avtomatik olaraq əvvəlki klaviaturaya geri dön</string>
|
||||
<string name="custom_fields">Özəl sahələr (bölmə)</string>
|
||||
<string name="back_to_previous_keyboard">Əvvəlki klaviaturaya geri dön</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<string name="menu_master_key_settings">Postavke glavnog ključa</string>
|
||||
<string name="menu_security_settings">Bezbednosne postavke</string>
|
||||
<string name="menu_database_settings">Postavke baze podataka</string>
|
||||
<string name="menu_advanced_unlock_settings">Napredno otključavanje</string>
|
||||
<string name="menu_device_unlock_settings">Napredno otključavanje</string>
|
||||
<string name="menu_form_filling_settings">Popunjavanje obrasca</string>
|
||||
<string name="menu_app_settings">Postavke aplikacije</string>
|
||||
<string name="settings">Postavke</string>
|
||||
@@ -74,7 +74,7 @@
|
||||
<string name="file_not_found_content">Nije moguće pronaći datoteku. Pokušajte ponovo da je otvorite iz svog pregledača datoteka.</string>
|
||||
<string name="field_value">Vrednost polja</string>
|
||||
<string name="field_name">Naziv polja</string>
|
||||
<string name="error_registration_read_only">Čuvanje nove stavke nije dozvoljeno u bazi podataka koja je samo za čitanje</string>
|
||||
<string name="error_registration_read_only">Čuvanje nove stavke nije dozvoljeno u bazi podataka koja je samo za čitanje.</string>
|
||||
<string name="error_string_type">Ovaj tekst se ne podudara sa traženom stavkom.</string>
|
||||
<string name="error_otp_period">Broj sekundi perioda mora biti u opsegu od %1$d do %2$d.</string>
|
||||
<string name="error_otp_digits">Broj cifara tokena mora biti u opsegu od %1$d do %2$d.</string>
|
||||
@@ -100,7 +100,7 @@
|
||||
<string name="error_invalid_OTP">Nevažeća OTP tajna.</string>
|
||||
<string name="error_invalid_path">Proverite da li je putanja ispravna.</string>
|
||||
<string name="error_invalid_db">Nije moguće pročitati bazu podataka.</string>
|
||||
<string name="error_file_not_create">Nije moguće kreirati datoteku</string>
|
||||
<string name="error_file_not_create">Nije moguće kreirati datoteku.</string>
|
||||
<string name="error_can_not_handle_uri">Nije moguće obraditi ovaj URI u keePassDX-u.</string>
|
||||
<string name="error_arc4">Archfour šifrovanje nije podržano.</string>
|
||||
<string name="entry_user_name">Korisničko ime</string>
|
||||
@@ -196,8 +196,8 @@
|
||||
<string name="biometric_auto_open_prompt_title">Automatski otvori biometrijski upit</string>
|
||||
<string name="biometric_unlock_enable_summary">Omogućava Vam da skenirate svoju biometriju da biste otvorili bazu podataka</string>
|
||||
<string name="biometric_unlock_enable_title">Biometrijsko otključavanje</string>
|
||||
<string name="advanced_unlock_explanation_summary">Koristite napredno otključavanje kako bi ste lakše otvorili bazu podataka</string>
|
||||
<string name="advanced_unlock">Napredno otključavanje</string>
|
||||
<string name="device_unlock_explanation_summary">Koristite napredno otključavanje kako bi ste lakše otvorili bazu podataka</string>
|
||||
<string name="device_unlock">Napredno otključavanje</string>
|
||||
<string name="education_unlock_summary">Unesite lozinku i/ili datoteku ključa da bi ste otključali bazu podataka.
|
||||
\n
|
||||
\nNapravite rezervnu kopiju datoteke baze podataka na bezbednom mestu nakon svake promene.</string>
|
||||
@@ -312,7 +312,7 @@
|
||||
<string name="bank_name">Ime banke</string>
|
||||
<string name="version">Verzija</string>
|
||||
<string name="error_remove_file">Došlo je do greške pri uklanjanju podataka iz datoteke.</string>
|
||||
<string name="error_challenge_already_requested">Izazov je već zahtevan</string>
|
||||
<string name="error_challenge_already_requested">Izazov je već zahtevan.</string>
|
||||
<string name="error_hardware_key_unsupported">Hardverski ključ nije podržan.</string>
|
||||
<string name="error_empty_key">Ključ ne može biti prazan.</string>
|
||||
<string name="colorize_password_summary">Obojite znakove lozinke po tipu</string>
|
||||
@@ -326,7 +326,7 @@
|
||||
<string name="rounds_explanation">Dodatne runde šifrovanja pružaju veću zaštitu od napada grube sile, ali zaista mogu usporiti učitavanje i čuvanje.</string>
|
||||
<string name="memory_usage_explanation">Količina memorije koju će koristiti funkcija izvođenja ključa.</string>
|
||||
<string name="space">Razmak</string>
|
||||
<string name="error_unable_merge_database_kdb">Nije moguće spojiti sa kdb datotekom baze podataka</string>
|
||||
<string name="error_unable_merge_database_kdb">Nije moguće spojiti sa kdb datotekom baze podataka.</string>
|
||||
<string name="error_location_unknown">Lokacija baze podataka je nepoznata, radnja baze podataka se ne može izvršiti.</string>
|
||||
<string name="corrupted_file">Oštećena datoteka.</string>
|
||||
<string name="colorize_password_title">Obojite lozinke</string>
|
||||
@@ -340,7 +340,7 @@
|
||||
<string name="remember_database_locations_title">Zapamtite lokacije baza podataka</string>
|
||||
<string name="remember_hardware_key_title">Zapamtite hardverske ključeve</string>
|
||||
<string name="remember_hardware_key_summary">Vodi evidenciju o korišćenim hardverskim ključevima</string>
|
||||
<string name="error_import_app_properties">Greška tokom uvoza podešavanja aplikacije</string>
|
||||
<string name="error_import_app_properties">Greška tokom uvoza podešavanja aplikacije.</string>
|
||||
<string name="hide_broken_locations_title">Sakrij neispravne veze baze podataka</string>
|
||||
<string name="import_app_properties_title">Uvezite podešavanja aplikacije</string>
|
||||
<string name="description_app_properties">KeePassDX svojstva za upravljanje podešavanjima aplikacije</string>
|
||||
@@ -394,7 +394,7 @@
|
||||
<string name="error_driver_required">Potreban je drajver za %1$s.</string>
|
||||
<string name="menu_app_settings_summary">Pretraga, zaključavanje, istorija, svojstva</string>
|
||||
<string name="menu_form_filling_settings_summary">Tastatura, automatsko popunjavanje, klipbord</string>
|
||||
<string name="menu_advanced_unlock_settings_summary">Biometrija, akreditiv uređaja</string>
|
||||
<string name="menu_device_unlock_settings_summary">Biometrija, akreditiv uređaja</string>
|
||||
<string name="menu_database_settings_summary">Metapodaci, korpa za otpatke, šabloni, istorija</string>
|
||||
<string name="menu_security_settings_summary">Šifrovanje, funkcija izvođenja ključa</string>
|
||||
<string name="master_key_settings_summary">Promena, obnova</string>
|
||||
@@ -405,7 +405,7 @@
|
||||
<string name="subdomain_search_title">Pretraga poddomena</string>
|
||||
<string name="progress_create">Pravljenje nove baze podataka…</string>
|
||||
<string name="read_only">Zaštićeno od pisanja</string>
|
||||
<string name="error_export_app_properties">Greška tokom izvoza podešavanja aplikacije</string>
|
||||
<string name="error_export_app_properties">Greška tokom izvoza podešavanja aplikacije.</string>
|
||||
<string name="contains_duplicate_uuid">Baza podataka sadrži duplirane UUID-ove.</string>
|
||||
<string name="contains_duplicate_uuid_procedure">Rešiti problem generisanjem novih UUID-ova za nastavak duplikata?</string>
|
||||
<string name="search_mode">Režim pretrage</string>
|
||||
@@ -413,4 +413,4 @@
|
||||
<string name="parallelism">Paralelizam</string>
|
||||
<string name="parallelism_explanation">Stepen paralelizma (tj. broj niti) koji koristi funkcija izvođenja ključa.</string>
|
||||
<string name="saving_database">Čuvanje baze podataka…</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<string name="entry_otp">OTP</string>
|
||||
<string name="error_arc4">唔支援Arcfour stream密碼。</string>
|
||||
<string name="error_can_not_handle_uri">KeePassDX 處理唔到呢個網址。</string>
|
||||
<string name="error_file_not_create">新增唔到檔案</string>
|
||||
<string name="error_file_not_create">新增唔到檔案。</string>
|
||||
<string name="error_invalid_db">讀取唔到資料庫。</string>
|
||||
<string name="error_invalid_path">請確保路徑正確。</string>
|
||||
<string name="error_invalid_OTP">無效 OTP 密鑰。</string>
|
||||
@@ -139,4 +139,4 @@
|
||||
<string name="bank_name">銀行名稱</string>
|
||||
<string name="entry_url">網址</string>
|
||||
<string name="entry_user_name">用戶名</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user