mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
1203 Commits
4.0.2
...
3b793a72b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b793a72b8 | ||
|
|
f19afbdb2e | ||
|
|
622e9cefdd | ||
|
|
3ba56677ba | ||
|
|
39b4b4df70 | ||
|
|
4180ca92b0 | ||
|
|
bc9d00a1e1 | ||
|
|
5bdc72aa67 | ||
|
|
2be32e6884 | ||
|
|
612db4a6fc | ||
|
|
e74176f3bc | ||
|
|
af1fba42a0 | ||
|
|
bebf30aec1 | ||
|
|
321bb46df5 | ||
|
|
429f6db93f | ||
|
|
fc5a13160a | ||
|
|
c6eee8d449 | ||
|
|
7d227f372f | ||
|
|
3ac56b974f | ||
|
|
2e85ea401b | ||
|
|
fd080fb952 | ||
|
|
cc8e07366a | ||
|
|
c21bcbdbc2 | ||
|
|
e2ee17dae7 | ||
|
|
e68830fa25 | ||
|
|
9ddd66ce85 | ||
|
|
e3b69789bf | ||
|
|
54f2ed9fab | ||
|
|
2fea019b95 | ||
|
|
9ac7ef2d22 | ||
|
|
6d452fa49c | ||
|
|
d99edb6b4d | ||
|
|
cb679f0d59 | ||
|
|
2e237fba2d | ||
|
|
e68863a154 | ||
|
|
5dd9f75095 | ||
|
|
403021d38b | ||
|
|
fea7b30d6f | ||
|
|
ab5c859db4 | ||
|
|
3fcbc65de0 | ||
|
|
3f1ee6bbea | ||
|
|
37ce2ab781 | ||
|
|
ffaf4a761a | ||
|
|
56b7cc9118 | ||
|
|
987f3f9047 | ||
|
|
3039efc67c | ||
|
|
26daac4637 | ||
|
|
88a93829a9 | ||
|
|
7923a63d36 | ||
|
|
9a5c782d5d | ||
|
|
c39e4ba693 | ||
|
|
7db3d0502f | ||
|
|
d557e8b516 | ||
|
|
d6ae17657b | ||
|
|
3468b0f6f5 | ||
|
|
79777801e8 | ||
|
|
a202f66d48 | ||
|
|
ba58d5d47c | ||
|
|
46685592df | ||
|
|
ba9e2892ef | ||
|
|
a1da3b4fbd | ||
|
|
8bee0ec220 | ||
|
|
aebf6b21de | ||
|
|
0cf9253ea4 | ||
|
|
b63ceb37a4 | ||
|
|
c462dae6f5 | ||
|
|
ddf890b861 | ||
|
|
252eb30b13 | ||
|
|
62ab11cc56 | ||
|
|
e19ad3a8cc | ||
|
|
51fd8a77eb | ||
|
|
5ee0c2eb13 | ||
|
|
6d0ef8265c | ||
|
|
ea69d5acb2 | ||
|
|
1fb9595ec3 | ||
|
|
88e0bd51dc | ||
|
|
67477cc53b | ||
|
|
d2549d61d6 | ||
|
|
d6dc75961b | ||
|
|
f40c83812a | ||
|
|
b29c638d20 | ||
|
|
5bb03c2eef | ||
|
|
a76b1195e5 | ||
|
|
64da26f42c | ||
|
|
ef82552a0f | ||
|
|
d61b27ccd0 | ||
|
|
910ba99056 | ||
|
|
3de2a9acfd | ||
|
|
a48dccf27a | ||
|
|
2a561fb37e | ||
|
|
e27a329ac5 | ||
|
|
8e06a2a7cb | ||
|
|
ace82852af | ||
|
|
73369974b8 | ||
|
|
332eda8a7a | ||
|
|
e5ea1e35aa | ||
|
|
86aae9635a | ||
|
|
db3ccae87d | ||
|
|
4cec26967c | ||
|
|
a0368a4981 | ||
|
|
a1c7fe1e99 | ||
|
|
bf247ddeb7 | ||
|
|
1d2bc0fbfb | ||
|
|
85a12fe4ee | ||
|
|
a443ef996b | ||
|
|
c6995ad403 | ||
|
|
9018807eb8 | ||
|
|
b463106dd5 | ||
|
|
a23d28e1fa | ||
|
|
a0454f42d0 | ||
|
|
1c2ac88f47 | ||
|
|
11eb1bae45 | ||
|
|
089d86165a | ||
|
|
6a7362ad35 | ||
|
|
d2c10e2e4e | ||
|
|
0c20a14e67 | ||
|
|
acccf290de | ||
|
|
6ebe0f78af | ||
|
|
935c09ccd2 | ||
|
|
1eb10ad5bd | ||
|
|
ca4283151e | ||
|
|
8fb98ca4e7 | ||
|
|
be74c9710f | ||
|
|
24fb3c4c30 | ||
|
|
3bdc5fe600 | ||
|
|
c30884d6d0 | ||
|
|
5d26c3bd09 | ||
|
|
02e35cf5b7 | ||
|
|
085aefd2b9 | ||
|
|
1ea5b7a50c | ||
|
|
6cba96dd42 | ||
|
|
46238a76bc | ||
|
|
d5a9b664a1 | ||
|
|
6fdc4504d5 | ||
|
|
8a7c411a35 | ||
|
|
5ff9d5fa2f | ||
|
|
bb0f3c80d3 | ||
|
|
597d9c8274 | ||
|
|
4dd8c06fd2 | ||
|
|
72c66b3cd9 | ||
|
|
c2223afa6f | ||
|
|
d338d1340f | ||
|
|
ed4423666b | ||
|
|
d21fe662ff | ||
|
|
4da1c5bd92 | ||
|
|
18c18605fb | ||
|
|
988cb1a8d0 | ||
|
|
b6e01767e0 | ||
|
|
5414854e9c | ||
|
|
ae7f0732c6 | ||
|
|
d49d33fe3a | ||
|
|
5e7fc2d468 | ||
|
|
0d26e6a870 | ||
|
|
dd92f9ceb6 | ||
|
|
ff9239b9c4 | ||
|
|
319d35e485 | ||
|
|
28e65a4601 | ||
|
|
eb626e5bfe | ||
|
|
e1decf9a23 | ||
|
|
fff0e84b95 | ||
|
|
d73a7004b1 | ||
|
|
f71061e835 | ||
|
|
b2d25cc512 | ||
|
|
4d54b56c1d | ||
|
|
c764c6afff | ||
|
|
87b97a3849 | ||
|
|
5e6db44476 | ||
|
|
8615fa817f | ||
|
|
2f891bacd3 | ||
|
|
0d8a426df4 | ||
|
|
c952eb4415 | ||
|
|
2fd53b9416 | ||
|
|
244ca08890 | ||
|
|
208f1e97d5 | ||
|
|
e4e0628e20 | ||
|
|
f60f31771f | ||
|
|
ff6367bac4 | ||
|
|
540e72812e | ||
|
|
5fe4af8e9d | ||
|
|
ae42ab43b7 | ||
|
|
c463055971 | ||
|
|
1849dca81d | ||
|
|
b3dd3dcfb5 | ||
|
|
fef88ff270 | ||
|
|
f1f7dd1e6c | ||
|
|
409f290e33 | ||
|
|
96c3af097a | ||
|
|
4fe6b2e115 | ||
|
|
cc936b9304 | ||
|
|
e7f2a22583 | ||
|
|
4bf905ecda | ||
|
|
f8d80525d9 | ||
|
|
7ce6092270 | ||
|
|
65857596a6 | ||
|
|
e6253336bd | ||
|
|
e5595a3275 | ||
|
|
366e8bf1d7 | ||
|
|
fa63265599 | ||
|
|
755e0ea9a5 | ||
|
|
a819f2f8a8 | ||
|
|
c92da0a72f | ||
|
|
524963dbd8 | ||
|
|
50b1ac388e | ||
|
|
51c62034df | ||
|
|
e4d0cd89c6 | ||
|
|
bfe50fa985 | ||
|
|
3d798e6585 | ||
|
|
068c59ac98 | ||
|
|
34ec94a0c3 | ||
|
|
576a355342 | ||
|
|
aa19f11699 | ||
|
|
2fb4dff46d | ||
|
|
e6cf3f12a5 | ||
|
|
ca94ce86ba | ||
|
|
dea6b25bb4 | ||
|
|
c48f64d331 | ||
|
|
5e3a504c1f | ||
|
|
b9b7d7b2db | ||
|
|
e085d5d277 | ||
|
|
05336e93a0 | ||
|
|
90b3b56893 | ||
|
|
02c514272e | ||
|
|
989e47ed12 | ||
|
|
1caf132558 | ||
|
|
1b98bd740c | ||
|
|
5adeb5cde0 | ||
|
|
b949d5d861 | ||
|
|
b4264a30a4 | ||
|
|
cf799c0f68 | ||
|
|
97f0ca519b | ||
|
|
cf4047b701 | ||
|
|
40608a3eb5 | ||
|
|
99cb50d031 | ||
|
|
b0d0c35241 | ||
|
|
6044c93a4a | ||
|
|
b544b5d54d | ||
|
|
852378e484 | ||
|
|
711a344860 | ||
|
|
72087c7e5c | ||
|
|
a337de3679 | ||
|
|
75b37f5a9f | ||
|
|
075f54b286 | ||
|
|
e07cbc2e14 | ||
|
|
ac29b7bac7 | ||
|
|
b9129cb941 | ||
|
|
6957fcd81a | ||
|
|
cfe56fc055 | ||
|
|
6f3e065ad1 | ||
|
|
abfa7a3f47 | ||
|
|
dd0d85e54e | ||
|
|
76c20263f7 | ||
|
|
e447388611 | ||
|
|
1bfec67c02 | ||
|
|
45041216d6 | ||
|
|
e075e9018c | ||
|
|
eed304ec40 | ||
|
|
5bcbbac97f | ||
|
|
ea4750fc11 | ||
|
|
5037821529 | ||
|
|
3a4c88f19a | ||
|
|
e960a8e169 | ||
|
|
1d4e1687cf | ||
|
|
033fa95285 | ||
|
|
f17d211fbd | ||
|
|
cae69e7572 | ||
|
|
01d778650c | ||
|
|
dd389dbab1 | ||
|
|
272ebd0c3f | ||
|
|
0aecc21f43 | ||
|
|
1e7e464e65 | ||
|
|
ae903ad236 | ||
|
|
d5c378ac85 | ||
|
|
672f1ca37d | ||
|
|
2f9e1e4bf2 | ||
|
|
25d97e4f2e | ||
|
|
f49dcbd654 | ||
|
|
bf2d56b4fd | ||
|
|
7c3a15ce79 | ||
|
|
5893541dd2 | ||
|
|
2230fe66ab | ||
|
|
84a62a32ff | ||
|
|
da8ef9340c | ||
|
|
af068349e4 | ||
|
|
56cb5953dd | ||
|
|
2fc2a9c7c1 | ||
|
|
69e7cdbc47 | ||
|
|
39d9a74a73 | ||
|
|
b609d4e182 | ||
|
|
7212c73481 | ||
|
|
3ee4caa153 | ||
|
|
28e4d929bb | ||
|
|
803d637510 | ||
|
|
ccd5da0962 | ||
|
|
e8ecf28f7c | ||
|
|
3d5adbfc01 | ||
|
|
72bfc50703 | ||
|
|
36e3b85400 | ||
|
|
cd73880e21 | ||
|
|
a60e2e780d | ||
|
|
9210851765 | ||
|
|
8337f98f3a | ||
|
|
47fbb562b7 | ||
|
|
a46251be7b | ||
|
|
ef98e8a2db | ||
|
|
e8ec27dc38 | ||
|
|
30dd7c567c | ||
|
|
e562694606 | ||
|
|
464bc1442d | ||
|
|
c1730353d0 | ||
|
|
55e32e4ac5 | ||
|
|
96ed9fc7a6 | ||
|
|
5fda628c9c | ||
|
|
17742e25a9 | ||
|
|
8086289e4b | ||
|
|
65157f661f | ||
|
|
5df637d01f | ||
|
|
8084920b9e | ||
|
|
b196145578 | ||
|
|
ac347db2d1 | ||
|
|
013c437cf7 | ||
|
|
1f600d60e3 | ||
|
|
a6af9976fc | ||
|
|
05c480b6d3 | ||
|
|
d5ecaeb331 | ||
|
|
db8b0100de | ||
|
|
5f41177a1f | ||
|
|
fb909dac52 | ||
|
|
a8130d67be | ||
|
|
754d195e26 | ||
|
|
074910ea19 | ||
|
|
988b18b515 | ||
|
|
8924254c25 | ||
|
|
0db2b7023e | ||
|
|
a2c2a21dde | ||
|
|
d7a3e7fedd | ||
|
|
2bedbf8a6c | ||
|
|
437a704bc8 | ||
|
|
a3bd5e1593 | ||
|
|
3feb177afc | ||
|
|
821f35fe05 | ||
|
|
d36f675da7 | ||
|
|
b7f9690a38 | ||
|
|
5e4ee167fc | ||
|
|
c911b7c511 | ||
|
|
c79d1f1b81 | ||
|
|
daf717becd | ||
|
|
48d4483484 | ||
|
|
c861fe790c | ||
|
|
1a717bda03 | ||
|
|
b80acd5a2d | ||
|
|
7e41527cfe | ||
|
|
200881278c | ||
|
|
0d133ffdb0 | ||
|
|
c6b0ee27df | ||
|
|
0053726d0b | ||
|
|
1395af88d1 | ||
|
|
2e3ade1b4a | ||
|
|
90c43acfbf | ||
|
|
90b68fd972 | ||
|
|
f8787ba03d | ||
|
|
4f10d13691 | ||
|
|
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 | ||
|
|
98007c962d | ||
|
|
5f27f161a5 | ||
|
|
fcf723849b | ||
|
|
8a60056866 | ||
|
|
e9d20a51a5 | ||
|
|
a28d77ba32 | ||
|
|
5bd866e104 | ||
|
|
9985c6065d | ||
|
|
1f2e4a3719 | ||
|
|
fa2555a3f7 | ||
|
|
b4de7afe77 | ||
|
|
736cafbcc2 | ||
|
|
d143605a40 | ||
|
|
f2f4c1e63d | ||
|
|
bc86ee87a0 | ||
|
|
5cbd60c024 | ||
|
|
15972efb4f | ||
|
|
dae5f65c0d | ||
|
|
564b5f10ea | ||
|
|
e6e40f9bd4 | ||
|
|
bd15e36b52 | ||
|
|
43faca3061 | ||
|
|
82af9bada2 | ||
|
|
5817273872 | ||
|
|
32d6a11353 | ||
|
|
9477fba704 | ||
|
|
80b16bccf1 | ||
|
|
2befa68c93 | ||
|
|
6672085d84 | ||
|
|
05a39f6922 | ||
|
|
40e8dea485 | ||
|
|
7e09532d5d | ||
|
|
4034a2bfc4 | ||
|
|
0d93e867cf | ||
|
|
44e8f4f406 | ||
|
|
e3083c7773 | ||
|
|
d0c0c4a4d6 | ||
|
|
a9e8de26f8 | ||
|
|
c7a256ebf1 | ||
|
|
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 | ||
|
|
d0ab5267cf | ||
|
|
88b701fd39 | ||
|
|
4a1cee619c | ||
|
|
26cb36ccd0 | ||
|
|
67e0496efc | ||
|
|
6c4d040564 | ||
|
|
c7741115ff | ||
|
|
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 | ||
|
|
19c987abc3 | ||
|
|
b6201262f1 | ||
|
|
b99fa9ffcf | ||
|
|
1497ab85b2 | ||
|
|
2ade463974 | ||
|
|
3a67ec09d5 | ||
|
|
dca800b1bb | ||
|
|
70665f110d | ||
|
|
3b39cafb99 | ||
|
|
0c1b94468d | ||
|
|
7a841bbf57 | ||
|
|
0066f1c77a | ||
|
|
14dbff603d | ||
|
|
42afa93293 | ||
|
|
2fa25a51ad | ||
|
|
2b7f41477f | ||
|
|
be0d5f80c3 | ||
|
|
3844188fcc | ||
|
|
ef81ba5a8f | ||
|
|
2871668d8f | ||
|
|
d03693341e | ||
|
|
2b5ecb2f84 | ||
|
|
e397b92c36 | ||
|
|
e273eb6e03 | ||
|
|
28b624afa3 | ||
|
|
fb1e6cdc3f | ||
|
|
8b6499d040 | ||
|
|
054af507ad | ||
|
|
bf496333eb | ||
|
|
ac9bb9b666 | ||
|
|
809e1929e5 | ||
|
|
a1b1338d67 | ||
|
|
bd4cacfab1 | ||
|
|
e0343bdc55 | ||
|
|
b743d004e2 | ||
|
|
4b20e035b2 | ||
|
|
afe5fddc50 | ||
|
|
d68ca1b51f | ||
|
|
061b087229 | ||
|
|
bb3a379965 | ||
|
|
593b5c6338 | ||
|
|
56f8a1bf9f | ||
|
|
962b547b36 | ||
|
|
c6b01947b3 | ||
|
|
6df8ff4310 | ||
|
|
52f17140b8 | ||
|
|
75c2bb4a87 | ||
|
|
f36f6c3155 | ||
|
|
b88b92c5b0 | ||
|
|
d2c569c4f0 | ||
|
|
91781f36ac | ||
|
|
3fbdf78ba1 | ||
|
|
d1f463d497 | ||
|
|
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 | ||
|
|
488fd60d5d | ||
|
|
41025f64c0 | ||
|
|
a2eac2ff76 | ||
|
|
34f2a2391a | ||
|
|
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 | ||
|
|
2b0f4fe46b | ||
|
|
55c2f41c71 | ||
|
|
94985422ca | ||
|
|
eaff1aa58f | ||
|
|
c7cb9d0990 | ||
|
|
a9fdf30421 | ||
|
|
d0f45f6dfb | ||
|
|
8cdb6a3c9f | ||
|
|
b369a46431 | ||
|
|
f40ca2d5e0 | ||
|
|
400393c677 | ||
|
|
5710e3be55 | ||
|
|
af1312a92b | ||
|
|
d2a63c48b1 | ||
|
|
8f10ea7ed6 | ||
|
|
7bd701368a | ||
|
|
894e846a62 | ||
|
|
504ef5a7ab | ||
|
|
af6436da77 | ||
|
|
d031420ed3 | ||
|
|
4b6b7478de | ||
|
|
bb1b5eab96 | ||
|
|
9486b50342 | ||
|
|
fc9a5b3545 | ||
|
|
8b4d0e2541 | ||
|
|
0ef07f615c | ||
|
|
836413cff2 | ||
|
|
c71e34fee9 | ||
|
|
b389a4db92 | ||
|
|
0a1a54cb33 | ||
|
|
630228675c | ||
|
|
f7955d00fc | ||
|
|
68b08f9b9a | ||
|
|
612f136ced | ||
|
|
8c313c3d48 | ||
|
|
517a6c0062 | ||
|
|
46c61b10de | ||
|
|
edc8d27577 | ||
|
|
05354777fe | ||
|
|
98d3d2a39b | ||
|
|
f91f75912e | ||
|
|
9f21f67035 | ||
|
|
699e4da112 | ||
|
|
6458285e75 | ||
|
|
e0ddf3711f | ||
|
|
68d415375d | ||
|
|
0e11afdd8b | ||
|
|
ff2c01584f | ||
|
|
a59e20b864 | ||
|
|
d7d898896d | ||
|
|
2dde78d5e7 | ||
|
|
2ca39ff399 | ||
|
|
1f6fdaf9b3 | ||
|
|
edaf58135f | ||
|
|
724698fc51 | ||
|
|
6aabe9e12c | ||
|
|
2b5a1bb893 | ||
|
|
7403305f3c | ||
|
|
e780f8a3f0 | ||
|
|
67b09014aa | ||
|
|
7f2fda0327 | ||
|
|
ada8f74e2c | ||
|
|
1ddfeaf950 | ||
|
|
2ed2cc1499 | ||
|
|
be0f90f12a | ||
|
|
4b362df23b | ||
|
|
b953a1c2f6 | ||
|
|
4c30fa43d3 | ||
|
|
6866b1a3bb | ||
|
|
328030f152 | ||
|
|
fd195bd926 | ||
|
|
87e07366cd | ||
|
|
8133977e09 | ||
|
|
11199b996c | ||
|
|
eee61db189 | ||
|
|
c7c5130030 | ||
|
|
6de14a3840 | ||
|
|
55206b3dde | ||
|
|
e1a44477af | ||
|
|
23afee453e | ||
|
|
8e05309021 | ||
|
|
26fdf87070 | ||
|
|
1c95a0edc4 | ||
|
|
4723fb39e9 | ||
|
|
13d667d81c | ||
|
|
dce255dc58 | ||
|
|
f72c9704d9 | ||
|
|
e623010e91 | ||
|
|
587bfdc162 | ||
|
|
7c1c299282 | ||
|
|
f2a5c0b04b | ||
|
|
4ba77b76ec | ||
|
|
0bfce44317 | ||
|
|
13b6d6384c | ||
|
|
80838bbef0 | ||
|
|
8a557ff2fb | ||
|
|
16e394087d | ||
|
|
ee1b67b36e | ||
|
|
36b9fa2387 | ||
|
|
378169e939 | ||
|
|
70a01e559d | ||
|
|
c09e8196f3 | ||
|
|
0382c05152 | ||
|
|
75cf6e2a56 | ||
|
|
9fb4754430 | ||
|
|
0312b504a9 | ||
|
|
1d6a9651bf | ||
|
|
e36b18e85e | ||
|
|
4ae4951e0d | ||
|
|
a65f52ffba | ||
|
|
6de25ffa65 | ||
|
|
d8429bdd99 | ||
|
|
c907750446 | ||
|
|
26976ae6cf | ||
|
|
53beaca563 | ||
|
|
77628e2fb9 | ||
|
|
40d2f2de96 | ||
|
|
84cdb2483f | ||
|
|
c7866bfbbf | ||
|
|
5de1d6b343 | ||
|
|
7bde363704 | ||
|
|
1f9b2ce7b9 | ||
|
|
ac2e47776a | ||
|
|
e7de5ca263 | ||
|
|
f7f079e653 | ||
|
|
f7cccb33de | ||
|
|
69114c3cc0 | ||
|
|
8177c9c34b | ||
|
|
850c46f881 | ||
|
|
ffcfe966d2 | ||
|
|
800badd2a4 | ||
|
|
0a7ffbcc8f | ||
|
|
019ec4de9a | ||
|
|
78d1e4a12a | ||
|
|
6c99fefad0 | ||
|
|
24bc1424b9 | ||
|
|
94a0e17cfc | ||
|
|
2f012d8cf2 | ||
|
|
75b800054f | ||
|
|
d7c7733315 | ||
|
|
99600ad8d8 | ||
|
|
4a4c7b8b6b | ||
|
|
8c3267b345 | ||
|
|
c0240b047b | ||
|
|
c52957ccfe | ||
|
|
fb3f057adf | ||
|
|
e333bd08a4 | ||
|
|
1844a269cb | ||
|
|
859882d24f | ||
|
|
c95543b8b0 | ||
|
|
d874125dc1 | ||
|
|
795cd099f4 | ||
|
|
66ef6fd9d8 | ||
|
|
3b0655354d | ||
|
|
76acec93fd | ||
|
|
3b60068369 | ||
|
|
a0fd0a71a2 | ||
|
|
907accbcc9 | ||
|
|
ee284abf8d | ||
|
|
8239275770 | ||
|
|
029485bace | ||
|
|
cef9f6ae3b | ||
|
|
8d88c94956 | ||
|
|
692e155117 | ||
|
|
9a9410de2b | ||
|
|
a27f1181ea | ||
|
|
ac65cadb1b | ||
|
|
4345e75b20 | ||
|
|
f64d085e7b | ||
|
|
325b878f0a | ||
|
|
330f375a30 | ||
|
|
7d6c211de1 | ||
|
|
af5b36752c | ||
|
|
e0f563befb | ||
|
|
bf1b84dfea | ||
|
|
b3f8ce9c16 | ||
|
|
499152d066 | ||
|
|
c47e7edc9e | ||
|
|
afc034b495 | ||
|
|
e1d19741af | ||
|
|
8d2de40df6 | ||
|
|
f4b7db667f | ||
|
|
4032e52317 | ||
|
|
f9db4325d8 | ||
|
|
815824f76d | ||
|
|
8534672c33 | ||
|
|
b9bb9a166a | ||
|
|
185c886472 | ||
|
|
acc565d021 | ||
|
|
64db137c6c | ||
|
|
4f2bdeb2c9 | ||
|
|
527994084b | ||
|
|
ffaf5c9475 | ||
|
|
d1e24bfcd8 | ||
|
|
aab8612c63 | ||
|
|
79c1e2d21c | ||
|
|
7ff44f4839 | ||
|
|
2de0272e3e | ||
|
|
1d22fe72d0 | ||
|
|
7e9821551c | ||
|
|
8fb3cd71b9 | ||
|
|
b0f6f1f2ba | ||
|
|
df56dc4a4a | ||
|
|
b572ec4901 | ||
|
|
75759171c1 | ||
|
|
322fe185dd | ||
|
|
effe514ecb | ||
|
|
4ff9f69548 | ||
|
|
e1c7cb17da | ||
|
|
e6defd7770 | ||
|
|
bba9f45075 | ||
|
|
de1a2d1557 | ||
|
|
e995d06a26 | ||
|
|
63f54a1318 | ||
|
|
0828c9f901 | ||
|
|
51cd38450a | ||
|
|
0a4a720fea | ||
|
|
7f7ce171e0 | ||
|
|
87622cc6ec | ||
|
|
aaf2e64fdb | ||
|
|
c6f22e7ce5 | ||
|
|
749a2d19aa | ||
|
|
ed3491fbba | ||
|
|
f5e4b7cd6a | ||
|
|
0b614e81ee | ||
|
|
0a93b660cf | ||
|
|
71e104e4bd | ||
|
|
189c5de7ea | ||
|
|
a5e141c361 | ||
|
|
e76d8d4df7 | ||
|
|
d54c093985 | ||
|
|
086709e40f | ||
|
|
9477e32ec8 | ||
|
|
a66a0628ad | ||
|
|
ea018bd4c1 | ||
|
|
8e81ee3b75 | ||
|
|
9c326f9be9 | ||
|
|
7d2a0aa793 | ||
|
|
22d1442033 | ||
|
|
ffa32a5501 | ||
|
|
f50a6a8416 | ||
|
|
7a82b75ee2 | ||
|
|
350c0585b8 | ||
|
|
5c4454d3ed | ||
|
|
dfd1ae7a50 | ||
|
|
d16fdd062f | ||
|
|
d6432698fc | ||
|
|
1a9c1e8bc1 | ||
|
|
2d05c6cb13 | ||
|
|
05f689913f | ||
|
|
9e714c4192 | ||
|
|
e1ad1e31f8 | ||
|
|
b473bf1da8 | ||
|
|
dfdc6eecb6 | ||
|
|
e9145df77a | ||
|
|
4e824990d1 | ||
|
|
5a053378e1 | ||
|
|
e09306b6f3 | ||
|
|
21e577c1d3 | ||
|
|
9d85d0979c | ||
|
|
e4fec7ea1f | ||
|
|
c998e513fc | ||
|
|
855b06c4b2 | ||
|
|
776012f02f | ||
|
|
9bff531618 | ||
|
|
4d64818da1 | ||
|
|
837c8773a8 | ||
|
|
97a199b504 | ||
|
|
9cd37be5b1 | ||
|
|
90895fed52 | ||
|
|
df4b73abbb | ||
|
|
d2bed08ae0 | ||
|
|
fd5e1472e1 | ||
|
|
6a22126006 | ||
|
|
ff5e9049a0 | ||
|
|
7570aa9f49 | ||
|
|
4fcdd3da23 | ||
|
|
f8a65b4d13 | ||
|
|
5bb70a5043 | ||
|
|
c4788159e4 | ||
|
|
403b0aedca | ||
|
|
cae12b84df | ||
|
|
40dbbbf0a0 | ||
|
|
9e0da27484 | ||
|
|
7e6c2463d6 | ||
|
|
69a76db166 | ||
|
|
efd282e326 | ||
|
|
6471fef28c | ||
|
|
5a0691b70c | ||
|
|
27ea357348 | ||
|
|
e12de29cce | ||
|
|
a236be4e38 | ||
|
|
5300643f62 | ||
|
|
469b837daf | ||
|
|
84ca3e8982 | ||
|
|
bfb8be159a | ||
|
|
13d7af9c76 | ||
|
|
66d78497b9 | ||
|
|
32743f7e19 | ||
|
|
c2cc4451ff | ||
|
|
67370bbc3d | ||
|
|
1201ae1956 | ||
|
|
0eb2786ce5 | ||
|
|
bbdeb66f8e | ||
|
|
641698a652 | ||
|
|
15880e995a | ||
|
|
1e849b106b | ||
|
|
31bc405e86 | ||
|
|
6e69eee2a3 | ||
|
|
279b095f30 | ||
|
|
9821499cdb | ||
|
|
b76c39862a | ||
|
|
7948d234f5 | ||
|
|
50fc3aa9fc | ||
|
|
79a7adb05b | ||
|
|
c02770ec2a | ||
|
|
bf24033513 | ||
|
|
d97c87e90c | ||
|
|
6d29fdc448 | ||
|
|
d16f0dd3f6 | ||
|
|
d023c97049 | ||
|
|
73c8d63f0e | ||
|
|
30709de7e2 | ||
|
|
c3e560766e | ||
|
|
7c7889e87e | ||
|
|
6547ccecdd | ||
|
|
d467a591b0 | ||
|
|
42613ff480 | ||
|
|
3a3d4b9e54 | ||
|
|
0b5c0bcf74 | ||
|
|
5937f009e1 | ||
|
|
c7f3d2a64e | ||
|
|
41263307d7 | ||
|
|
f2ff52477e | ||
|
|
6baa5a0730 | ||
|
|
5a5cb8c866 | ||
|
|
a3a78366f7 | ||
|
|
d936292ed9 | ||
|
|
a3ded18f59 | ||
|
|
0969135f7f | ||
|
|
7e1c1a6324 | ||
|
|
aa43dd2a49 | ||
|
|
81fe9b6fc2 | ||
|
|
6807fdc1cc | ||
|
|
ac1590810f | ||
|
|
9dd4d77535 | ||
|
|
c7aa6a9d96 | ||
|
|
fc56db2f83 | ||
|
|
c58e56257d | ||
|
|
b27eb280a8 | ||
|
|
78c7f4078c | ||
|
|
28ccaffcf5 | ||
|
|
78739558d6 | ||
|
|
cd195d30de | ||
|
|
03bf752284 | ||
|
|
238fab3e1d | ||
|
|
fcd9af8f84 | ||
|
|
b44491ebbe | ||
|
|
1f8018fd5b | ||
|
|
1d67656fa0 | ||
|
|
64b8023d1a | ||
|
|
cc1697e7ec | ||
|
|
28943e77e8 | ||
|
|
575109da9f | ||
|
|
a99667d471 | ||
|
|
6a7420bd3a | ||
|
|
e8dbe05615 | ||
|
|
6b6566cd29 | ||
|
|
0001d31c2c | ||
|
|
974686e698 | ||
|
|
7b7063b9be | ||
|
|
55061a9469 | ||
|
|
c433fb643c | ||
|
|
02306385b6 | ||
|
|
432ac1bcec | ||
|
|
d9480e0c9a | ||
|
|
815fb911d6 | ||
|
|
68cbdae8e0 | ||
|
|
2d8f8aeef3 | ||
|
|
479bc7be71 | ||
|
|
cbc6df2e62 | ||
|
|
d16b4cfadb | ||
|
|
a97bad1f86 | ||
|
|
7198ffff43 | ||
|
|
e173159d13 | ||
|
|
1b54b79e88 | ||
|
|
bb3436615e | ||
|
|
809db61c35 | ||
|
|
88e53fcba8 | ||
|
|
fe68b7e294 | ||
|
|
b7b76d6da7 | ||
|
|
e2eae43fc9 | ||
|
|
f41ecec09c | ||
|
|
15ad4d11ef | ||
|
|
0cf9d98f14 | ||
|
|
612e642523 | ||
|
|
0ff77eb157 | ||
|
|
2b8935a5d7 | ||
|
|
afdc5c8460 | ||
|
|
91ba2dff2d | ||
|
|
2d26079c49 | ||
|
|
f13d99e0d1 | ||
|
|
d3182b8d2a | ||
|
|
f52d139acc | ||
|
|
87e9a38548 | ||
|
|
faa70c57b3 | ||
|
|
5172c07c18 | ||
|
|
a80fa03db4 | ||
|
|
d73e02948e | ||
|
|
283657e1b7 | ||
|
|
d3c4a3a17e | ||
|
|
9184bc40e5 | ||
|
|
84bd98ebf4 | ||
|
|
4ef2cbcaeb | ||
|
|
35f8b45bf4 | ||
|
|
0474e5b1fe |
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:"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,6 +19,9 @@ bin/
|
|||||||
gen/
|
gen/
|
||||||
out/
|
out/
|
||||||
|
|
||||||
|
# Kotlin folder
|
||||||
|
.kotlin/
|
||||||
|
|
||||||
# Gradle files
|
# Gradle files
|
||||||
.gradle/
|
.gradle/
|
||||||
build/
|
build/
|
||||||
|
|||||||
128
CHANGELOG
128
CHANGELOG
@@ -1,3 +1,131 @@
|
|||||||
|
KeePassDX(4.3.0)
|
||||||
|
* Manual change of app language #1884 #1990
|
||||||
|
* Fix autofill username detection #2276
|
||||||
|
|
||||||
|
KeePassDX(4.2.4)
|
||||||
|
* Fix remembering database location #2262
|
||||||
|
|
||||||
|
KeePassDX(4.2.3)
|
||||||
|
* Fix multiple Passkey selection #2253
|
||||||
|
* Fix database dialog subtitle #2254
|
||||||
|
* Fix save search info if URL present #2255
|
||||||
|
* Small fixes
|
||||||
|
|
||||||
|
KeePassDX(4.2.2)
|
||||||
|
* Fix database merge algorithm #2223
|
||||||
|
* Fix save search info #2243
|
||||||
|
* Fix Play Service as privileged app for Passkey Cross Device Authentication #2244
|
||||||
|
* Small fixes
|
||||||
|
|
||||||
|
KeePassDX(4.2.1)
|
||||||
|
* Fix Magikeyboard autosearch #2233
|
||||||
|
* Fix database merge #2223
|
||||||
|
* Fix dialog database action #2234
|
||||||
|
* Fix autofill selection #2238 #2235
|
||||||
|
* Small fixes
|
||||||
|
|
||||||
|
KeePassDX(4.2.0)
|
||||||
|
* Passkeys management #1421 #2097 (@cali-95)
|
||||||
|
* Confirm usage of passkey #2165 #2124
|
||||||
|
* Dialog to manage missing signature #2152 #2155 #2161 #2160
|
||||||
|
* Capture error #2159 #2215
|
||||||
|
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
|
||||||
|
* Search settings #2112 #2181 #2187 #2204
|
||||||
|
* Autofill refactoring #765 #2196
|
||||||
|
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
KeePassDX(4.1.0)
|
||||||
|
* Generate keyfile #1290
|
||||||
|
* Hide template group #1894
|
||||||
|
* Group count sum recursively #421
|
||||||
|
* Fix date fields #1695 #1710
|
||||||
|
* Fix distinct domain names #1105 #1820
|
||||||
|
* Resets the advanced unlock expiration #1600
|
||||||
|
* Password entropy #1490 #1355
|
||||||
|
* Upgrade to API 34 (Android 14) #1730
|
||||||
|
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889 #1289 #1600 #1467 #1870
|
||||||
|
|
||||||
|
KeePassDX(4.0.8)
|
||||||
|
* Fix graphical bug that prevented databases from being opened on some versions of Android #1848 #1850
|
||||||
|
|
||||||
|
KeePassDX(4.0.7)
|
||||||
|
* Prevent 0 Byte file with cache during a save exception #1620 #1594 #1680
|
||||||
|
* Fix inline suggestions in keyboard #1840
|
||||||
|
* Fix broken links by default #1755
|
||||||
|
* Fix UX by allowing validation in entry edition #1770
|
||||||
|
* Fix small bugs #1709
|
||||||
|
|
||||||
|
KeePassDX(4.0.6)
|
||||||
|
* Fix form filled recognition #1508 #1735 #1508 #1790 #1783 #1797 #1801 #1802 #1804 #1665
|
||||||
|
* Fix translations #1707 #1683 #1712
|
||||||
|
* Update APK verifier #1810
|
||||||
|
|
||||||
|
KeePassDX(4.0.5)
|
||||||
|
* Fix form filled recognition #1572 #1508
|
||||||
|
* Rollback password color #1686 #1490
|
||||||
|
|
||||||
|
KeePassDX(4.0.4)
|
||||||
|
* Fix form filled recognition #1572 #1677
|
||||||
|
* Fix device unlock #1682
|
||||||
|
* Fix password color #1490
|
||||||
|
|
||||||
|
KeePassDX(4.0.3)
|
||||||
|
* Fix "Save as" in Read Only mode #1666
|
||||||
|
* Fix username autofill #1665 #530 #1572 #1426 #1523 #1556 #1653 #1658 #1508 #1667
|
||||||
|
* Fix regex OTP recognition #1596
|
||||||
|
* Change password color dynamically #1490
|
||||||
|
* Small fixes #1641 #1656 #1649 #1400 #1674
|
||||||
|
|
||||||
KeePassDX(4.0.2)
|
KeePassDX(4.0.2)
|
||||||
* Fix Autofill with API 33
|
* Fix Autofill with API 33
|
||||||
|
|
||||||
|
|||||||
172
Gemfile.lock
172
Gemfile.lock
@@ -1,43 +1,49 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.6)
|
CFPropertyList (3.0.7)
|
||||||
|
base64
|
||||||
|
nkf
|
||||||
rexml
|
rexml
|
||||||
addressable (2.8.4)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
artifactory (3.0.15)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.794.0)
|
aws-partitions (1.1146.0)
|
||||||
aws-sdk-core (3.180.0)
|
aws-sdk-core (3.229.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.71.0)
|
logger
|
||||||
aws-sdk-core (~> 3, >= 3.177.0)
|
aws-sdk-kms (1.110.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sdk-core (~> 3, >= 3.228.0)
|
||||||
aws-sdk-s3 (1.132.0)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-core (~> 3, >= 3.179.0)
|
aws-sdk-s3 (1.196.1)
|
||||||
|
aws-sdk-core (~> 3, >= 3.228.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.6)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.6.0)
|
aws-sigv4 (1.12.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
|
base64 (0.3.0)
|
||||||
|
bigdecimal (3.2.2)
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
commander (4.6.0)
|
commander (4.6.0)
|
||||||
highline (~> 2.0.0)
|
highline (~> 2.0.0)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.7.0)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.6.20240107)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
emoji_regex (3.2.3)
|
emoji_regex (3.2.3)
|
||||||
excon (0.100.0)
|
excon (0.112.0)
|
||||||
faraday (1.10.3)
|
faraday (1.10.4)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
faraday-excon (~> 1.1)
|
faraday-excon (~> 1.1)
|
||||||
@@ -53,27 +59,27 @@ GEM
|
|||||||
faraday (>= 0.8.0)
|
faraday (>= 0.8.0)
|
||||||
http-cookie (~> 1.0.0)
|
http-cookie (~> 1.0.0)
|
||||||
faraday-em_http (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-excon (1.1.0)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
faraday-multipart (1.0.4)
|
faraday-multipart (1.1.1)
|
||||||
multipart-post (~> 2)
|
multipart-post (~> 2.0)
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.2)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
faraday-rack (1.0.0)
|
faraday-rack (1.0.0)
|
||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.3)
|
||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.1)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.2.7)
|
fastimage (2.4.0)
|
||||||
fastlane (2.214.0)
|
fastlane (2.228.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
aws-sdk-s3 (~> 1.0)
|
aws-sdk-s3 (~> 1.0)
|
||||||
babosa (>= 1.0.3, < 2.0.0)
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
bundler (>= 1.12.0, < 3.0.0)
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
colored
|
colored (~> 1.2)
|
||||||
commander (~> 4.6)
|
commander (~> 4.6)
|
||||||
dotenv (>= 2.1.1, < 3.0.0)
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
emoji_regex (>= 0.1, < 4.0)
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
@@ -82,34 +88,39 @@ GEM
|
|||||||
faraday-cookie_jar (~> 0.0.6)
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
faraday_middleware (~> 1.0)
|
faraday_middleware (~> 1.0)
|
||||||
fastimage (>= 2.1.0, < 3.0.0)
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
|
fastlane-sirp (>= 1.0.0)
|
||||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
google-apis-androidpublisher_v3 (~> 0.3)
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
google-apis-playcustomapp_v1 (~> 0.1)
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||||
google-cloud-storage (~> 1.31)
|
google-cloud-storage (~> 1.31)
|
||||||
highline (~> 2.0)
|
highline (~> 2.0)
|
||||||
|
http-cookie (~> 1.0.5)
|
||||||
json (< 3.0.0)
|
json (< 3.0.0)
|
||||||
jwt (>= 2.1.0, < 3)
|
jwt (>= 2.1.0, < 3)
|
||||||
mini_magick (>= 4.9.4, < 5.0.0)
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
multipart-post (>= 2.0.0, < 3.0.0)
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
naturally (~> 2.2)
|
naturally (~> 2.2)
|
||||||
optparse (~> 0.1.1)
|
optparse (>= 0.1.1, < 1.0.0)
|
||||||
plist (>= 3.1.0, < 4.0.0)
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
rubyzip (>= 2.0.0, < 3.0.0)
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
security (= 0.1.3)
|
security (= 0.1.5)
|
||||||
simctl (~> 1.6.3)
|
simctl (~> 1.6.3)
|
||||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
terminal-table (>= 1.4.5, < 2.0.0)
|
terminal-table (~> 3)
|
||||||
tty-screen (>= 0.6.3, < 1.0.0)
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
word_wrap (~> 1.0.0)
|
word_wrap (~> 1.0.0)
|
||||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
xcpretty (~> 0.3.0)
|
xcpretty (~> 0.4.1)
|
||||||
xcpretty-travis-formatter (>= 0.0.3)
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
fastlane-plugin-versioning_android (0.1.1)
|
fastlane-plugin-versioning_android (0.1.1)
|
||||||
|
fastlane-sirp (1.0.0)
|
||||||
|
sysrandom (~> 1.0)
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.46.0)
|
google-apis-androidpublisher_v3 (0.54.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-core (0.11.1)
|
google-apis-core (0.11.3)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
@@ -117,64 +128,66 @@ GEM
|
|||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
rexml
|
||||||
webrick
|
|
||||||
google-apis-iamcredentials_v1 (0.17.0)
|
google-apis-iamcredentials_v1 (0.17.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-playcustomapp_v1 (0.13.0)
|
google-apis-playcustomapp_v1 (0.13.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-storage_v1 (0.19.0)
|
google-apis-storage_v1 (0.31.0)
|
||||||
google-apis-core (>= 0.9.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-cloud-core (1.6.0)
|
google-cloud-core (1.8.0)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (1.6.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 0.17.3, < 3.0)
|
||||||
google-cloud-errors (1.3.1)
|
google-cloud-errors (1.5.0)
|
||||||
google-cloud-storage (1.44.0)
|
google-cloud-storage (1.47.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-apis-storage_v1 (~> 0.19.0)
|
google-apis-storage_v1 (~> 0.31.0)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.7.0)
|
googleauth (1.8.1)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
highline (2.0.3)
|
highline (2.0.3)
|
||||||
http-cookie (1.0.5)
|
http-cookie (1.0.8)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httpclient (2.8.3)
|
httpclient (2.9.0)
|
||||||
|
mutex_m
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.6.3)
|
json (2.13.2)
|
||||||
jwt (2.7.1)
|
jwt (2.10.2)
|
||||||
memoist (0.16.2)
|
base64
|
||||||
mini_magick (4.12.0)
|
logger (1.7.0)
|
||||||
mini_mime (1.1.2)
|
mini_magick (4.13.2)
|
||||||
multi_json (1.15.0)
|
mini_mime (1.1.5)
|
||||||
multipart-post (2.3.0)
|
multi_json (1.17.0)
|
||||||
nanaimo (0.3.0)
|
multipart-post (2.4.1)
|
||||||
naturally (2.2.1)
|
mutex_m (0.3.0)
|
||||||
optparse (0.1.1)
|
nanaimo (0.4.0)
|
||||||
|
naturally (2.3.0)
|
||||||
|
nkf (0.2.0)
|
||||||
|
optparse (0.6.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.7.0)
|
plist (3.7.2)
|
||||||
public_suffix (5.0.3)
|
public_suffix (6.0.2)
|
||||||
rake (13.0.6)
|
rake (13.3.0)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.6)
|
rexml (3.4.1)
|
||||||
rouge (2.0.7)
|
rouge (3.28.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.4.1)
|
||||||
security (0.1.3)
|
security (0.1.5)
|
||||||
signet (0.17.0)
|
signet (0.20.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
@@ -182,30 +195,27 @@ GEM
|
|||||||
simctl (1.6.10)
|
simctl (1.6.10)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
|
sysrandom (1.0.5)
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (1.8.0)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
tty-cursor (0.7.1)
|
tty-cursor (0.7.1)
|
||||||
tty-screen (0.8.1)
|
tty-screen (0.8.2)
|
||||||
tty-spinner (0.9.3)
|
tty-spinner (0.9.3)
|
||||||
tty-cursor (~> 0.7)
|
tty-cursor (~> 0.7)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unf (0.1.4)
|
unicode-display_width (2.6.0)
|
||||||
unf_ext
|
|
||||||
unf_ext (0.0.8.2)
|
|
||||||
unicode-display_width (1.8.0)
|
|
||||||
webrick (1.8.1)
|
|
||||||
word_wrap (1.0.0)
|
word_wrap (1.0.0)
|
||||||
xcodeproj (1.22.0)
|
xcodeproj (1.27.0)
|
||||||
CFPropertyList (>= 2.3.3, < 4.0)
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
atomos (~> 0.1.3)
|
atomos (~> 0.1.3)
|
||||||
claide (>= 1.0.2, < 2.0)
|
claide (>= 1.0.2, < 2.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
nanaimo (~> 0.3.0)
|
nanaimo (~> 0.4.0)
|
||||||
rexml (~> 3.2.4)
|
rexml (>= 3.3.6, < 4.0)
|
||||||
xcpretty (0.3.0)
|
xcpretty (0.4.1)
|
||||||
rouge (~> 2.0.7)
|
rouge (~> 3.28.0)
|
||||||
xcpretty-travis-formatter (1.0.1)
|
xcpretty-travis-formatter (1.0.1)
|
||||||
xcpretty (~> 0.2, >= 0.0.7)
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
@@ -217,4 +227,4 @@ DEPENDENCIES
|
|||||||
fastlane-plugin-versioning_android
|
fastlane-plugin-versioning_android
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.1.4
|
2.6.9
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -1,24 +1,26 @@
|
|||||||
# Android KeePassDX
|
# Android KeePassDX
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
|
<img alt="KeePassDX Icon" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password safe and manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
<img alt="KeePassDX Screenshot" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Create database files / entries and groups.
|
- **Passkeys** for authentication and **local storage of private keys**.
|
||||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
- **Biometric recognition** for fast unlocking (fingerprint / face unlock / …).
|
||||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
- **One-Time Password** management (HOTP / TOTP) for two-factor authentication (2FA).
|
||||||
|
- **Autofill** for easy form filling with passwords.
|
||||||
|
- **Magikeyboard** to efficiently fill in any field.
|
||||||
|
- Create **encrypted database files**.
|
||||||
|
- Organisation of credentials by **entry** and in **group** trees.
|
||||||
- Allows opening and **copying URI / URL fields quickly**.
|
- Allows opening and **copying URI / URL fields quickly**.
|
||||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
- Dynamic **templates** for each type of entry.
|
||||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
|
||||||
- Material design with **themes**.
|
|
||||||
- **Auto-Fill** and Integration.
|
|
||||||
- Field filling **keyboard**.
|
|
||||||
- Dynamic **templates**
|
|
||||||
- **History** of each entry.
|
- **History** of each entry.
|
||||||
- Precise management of **settings**.
|
- Precise management of **settings**.
|
||||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
- Material design with **themes**.
|
||||||
|
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||||
|
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||||
|
- Code written in **native languages** (Kotlin / Java / JNI / C).
|
||||||
|
|
||||||
KeePassDX is **open source** and **ad-free**.
|
KeePassDX is **open source** and **ad-free**.
|
||||||
|
|
||||||
@@ -48,17 +50,39 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
|||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
*[F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies that all the libraries and app code is libre software.*
|
*[F-Droid](https://f-droid.org/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies all the libraries and app code is libre software.*
|
||||||
|
|
||||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
| Source | Status | [Version](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ#why-a-libre-and-free-version) |
|
||||||
alt="Get it on F-Droid"
|
|--------|--------|---------|
|
||||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
| [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) |
|
||||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
| [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) |  | Libre |
|
||||||
alt="Get it on Google Play"
|
| [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) |
|
||||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
| [GitHub](https://github.com/Kunzisoft/KeePassDX/releases) / [Obtainium](https://github.com/ImranR98/Obtainium) |  | Free & Libre |
|
||||||
[<img src="https://raw.githubusercontent.com/Kunzisoft/Github-badge/main/get-it-on-github.png"
|
|
||||||
alt="Get it on Github"
|
## Package authenticity from GitHub
|
||||||
height="80">](https://github.com/Kunzisoft/KeePassDX/releases)
|
- Download the app from [GitHub releases](https://github.com/Kunzisoft/KeePassDX/releases/latest)
|
||||||
|
- Install [`apksigner`](https://developer.android.com/tools/apksigner) from [Android Studio](https://developer.android.com/studio)
|
||||||
|
- Open the directory where you saved the downloaded file in the Terminal
|
||||||
|
- Make sure that you have `apksigner` installed by running:
|
||||||
|
```shell
|
||||||
|
apksigner --version
|
||||||
|
```
|
||||||
|
- Depending on the APK file you downloaded, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
apksigner verify --verbose --print-certs -min-sdk-version 24 KeePassDX-*.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
You should get this output :
|
||||||
|
```shell
|
||||||
|
Verified using v2 scheme (APK Signature Scheme v2): true
|
||||||
|
...
|
||||||
|
Number of signers: 1
|
||||||
|
Signer #1 certificate SHA-256 digest: 7d55b8af210381aabf960f07e17cf7857b6d2a642ca2da6bf0bdf1b200362f04
|
||||||
|
...
|
||||||
|
Signer #1 public key SHA-256 digest: 5d261d3176db1e077b80112824d9390167f3be0561827e42112ed6b71192db81
|
||||||
|
```
|
||||||
|
If it's the case, this means that the APK was well built by the author of KeePassDX.
|
||||||
|
|
||||||
## Frequently Asked Questions
|
## Frequently Asked Questions
|
||||||
|
|
||||||
@@ -74,7 +98,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2023 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
Copyright © 2025 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||||
|
|
||||||
This file is part of KeePassDX.
|
This file is part of KeePassDX.
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ apply plugin: 'kotlin-kapt'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.kunzisoft.keepass'
|
namespace 'com.kunzisoft.keepass'
|
||||||
compileSdkVersion 33
|
compileSdkVersion 36
|
||||||
buildToolsVersion "33.0.2"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 15
|
minSdkVersion 19
|
||||||
targetSdkVersion 33
|
targetSdkVersion 35
|
||||||
versionCode = 125
|
versionCode = 150
|
||||||
versionName = "4.0.2"
|
versionName = "4.3.0"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -36,6 +35,17 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
|
dependenciesInfo {
|
||||||
|
// Disables dependency metadata when building APKs.
|
||||||
|
includeInApk = false
|
||||||
|
// Disables dependency metadata when building Android App Bundles.
|
||||||
|
includeInBundle = false
|
||||||
|
}
|
||||||
|
|
||||||
flavorDimensions "version"
|
flavorDimensions "version"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
libre {
|
libre {
|
||||||
@@ -85,12 +95,24 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
|
||||||
|
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
|
||||||
|
}
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +131,7 @@ dependencies {
|
|||||||
implementation 'androidx.media:media:1.6.0'
|
implementation 'androidx.media:media:1.6.0'
|
||||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation "androidx.core:core-ktx:$android_core_version"
|
implementation "androidx.core:core-ktx:$android_core_version"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
||||||
implementation "com.google.android.material:material:$android_material_version"
|
implementation "com.google.android.material:material:$android_material_version"
|
||||||
// Token auto complete
|
// Token auto complete
|
||||||
@@ -120,7 +143,7 @@ dependencies {
|
|||||||
// Autofill
|
// Autofill
|
||||||
implementation "androidx.autofill:autofill:1.1.0"
|
implementation "androidx.autofill:autofill:1.1.0"
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.10.13'
|
implementation 'joda-time:joda-time:2.13.0'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
||||||
// Education
|
// Education
|
||||||
@@ -131,11 +154,13 @@ dependencies {
|
|||||||
// Password generator
|
// Password generator
|
||||||
implementation 'me.gosimple:nbvcxz:1.5.0'
|
implementation 'me.gosimple:nbvcxz:1.5.0'
|
||||||
|
|
||||||
|
// Credentials Provider
|
||||||
|
implementation "androidx.credentials:credentials:1.2.2"
|
||||||
|
|
||||||
// Modules import
|
// Modules import
|
||||||
implementation project(path: ':database')
|
implementation project(path: ':database')
|
||||||
implementation project(path: ':icon-pack')
|
implementation project(path: ':icon-pack')
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
76
app/src/free/assets/passkeys_privileged_apps_community.json
Normal file
76
app/src/free/assets/passkeys_privileged_apps_community.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.github.forkmaintainers.iceraven",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.chromium.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A8:56:48:50:79:BC:B3:57:BF:BE:69:BA:19:A9:BA:43:CD:0A:D9:AB:22:67:52:C7:80:B6:88:8A:FD:48:21:6B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.cromite.cromite",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "63:3F:A4:1D:82:11:D6:D0:91:6A:81:9B:89:66:8C:6D:E9:2E:64:23:2D:A6:7F:9D:16:FD:81:C3:B7:E9:23:FF"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.ironfoxoss.ironfox",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.ironfoxoss.ironfox.nightly",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_fdroid",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "06:66:53:58:EF:D8:BA:05:BE:23:6A:47:A1:2C:B0:95:8D:7D:75:DD:93:9D:77:C2:B3:1F:53:98:53:7E:BD:C5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
820
app/src/free/assets/passkeys_privileged_apps_google.json
Normal file
820
app/src/free/assets/passkeys_privileged_apps_google.json
Normal file
@@ -0,0 +1,820 @@
|
|||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.android.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.dev",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.chromium.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.google.android.apps.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_webauthndebug",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.firefox",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.firefox_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.focus",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_aurora",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.rocket",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fenix",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "50:04:77:90:88:E7:F9:88:D5:BC:5C:C5:F8:79:8F:EB:F4:F8:CD:08:4A:1B:2A:46:EF:D4:C8:EE:4A:EA:F2:11"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fenix.debug",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.focus.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.focus.nightly",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.klar",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.reference.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "B0:09:90:E3:0F:9D:81:5D:2E:BC:7B:9B:B2:21:CE:47:E5:C9:D5:17:AA:C7:0E:7F:D5:95:B1:E5:3E:9A:4B:14"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.dev",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.rolling",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.local",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser_nightly",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "app.vanadium.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser.snapshot",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser.sopranos",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.citrix.Receiver",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:D1:12:67:10:69:AB:36:4E:F9:BE:73:9A:B7:B5:EE:15:E1:CD:E9:D8:75:7B:1B:F0:64:F5:0C:55:68:9A:49"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "CE:B2:23:D7:77:09:F2:B6:BC:0B:3A:78:36:F5:A5:AF:4C:E1:D3:55:F4:A7:28:86:F7:9D:F8:0D:C9:D6:12:2E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AA:D0:D4:57:E6:33:C3:78:25:77:30:5B:C1:B2:D9:E3:81:41:C7:21:DF:0D:AA:6E:29:07:2F:C4:1D:34:F0:AB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.android.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C9:00:9D:01:EB:F9:F5:D0:30:2B:C7:1B:2F:E9:AA:9A:47:A4:32:BB:A1:73:08:A3:11:1B:75:D7:B2:14:90:25"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.sec.android.app.sbrowser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.sec.android.app.sbrowser.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.google.android.gms",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "7C:E8:3C:1B:71:F3:D5:72:FE:D0:4C:8D:40:C5:CB:10:FF:75:E6:D8:7D:9D:F6:FB:D5:3F:04:68:C2:90:50:53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "D2:2C:C5:00:29:9F:B2:28:73:A0:1A:01:0D:E1:C8:2F:BE:4D:06:11:19:B9:48:14:DD:30:1D:AB:50:CB:76:78"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.alpha",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.corp",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.broteam",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.talonsec.talon",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A3:66:03:44:A6:F6:AF:CA:81:8C:BF:43:96:A2:3C:CF:D5:ED:7A:78:1B:B4:A3:D1:85:03:01:E2:F4:6D:23:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E2:A5:64:74:EA:23:7B:06:67:B6:F5:2C:DC:E9:04:5E:24:88:3B:AE:D0:82:59:9A:A2:DF:0B:60:3A:CF:6A:3B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.talonsec.talon_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F5:86:62:7A:32:C8:9F:E6:7E:00:6D:B1:8C:34:31:9E:01:7F:B3:B2:BE:D6:9D:01:01:B7:F9:43:E7:7C:48:AE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9A:A1:25:D5:E5:5E:3F:B0:DE:96:72:D9:A9:5D:04:65:3F:49:4A:1E:C3:EE:76:1E:94:C4:4E:5D:2F:65:8E:2F"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.duckduckgo.mobile.android.debug",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C4:F0:9E:2B:D7:25:AD:F5:AD:92:0B:A2:80:27:66:AC:16:4A:C1:53:B3:EA:9E:08:48:B0:57:98:37:F7:6A:29"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.duckduckgo.mobile.android",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "BB:7B:B3:1C:57:3C:46:A1:DA:7F:C5:C5:28:A6:AC:F4:32:10:84:56:FE:EC:50:81:0C:7F:33:69:4E:B3:D2:D4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.naver.whale",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "0B:8B:85:23:BB:4A:EF:FA:34:6E:4B:DD:4F:BF:7D:19:34:50:56:9A:A1:4A:AA:D4:AD:FD:94:A3:F7:B2:27:BB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.fido.fido2client",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "FC:98:DA:E6:3A:D3:96:26:C8:C6:7F:BE:83:F2:F0:6F:74:93:2A:9C:D1:46:B9:2C:EC:FC:6A:04:7A:90:43:86"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.heytap.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AF:F8:A7:49:CF:0E:7D:75:44:65:D0:FB:FA:7B:8D:0C:64:5E:22:5C:10:C6:E2:32:AD:A0:D9:74:88:36:B8:E5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A8:FE:A4:CA:FB:93:32:DA:26:B8:E6:81:08:17:C1:DA:90:A5:03:0E:35:A6:0A:79:E0:6C:90:97:AA:C6:A4:42"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.Island",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "D9:C3:39:AC:9C:3A:EE:E1:75:1D:85:8C:35:D9:BA:C5:CC:87:B3:CE:76:30:93:F0:F5:10:64:F5:A2:F6:9B:04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.IslandCanary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "90:17:13:23:45:6E:6F:39:CB:FD:CF:B2:56:BE:1D:CF:F3:BC:1C:59:8A:15:93:30:E4:97:73:D0:4C:B9:C9:05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.IslandBeta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "35:31:83:1A:9E:2B:21:1D:E6:AA:C3:69:4B:45:83:6E:56:09:B9:D7:D0:04:C3:1B:21:87:40:FB:77:17:38:D1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.IslandDev",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.island.intune",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C2:38:24:15:41:20:A0:8F:C3:95:42:AC:D8:2A:E9:24:94:78:80:1E:47:FD:6C:66:2B:18:1C:28:CA:7E:59:4E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.island.canary.intune",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "1E:16:74:BB:79:EA:09:FB:37:CF:9F:1B:07:1B:1D:51:8D:46:03:0E:D3:EE:F2:C1:4E:AD:93:9E:C6:EE:3A:4C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.island.beta.intune",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "D2:5E:AD:F6:1C:E6:36:6C:A4:23:A4:7F:C4:DB:9B:8C:9C:8A:35:B4:B0:19:E8:D9:82:FB:D0:8A:D9:DB:49:5A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.island.dev.intune",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "net.quetta.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "F1:38:00:4F:38:04:51:D4:8A:05:2B:B3:A3:EF:17:24:23:D4:B0:D0:C8:A3:AA:DD:FB:DB:66:30:31:48:EC:A4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "cz.seznam.sbrowser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "DB:95:40:66:10:78:83:6E:4E:B1:66:F6:9E:F4:07:30:9E:8D:AE:33:34:68:5E:C8:F6:FA:2F:13:81:B9:AC:F6"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.opera.mini.native",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.opera.mini.native.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -9,6 +9,10 @@
|
|||||||
android:anyDensity="true" />
|
android:anyDensity="true" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.POST_NOTIFICATIONS" />
|
android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
@@ -39,8 +43,10 @@
|
|||||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
|
android:supportsRtl="true"
|
||||||
android:theme="@style/KeepassDXStyle.Night"
|
android:theme="@style/KeepassDXStyle.Night"
|
||||||
tools:targetApi="s">
|
tools:targetApi="s"
|
||||||
|
tools:ignore="CredentialDependency">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="${googleAndroidBackupAPIKey}" />
|
android:value="${googleAndroidBackupAPIKey}" />
|
||||||
@@ -118,7 +124,7 @@
|
|||||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.default_searchable"
|
android:name="android.app.default_searchable"
|
||||||
android:value="com.kunzisoft.keepass.search.SearchResults"
|
android:value="com.kunzisoft.keepass.search.SearchResults"
|
||||||
@@ -145,7 +151,7 @@
|
|||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||||
android:windowSoftInputMode="adjustPan" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<!-- About and Settings -->
|
<!-- About and Settings -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||||
@@ -154,24 +160,40 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" />
|
||||||
android:theme="@style/Theme.Transparent"
|
|
||||||
android:configChanges="keyboardHidden"
|
|
||||||
android:excludeFromRecents="true"/>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
|
android:label="@string/keyboard_setting_label"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity"
|
||||||
|
tools:targetApi="26" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.settings.PasskeysSettingsActivity"
|
||||||
|
tools:targetApi="34" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
|
||||||
android:theme="@style/Theme.Transparent" />
|
android:theme="@style/Theme.Transparent"
|
||||||
|
android:exported="false"
|
||||||
|
android:excludeFromRecents="true" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent"
|
||||||
|
android:configChanges="keyboardHidden"
|
||||||
|
android:exported="false"
|
||||||
|
android:excludeFromRecents="true" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:excludeFromRecents="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -187,34 +209,41 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
||||||
android:label="@string/keyboard_setting_label"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:exported="true">
|
android:configChanges="keyboardHidden"
|
||||||
<intent-filter>
|
android:exported="false"
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
android:excludeFromRecents="true"
|
||||||
</intent-filter>
|
tools:targetApi="upside_down_cake" />
|
||||||
</activity>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
|
<service
|
||||||
|
android:name="com.kunzisoft.keepass.services.DeviceUnlockNotificationService"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<!-- Receiver for Autofill -->
|
<!-- Receiver for Autofill -->
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
android:name="com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService"
|
||||||
android:label="@string/autofill_service_name"
|
android:label="@string/app_name"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -225,7 +254,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
android:name="com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService"
|
||||||
android:label="@string/keyboard_label"
|
android:label="@string/keyboard_label"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||||
@@ -236,9 +265,21 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.credentialprovider.passkey.PasskeyProviderService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="true"
|
||||||
|
android:label="@string/passkey_service_name"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
||||||
|
tools:targetApi="upside_down_cake">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.credentials.CredentialProviderService" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.credentials.provider"
|
||||||
|
android:resource="@xml/provider" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ import android.graphics.RectF
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.*
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.ScaleGestureDetector
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.view.animation.Interpolator
|
import android.view.animation.Interpolator
|
||||||
@@ -202,7 +206,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
override fun onDown(e: MotionEvent): Boolean = true
|
override fun onDown(e: MotionEvent): Boolean = true
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
e1: MotionEvent,
|
e1: MotionEvent?,
|
||||||
e2: MotionEvent,
|
e2: MotionEvent,
|
||||||
distanceX: Float,
|
distanceX: Float,
|
||||||
distanceY: Float
|
distanceY: Float
|
||||||
@@ -220,7 +224,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
e1: MotionEvent,
|
e1: MotionEvent?,
|
||||||
e2: MotionEvent,
|
e2: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float
|
||||||
@@ -355,8 +359,6 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
|
|
||||||
isViewTranslateAnimationRunning = true
|
isViewTranslateAnimationRunning = true
|
||||||
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
imageView.run {
|
imageView.run {
|
||||||
val translationY = if (velY > 0) {
|
val translationY = if (velY > 0) {
|
||||||
originalViewBounds.top + height - top
|
originalViewBounds.top + height - top
|
||||||
@@ -392,41 +394,6 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) {
|
|
||||||
originalViewBounds.top + imageView.height - imageView.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processFlingBitmap(velocityX: Float, velocityY: Float) {
|
private fun processFlingBitmap(velocityX: Float, velocityY: Float) {
|
||||||
@@ -653,8 +620,6 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
|
|
||||||
private fun restoreViewTransform() {
|
private fun restoreViewTransform() {
|
||||||
val imageView = imageViewRef.get() ?: return
|
val imageView = imageViewRef.get() ?: return
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
imageView.run {
|
imageView.run {
|
||||||
animate()
|
animate()
|
||||||
.setDuration(restoreAnimationDuration)
|
.setDuration(restoreAnimationDuration)
|
||||||
@@ -683,41 +648,12 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDragToDismissAnimation() {
|
private fun startDragToDismissAnimation() {
|
||||||
val imageView = imageViewRef.get() ?: return
|
val imageView = imageViewRef.get() ?: return
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
imageView.run {
|
imageView.run {
|
||||||
val translationY = if (y - initialY > 0) {
|
val translationY = if (y - initialY > 0) {
|
||||||
originalViewBounds.top + height - top
|
originalViewBounds.top + height - top
|
||||||
@@ -753,37 +689,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processFlingToDismiss(velocityY: Float) {
|
private fun processFlingToDismiss(velocityY: Float) {
|
||||||
|
|||||||
@@ -24,13 +24,14 @@ import android.os.Bundle
|
|||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||||
import com.kunzisoft.keepass.utils.getPackageInfoCompat
|
import com.kunzisoft.keepass.utils.getPackageInfoCompat
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ class AboutActivity : StylishActivity() {
|
|||||||
var version: String
|
var version: String
|
||||||
var build: String
|
var build: String
|
||||||
try {
|
try {
|
||||||
version = packageManager.getPackageInfoCompat(packageName).versionName
|
version = packageManager.getPackageInfoCompat(packageName).versionName ?: ""
|
||||||
build = BuildConfig.BUILD_VERSION
|
build = BuildConfig.BUILD_VERSION
|
||||||
} catch (e: NameNotFoundException) {
|
} catch (e: NameNotFoundException) {
|
||||||
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
||||||
@@ -76,6 +77,8 @@ class AboutActivity : StylishActivity() {
|
|||||||
movementMethod = LinkMovementMethod.getInstance()
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
|
textDirection = View.TEXT_DIRECTION_ANY_RTL
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
||||||
|
|||||||
@@ -1,269 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
|
||||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
|
||||||
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.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|
||||||
import com.kunzisoft.keepass.utils.WebDomain
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
|
||||||
AutofillHelper.buildActivityResultLauncher(this, true)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
// Retrieve selection mode
|
|
||||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
|
||||||
when (specialMode) {
|
|
||||||
SpecialMode.SELECTION -> {
|
|
||||||
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
|
|
||||||
// To pass extra inline request
|
|
||||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
|
|
||||||
}
|
|
||||||
// Build search param
|
|
||||||
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
|
||||||
WebDomain.getConcreteWebDomain(
|
|
||||||
this,
|
|
||||||
searchInfo.webDomain
|
|
||||||
) { concreteWebDomain ->
|
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
|
||||||
val assistStructure = AutofillHelper
|
|
||||||
.retrieveAutofillComponent(intent)
|
|
||||||
?.assistStructure
|
|
||||||
val newAutofillComponent = if (assistStructure != null) {
|
|
||||||
AutofillComponent(
|
|
||||||
assistStructure,
|
|
||||||
compatInlineSuggestionsRequest
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
searchInfo.webDomain = concreteWebDomain
|
|
||||||
launchSelection(database, newAutofillComponent, searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove bundle
|
|
||||||
intent.removeExtra(KEY_SELECTION_BUNDLE)
|
|
||||||
}
|
|
||||||
SpecialMode.REGISTRATION -> {
|
|
||||||
// To register info
|
|
||||||
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
|
|
||||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
|
||||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
|
||||||
searchInfo.webDomain = concreteWebDomain
|
|
||||||
launchRegistration(database, searchInfo, registerInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// Not an autofill call
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchSelection(database: ContextualDatabase?,
|
|
||||||
autofillComponent: AutofillComponent?,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
if (autofillComponent == null) {
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
|
||||||
PreferencesUtil.applicationIdBlocklist(this))
|
|
||||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
|
||||||
PreferencesUtil.webDomainBlocklist(this))) {
|
|
||||||
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 {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showBlockRestartMessage() {
|
|
||||||
// If item not allowed, show a toast
|
|
||||||
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showReadOnlySaveMessage() {
|
|
||||||
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
|
||||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
|
||||||
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
|
||||||
|
|
||||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
|
||||||
|
|
||||||
fun getPendingIntentForSelection(context: Context,
|
|
||||||
searchInfo: SearchInfo? = null,
|
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
|
|
||||||
return PendingIntent.getActivity(context, 0,
|
|
||||||
// Doesn't work with direct extra Parcelable (don't know why?)
|
|
||||||
// Wrap into a bundle to bypass the problem
|
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
|
||||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
|
||||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
|
||||||
} else {
|
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPendingIntentForRegistration(context: Context,
|
|
||||||
registerInfo: RegisterInfo): PendingIntent {
|
|
||||||
return PendingIntent.getActivity(context, 0,
|
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
|
||||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
|
||||||
},
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
|
||||||
} else {
|
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchForRegistration(context: Context,
|
|
||||||
registerInfo: RegisterInfo) {
|
|
||||||
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
|
||||||
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@ import android.content.Intent
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -38,13 +37,10 @@ import androidx.activity.result.ActivityResultLauncher
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
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.BlendModeColorFilterCompat
|
||||||
import androidx.core.graphics.BlendModeCompat
|
import androidx.core.graphics.BlendModeCompat
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
@@ -54,15 +50,15 @@ import com.google.android.material.tabs.TabLayout
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
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.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
@@ -73,7 +69,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.view.WindowInsetPosition
|
import com.kunzisoft.keepass.view.WindowInsetPosition
|
||||||
import com.kunzisoft.keepass.view.applyWindowInsets
|
import com.kunzisoft.keepass.view.applyWindowInsets
|
||||||
@@ -83,11 +79,13 @@ import com.kunzisoft.keepass.view.hideByFading
|
|||||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
|
import java.util.EnumSet
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class EntryActivity : DatabaseLockActivity() {
|
class EntryActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
private var footer: ViewGroup? = null
|
private var footer: ViewGroup? = null
|
||||||
|
private var container: View? = null
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||||
private var appBarLayout: AppBarLayout? = null
|
private var appBarLayout: AppBarLayout? = null
|
||||||
@@ -127,6 +125,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
private var mBackgroundColor: Int? = null
|
private var mBackgroundColor: Int? = null
|
||||||
private var mForegroundColor: Int? = null
|
private var mForegroundColor: Int? = null
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -139,6 +139,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
// Get views
|
// Get views
|
||||||
footer = findViewById(R.id.activity_entry_footer)
|
footer = findViewById(R.id.activity_entry_footer)
|
||||||
|
container = findViewById(R.id.activity_entry_container)
|
||||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||||
appBarLayout = findViewById(R.id.app_bar)
|
appBarLayout = findViewById(R.id.app_bar)
|
||||||
@@ -154,8 +155,12 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
setTransparentNavigationBar {
|
setTransparentNavigationBar {
|
||||||
// To fix margin with API 27
|
// To fix margin with API 27
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
||||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
container?.applyWindowInsets(EnumSet.of(
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
WindowInsetPosition.TOP_MARGINS,
|
||||||
|
WindowInsetPosition.BOTTOM_MARGINS,
|
||||||
|
WindowInsetPosition.START_MARGINS,
|
||||||
|
WindowInsetPosition.END_MARGINS,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty title
|
// Empty title
|
||||||
@@ -261,7 +266,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
mIcon = entryInfo.icon
|
mIcon = entryInfo.icon
|
||||||
// Assign title text
|
// Assign title text
|
||||||
val entryTitle =
|
val entryTitle =
|
||||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
entryInfo.title.ifEmpty { entryInfo.id.asHexString() }
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
// Assign tags
|
// Assign tags
|
||||||
@@ -309,11 +314,11 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
launch(
|
launch(
|
||||||
this,
|
activity = this,
|
||||||
database,
|
database = database,
|
||||||
historySelected.nodeId,
|
entryId = historySelected.nodeId,
|
||||||
historySelected.historyPosition,
|
historyPosition = historySelected.historyPosition,
|
||||||
mEntryActivityResultLauncher
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,9 +332,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
return coordinatorLayout
|
return coordinatorLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mEntryViewModel.loadDatabase(database)
|
mEntryViewModel.loadDatabase(database)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,11 +479,12 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
mMainEntryId?.let { entryId ->
|
mMainEntryId?.let { entryId ->
|
||||||
EntryEditActivity.launchToUpdate(
|
EntryEditActivity.launch(
|
||||||
this,
|
activity = this,
|
||||||
database,
|
database = database,
|
||||||
entryId,
|
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||||
mEntryActivityResultLauncher
|
nodeId = entryId,
|
||||||
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,7 +522,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||||
setResult(Activity.RESULT_OK, this)
|
setResult(RESULT_OK, this)
|
||||||
}
|
}
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
@@ -531,34 +536,22 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open standard Entry activity
|
* Open standard or history Entry activity
|
||||||
*/
|
*/
|
||||||
fun launch(activity: Activity,
|
fun launch(
|
||||||
|
activity: Activity,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
historyPosition: Int? = null,
|
||||||
if (database.loaded) {
|
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
) {
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
|
||||||
activityResultLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open history Entry activity
|
|
||||||
*/
|
|
||||||
fun launch(activity: Activity,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
historyPosition: Int,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
|
historyPosition?.let {
|
||||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||||
|
}
|
||||||
activityResultLauncher.launch(intent)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,21 +30,20 @@ import android.util.Log
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.datepicker.MaterialDatePicker
|
import com.google.android.material.datepicker.MaterialDatePicker
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.timepicker.MaterialTimePicker
|
import com.google.android.material.timepicker.MaterialTimePicker
|
||||||
@@ -56,37 +55,40 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecialModeResponseAndSetResult
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
|
||||||
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.utils.TimeUtil.datePickerToDataDate
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.view.ToolbarAction
|
import com.kunzisoft.keepass.view.ToolbarAction
|
||||||
@@ -96,9 +98,11 @@ import com.kunzisoft.keepass.view.asError
|
|||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
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.ColorPickerViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.EnumSet
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class EntryEditActivity : DatabaseLockActivity(),
|
class EntryEditActivity : DatabaseLockActivity(),
|
||||||
@@ -108,8 +112,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
ReplaceFileDialogFragment.ActionChooseListener {
|
ReplaceFileDialogFragment.ActionChooseListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var footer: ViewGroup? = null
|
private var footer: View? = null
|
||||||
private var container: ViewGroup? = null
|
private var container: View? = null
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var scrollView: NestedScrollView? = null
|
private var scrollView: NestedScrollView? = null
|
||||||
private var templateSelectorSpinner: Spinner? = null
|
private var templateSelectorSpinner: Spinner? = null
|
||||||
@@ -153,8 +157,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// To ask data lost only one time
|
override fun manageDatabaseInfo(): Boolean = true
|
||||||
private var backPressedAlreadyApproved = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -179,8 +182,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// To apply fit window with transparency
|
// To apply fit window with transparency
|
||||||
setTransparentNavigationBar(applyToStatusBar = true) {
|
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||||
container?.applyWindowInsets(WindowInsetPosition.TOP)
|
container?.applyWindowInsets(EnumSet.of(
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
WindowInsetPosition.TOP_MARGINS,
|
||||||
|
WindowInsetPosition.BOTTOM_MARGINS,
|
||||||
|
WindowInsetPosition.START_MARGINS,
|
||||||
|
WindowInsetPosition.END_MARGINS,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||||
@@ -204,8 +211,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
mDatabase,
|
mDatabase,
|
||||||
entryId,
|
entryId,
|
||||||
parentId,
|
parentId,
|
||||||
EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent),
|
intent.retrieveRegisterInfo()
|
||||||
EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
?: intent.retrieveSearchInfo()?.toRegisterInfo()
|
||||||
)
|
)
|
||||||
|
|
||||||
// To retrieve attachment
|
// To retrieve attachment
|
||||||
@@ -301,7 +308,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
// Launch the time picker
|
// Launch the time picker
|
||||||
MaterialTimePicker.Builder().build().apply {
|
MaterialTimePicker.Builder().build().apply {
|
||||||
addOnPositiveButtonClickListener {
|
addOnPositiveButtonClickListener {
|
||||||
mEntryEditViewModel.selectTime(this.hour, this.minute)
|
mEntryEditViewModel.selectTime(DataTime(this.hour, this.minute))
|
||||||
}
|
}
|
||||||
show(supportFragmentManager, "TimePickerFragment")
|
show(supportFragmentManager, "TimePickerFragment")
|
||||||
}
|
}
|
||||||
@@ -309,7 +316,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
// Launch the date picker
|
// Launch the date picker
|
||||||
MaterialDatePicker.Builder.datePicker().build().apply {
|
MaterialDatePicker.Builder.datePicker().build().apply {
|
||||||
addOnPositiveButtonClickListener {
|
addOnPositiveButtonClickListener {
|
||||||
mEntryEditViewModel.selectDate(it)
|
mEntryEditViewModel.selectDate(datePickerToDataDate(it))
|
||||||
}
|
}
|
||||||
show(supportFragmentManager, "DatePickerFragment")
|
show(supportFragmentManager, "DatePickerFragment")
|
||||||
}
|
}
|
||||||
@@ -372,23 +379,30 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
} ?: run {
|
} ?: run {
|
||||||
updateEntry(entrySave.oldEntry, entrySave.newEntry)
|
updateEntry(entrySave.oldEntry, entrySave.newEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't wait for saving if it's to provide autofill
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
|
||||||
{},
|
|
||||||
{},
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
|
||||||
},
|
|
||||||
{ _, _ ->
|
|
||||||
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mEntryEditViewModel.uiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
EntryEditViewModel.UIState.Loading -> {}
|
||||||
|
EntryEditViewModel.UIState.ShowOverwriteMessage -> {
|
||||||
|
if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
|
||||||
|
AlertDialog.Builder(this@EntryEditActivity)
|
||||||
|
.setTitle(R.string.warning_overwrite_data_title)
|
||||||
|
.setMessage(R.string.warning_overwrite_data_description)
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
|
||||||
|
}
|
||||||
|
.create().show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,13 +415,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
mAllowCustomFields = database.allowEntryCustomFields() == true
|
||||||
mAllowOTP = database?.allowOTP == true
|
mAllowOTP = database.allowOTP == true
|
||||||
mEntryEditViewModel.loadDatabase(database)
|
mEntryEditViewModel.loadTemplateEntry(database)
|
||||||
mTemplatesSelectorAdapter?.apply {
|
mTemplatesSelectorAdapter?.apply {
|
||||||
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
iconDrawableFactory = database.iconDrawableFactory
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,41 +432,47 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
mEntryEditViewModel.unlockAction()
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
try {
|
try {
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
var newNodes: List<Node> = ArrayList()
|
result.data?.getNewEntry(database)?.let { entry ->
|
||||||
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
|
EntrySelectionHelper.doSpecialAction(
|
||||||
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
|
intent = intent,
|
||||||
}
|
defaultAction = {
|
||||||
if (newNodes.size == 1) {
|
|
||||||
(newNodes[0] as? Entry?)?.let { entry ->
|
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
|
||||||
{
|
|
||||||
// Finish naturally
|
// Finish naturally
|
||||||
finishForEntryResult(entry)
|
finishForEntryResult(entry)
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Nothing when search retrieved
|
// Nothing when search retrieved
|
||||||
},
|
},
|
||||||
{
|
selectionAction = { intentSender, typeMode, searchInfo ->
|
||||||
entryValidatedForSave(entry)
|
when(typeMode) {
|
||||||
},
|
TypeMode.DEFAULT -> {}
|
||||||
{
|
TypeMode.MAGIKEYBOARD ->
|
||||||
entryValidatedForKeyboardSelection(database, entry)
|
entryValidatedForKeyboardSelection(database, entry)
|
||||||
|
TypeMode.PASSKEY ->
|
||||||
|
entryValidatedForPasskey(database, entry)
|
||||||
|
TypeMode.AUTOFILL ->
|
||||||
|
entryValidatedForAutofill(database, entry)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ _, _ ->
|
registrationAction = { _, typeMode, _ ->
|
||||||
entryValidatedForAutofillSelection(database, entry)
|
when(typeMode) {
|
||||||
},
|
TypeMode.DEFAULT ->
|
||||||
{
|
entryValidatedForSave(entry)
|
||||||
entryValidatedForAutofillRegistration(entry)
|
TypeMode.MAGIKEYBOARD -> {}
|
||||||
|
TypeMode.PASSKEY ->
|
||||||
|
entryValidatedForPasskey(database, entry)
|
||||||
|
TypeMode.AUTOFILL ->
|
||||||
|
entryValidatedForAutofill(database, entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to retrieve entry after database action", e)
|
Log.e(TAG, "Unable to retrieve entry after database action", e)
|
||||||
}
|
}
|
||||||
@@ -467,29 +487,33 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
|
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
|
||||||
// Populate Magikeyboard with entry
|
// Build Magikeyboard response with the entry selected
|
||||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
this.buildSpecialModeResponseAndSetResult(
|
||||||
this,
|
entryInfo = entry.getEntryInfo(database),
|
||||||
entry.getEntryInfo(database)
|
extras = buildEntryResult(entry)
|
||||||
)
|
)
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
// Don't keep activity history for entry edition
|
|
||||||
finishForEntryResult(entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
|
private fun entryValidatedForAutofill(database: ContextualDatabase, entry: Entry) {
|
||||||
// Build Autofill response with the entry selected
|
// Build Autofill response with the entry selected
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
|
this.buildSpecialModeResponseAndSetResult(
|
||||||
database,
|
entryInfo = entry.getEntryInfo(database),
|
||||||
entry.getEntryInfo(database))
|
extras = buildEntryResult(entry)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
private fun entryValidatedForPasskey(database: ContextualDatabase, entry: Entry) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
this.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo = entry.getEntryInfo(database),
|
||||||
|
extras = buildEntryResult(entry) // To update the previous screen
|
||||||
|
)
|
||||||
|
}
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
finishForEntryResult(entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -502,7 +526,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Padding if lock button visible
|
// Padding if lock button visible
|
||||||
entryEditAddToolBar?.updateLockPaddingLeft()
|
entryEditAddToolBar?.updateLockPaddingStart()
|
||||||
|
|
||||||
mAttachmentFileBinderManager?.apply {
|
mAttachmentFileBinderManager?.apply {
|
||||||
registerProgressTask()
|
registerProgressTask()
|
||||||
@@ -603,16 +627,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
isVisible = isEnabled
|
isVisible = isEnabled
|
||||||
}
|
}
|
||||||
menu?.findItem(R.id.menu_add_attachment)?.apply {
|
menu?.findItem(R.id.menu_add_attachment)?.apply {
|
||||||
// Attachment not compatible below KitKat
|
|
||||||
isEnabled = !mIsTemplate
|
isEnabled = !mIsTemplate
|
||||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
|
||||||
isVisible = isEnabled
|
isVisible = isEnabled
|
||||||
}
|
}
|
||||||
menu?.findItem(R.id.menu_add_otp)?.apply {
|
menu?.findItem(R.id.menu_add_otp)?.apply {
|
||||||
// OTP not compatible below KitKat
|
|
||||||
isEnabled = mAllowOTP
|
isEnabled = mAllowOTP
|
||||||
&& !mIsTemplate
|
&& !mIsTemplate
|
||||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
|
||||||
isVisible = isEnabled
|
isVisible = isEnabled
|
||||||
}
|
}
|
||||||
return super.onPrepareOptionsMenu(menu)
|
return super.onPrepareOptionsMenu(menu)
|
||||||
@@ -727,13 +747,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onApprovedBackPressed(approved: () -> Unit) {
|
private fun onApprovedBackPressed(approved: () -> Unit) {
|
||||||
if (!backPressedAlreadyApproved) {
|
if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setMessage(R.string.discard_changes)
|
.setMessage(R.string.discard_changes)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.discard) { _, _ ->
|
.setPositiveButton(R.string.discard) { _, _ ->
|
||||||
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||||
backPressedAlreadyApproved = true
|
mEntryEditViewModel.backPressedAlreadyApproved = true
|
||||||
approved.invoke()
|
approved.invoke()
|
||||||
}.create().show()
|
}.create().show()
|
||||||
} else {
|
} else {
|
||||||
@@ -741,14 +761,19 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildEntryResult(entry: Entry): Bundle {
|
||||||
|
return Bundle().apply {
|
||||||
|
putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun finishForEntryResult(entry: Entry) {
|
private fun finishForEntryResult(entry: Entry) {
|
||||||
// Assign entry callback as a result
|
// Assign entry callback as a result
|
||||||
try {
|
try {
|
||||||
val bundle = Bundle()
|
val bundle = buildEntryResult(entry)
|
||||||
val intentEntry = Intent()
|
val intentEntry = Intent()
|
||||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
|
||||||
intentEntry.putExtras(bundle)
|
intentEntry.putExtras(bundle)
|
||||||
setResult(Activity.RESULT_OK, intentEntry)
|
setResult(RESULT_OK, intentEntry)
|
||||||
super.finish()
|
super.finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Exception when parcelable can't be done
|
// Exception when parcelable can't be done
|
||||||
@@ -756,6 +781,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class RegistrationType {
|
||||||
|
UPDATE, CREATE
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = EntryEditActivity::class.java.name
|
private val TAG = EntryEditActivity::class.java.name
|
||||||
@@ -765,23 +794,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
const val KEY_PARENT = "parent"
|
const val KEY_PARENT = "parent"
|
||||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||||
|
|
||||||
fun registerForEntryResult(fragment: Fragment,
|
fun registerForEntryResult(
|
||||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
activity: FragmentActivity,
|
||||||
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
): ActivityResultLauncher<Intent> {
|
||||||
entryAddedOrUpdatedListener.invoke(
|
|
||||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
entryAddedOrUpdatedListener.invoke(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerForEntryResult(activity: FragmentActivity,
|
|
||||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
|
||||||
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == RESULT_OK) {
|
||||||
entryAddedOrUpdatedListener.invoke(
|
entryAddedOrUpdatedListener.invoke(
|
||||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||||
)
|
)
|
||||||
@@ -792,151 +810,78 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
* Launch EntryEditActivity to update an existing entry or to add a new entry in an existing group
|
||||||
*/
|
*/
|
||||||
fun launchToUpdate(activity: Activity,
|
fun launch(
|
||||||
|
activity: Activity,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entryId: NodeId<UUID>,
|
registrationType: RegistrationType,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
nodeId: NodeId<*>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||||
|
) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
when (registrationType) {
|
||||||
|
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||||
|
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||||
|
}
|
||||||
activityResultLauncher.launch(intent)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to add a new entry in an existent group
|
* Launch EntryEditActivity to add a new entry in special selection
|
||||||
*/
|
*/
|
||||||
fun launchToCreate(activity: Activity,
|
fun launchForSelection(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
|
typeMode: TypeMode,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
searchInfo: SearchInfo? = null,
|
||||||
if (database.loaded && !database.isReadOnly) {
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
|
||||||
activityResultLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchToUpdateForSave(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
|
||||||
context,
|
|
||||||
intent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchToCreateForSave(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||||
context,
|
context = context,
|
||||||
intent,
|
intent = intent,
|
||||||
searchInfo
|
typeMode = typeMode,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
activityResultLauncher = activityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to add a new entry in keyboard selection
|
* Launch EntryEditActivity to update an updated entry or register a new entry (from autofill)
|
||||||
*/
|
*/
|
||||||
fun launchForKeyboardSelectionResult(context: Context,
|
fun launchForRegistration(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
groupId: NodeId<*>,
|
nodeId: NodeId<*>,
|
||||||
searchInfo: SearchInfo? = null) {
|
registerInfo: RegisterInfo? = null,
|
||||||
|
typeMode: TypeMode,
|
||||||
|
registrationType: RegistrationType,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
|
) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
when (registrationType) {
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
RegistrationType.UPDATE -> intent.putExtra(KEY_ENTRY, nodeId)
|
||||||
|
RegistrationType.CREATE -> intent.putExtra(KEY_PARENT, nodeId)
|
||||||
|
}
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
intent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to add a new entry in autofill selection
|
|
||||||
*/
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
autofillComponent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to register an updated entry (from autofill)
|
|
||||||
*/
|
|
||||||
fun launchToUpdateForRegistration(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryId: NodeId<UUID>,
|
|
||||||
registerInfo: RegisterInfo? = null) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
|
||||||
context,
|
|
||||||
intent,
|
intent,
|
||||||
registerInfo
|
registerInfo,
|
||||||
)
|
typeMode
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch EntryEditActivity to register a new entry (from autofill)
|
|
||||||
*/
|
|
||||||
fun launchToCreateForRegistration(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
groupId: NodeId<*>,
|
|
||||||
registerInfo: RegisterInfo? = null) {
|
|
||||||
if (database.loaded && !database.isReadOnly) {
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
|
||||||
context,
|
|
||||||
intent,
|
|
||||||
registerInfo
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
|
||||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.utils.WebDomain
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity to search or select entry in database,
|
|
||||||
* Commonly used with Magikeyboard
|
|
||||||
*/
|
|
||||||
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
|
||||||
if (keySelectionBundle != null) {
|
|
||||||
// To manage package name
|
|
||||||
var searchInfo = SearchInfo()
|
|
||||||
keySelectionBundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
|
|
||||||
searchInfo = mSearchInfo
|
|
||||||
}
|
|
||||||
launch(database, searchInfo)
|
|
||||||
} else {
|
|
||||||
// To manage share
|
|
||||||
var sharedWebDomain: String? = null
|
|
||||||
var otpString: String? = null
|
|
||||||
|
|
||||||
when (intent?.action) {
|
|
||||||
Intent.ACTION_SEND -> {
|
|
||||||
if ("text/plain" == intent.type) {
|
|
||||||
// Retrieve web domain or OTP
|
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
|
||||||
if (OtpEntryFields.isOTPUri(extra))
|
|
||||||
otpString = extra
|
|
||||||
else
|
|
||||||
sharedWebDomain = Uri.parse(extra).host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launchSelection(database, sharedWebDomain, otpString)
|
|
||||||
}
|
|
||||||
Intent.ACTION_VIEW -> {
|
|
||||||
// Retrieve OTP
|
|
||||||
intent.dataString?.let { extra ->
|
|
||||||
if (OtpEntryFields.isOTPUri(extra))
|
|
||||||
otpString = extra
|
|
||||||
}
|
|
||||||
launchSelection(database, sharedWebDomain, otpString)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
if (database != null) {
|
|
||||||
GroupActivity.launch(this, database)
|
|
||||||
} else {
|
|
||||||
FileDatabaseSelectActivity.launch(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchSelection(database: ContextualDatabase?,
|
|
||||||
sharedWebDomain: String?,
|
|
||||||
otpString: String?) {
|
|
||||||
// Build domain search param
|
|
||||||
val searchInfo = SearchInfo().apply {
|
|
||||||
this.webDomain = sharedWebDomain
|
|
||||||
this.otpString = otpString
|
|
||||||
}
|
|
||||||
|
|
||||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
|
||||||
searchInfo.webDomain = concreteWebDomain
|
|
||||||
launch(database, searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launch(database: ContextualDatabase?,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
|
|
||||||
// Setting to integrate Magikeyboard
|
|
||||||
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
|
|
||||||
|
|
||||||
// If database is open
|
|
||||||
val readOnly = database?.isReadOnly != false
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
database,
|
|
||||||
searchInfo,
|
|
||||||
{ openedDatabase, items ->
|
|
||||||
// Items found
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
if (!readOnly) {
|
|
||||||
GroupActivity.launchForSaveResult(
|
|
||||||
this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
MagikeyboardService.performSelection(
|
|
||||||
items,
|
|
||||||
{ entryInfo ->
|
|
||||||
// Automatically populate keyboard
|
|
||||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
|
||||||
this,
|
|
||||||
entryInfo
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{ autoSearch ->
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
autoSearch)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
GroupActivity.launchForSearchResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ openedDatabase ->
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
if (!readOnly) {
|
|
||||||
GroupActivity.launchForSaveResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
GroupActivity.launchForSearchResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// If database not open
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
FileDatabaseSelectActivity.launchForSaveResult(this,
|
|
||||||
searchInfo)
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
searchInfo)
|
|
||||||
} else {
|
|
||||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
|
||||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
|
||||||
|
|
||||||
fun launch(context: Context,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
|
|
||||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
|
||||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// New task needed because don't launch from an Activity context
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
||||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -33,8 +32,6 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@@ -44,35 +41,30 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||||
import com.kunzisoft.keepass.utils.DexUtil
|
import com.kunzisoft.keepass.utils.DexUtil
|
||||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.utils.parseUri
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
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.asError
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
@@ -100,10 +92,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -134,7 +123,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
uri?.let {
|
uri?.let {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchMainCredentialActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||||
@@ -163,7 +152,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||||
launchPasswordActivity(
|
launchMainCredentialActivity(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
||||||
fileDatabaseHistoryEntityToOpen.hardwareKey
|
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||||
@@ -180,17 +169,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||||
|
|
||||||
// Load default database if not an orientation change
|
// Load default database the first time
|
||||||
if (!(savedInstanceState != null
|
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
|
||||||
&& savedInstanceState.containsKey(EXTRA_STAY)
|
launchMainCredentialActivityWithPath(databaseFileUri)
|
||||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
|
||||||
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
|
||||||
|
|
||||||
databasePath?.parseUri()?.let { databaseFileUri ->
|
|
||||||
launchPasswordActivityWithPath(databaseFileUri)
|
|
||||||
} ?: run {
|
|
||||||
Log.i(TAG, "No default database to prepare")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the database URI provided by file manager after an orientation change
|
// Retrieve the database URI provided by file manager after an orientation change
|
||||||
@@ -233,54 +214,39 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
// Retrieve settings for default database
|
// Retrieve settings for default database
|
||||||
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove all the remember locations if needed
|
||||||
|
if (PreferencesUtil.rememberDatabaseLocations(applicationContext).not()) {
|
||||||
|
FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||||
|
.deleteAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
if (database != null) {
|
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
// Update list
|
|
||||||
when (actionTask) {
|
|
||||||
ACTION_DATABASE_CREATE_TASK,
|
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
|
||||||
result.data?.getParcelableCompat<Uri>(DATABASE_URI_KEY)?.let { databaseUri ->
|
|
||||||
val mainCredential =
|
|
||||||
result.data?.getParcelableCompat(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
|
||||||
?: MainCredential()
|
|
||||||
databaseFilesViewModel.addDatabaseFile(
|
|
||||||
databaseUri,
|
|
||||||
mainCredential.keyFileUri,
|
|
||||||
mainCredential.hardwareKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Launch activity
|
// Launch activity
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
GroupActivity.launch(
|
GroupActivity.launch(
|
||||||
this@FileDatabaseSelectActivity,
|
this@FileDatabaseSelectActivity,
|
||||||
database,
|
database,
|
||||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
|
false
|
||||||
)
|
)
|
||||||
|
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -298,17 +264,58 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||||
MainCredentialActivity.launch(this,
|
try {
|
||||||
databaseUri,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
keyFile,
|
intent = this.intent,
|
||||||
hardwareKey,
|
defaultAction = {
|
||||||
{ exception ->
|
MainCredentialActivity.launch(
|
||||||
fileNoFoundAction(exception)
|
activity = this,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{ onCancelSpecialMode() },
|
searchAction = { searchInfo ->
|
||||||
{ onLaunchActivitySpecialMode() },
|
MainCredentialActivity.launchForSearchResult(
|
||||||
mAutofillActivityResultLauncher)
|
activity = this,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
selectionAction = { intentSenderMode, typeMode, searchInfo ->
|
||||||
|
MainCredentialActivity.launchForSelection(
|
||||||
|
activity = this,
|
||||||
|
activityResultLauncher = if (intentSenderMode)
|
||||||
|
mCredentialActivityResultLauncher else null,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = typeMode,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
registrationAction = { intentSenderMode, typeMode, registerInfo ->
|
||||||
|
MainCredentialActivity.launchForRegistration(
|
||||||
|
activity = this,
|
||||||
|
activityResultLauncher = if (intentSenderMode)
|
||||||
|
mCredentialActivityResultLauncher else null,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = typeMode,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
@@ -318,13 +325,15 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher)
|
mCredentialActivityResultLauncher
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
|
||||||
launchPasswordActivity(databaseUri, null, null)
|
launchMainCredentialActivity(databaseUri, null, null)
|
||||||
// Delete flickering for kitkat <=
|
// Delete flickering for kitkat <=
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
overridePendingTransition(0, 0)
|
overridePendingTransition(0, 0)
|
||||||
}
|
}
|
||||||
@@ -338,13 +347,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
// Show open and create button or special mode
|
// Show open and create button or special mode
|
||||||
when (mSpecialMode) {
|
when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
if (packageManager.allowCreateDocumentByStorageAccessFramework()) {
|
|
||||||
// There is an activity which can handle this intent.
|
|
||||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
} else{
|
|
||||||
// No Activity found that can handle this intent.
|
|
||||||
createDatabaseButtonView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Disable create button if in selection mode or request for autofill
|
// Disable create button if in selection mode or request for autofill
|
||||||
@@ -352,10 +355,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
launchGroupActivityIfLoaded(database)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show recent files if allowed
|
// Show recent files if allowed
|
||||||
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||||
databaseFilesViewModel.loadListOfDatabases()
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
@@ -366,8 +365,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
// only to keep the current activity
|
|
||||||
outState.putBoolean(EXTRA_STAY, true)
|
|
||||||
// to retrieve the URI of a created database after an orientation change
|
// to retrieve the URI of a created database after an orientation change
|
||||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||||
}
|
}
|
||||||
@@ -376,7 +373,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
try {
|
try {
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
// Create the new database
|
// Create the new database
|
||||||
createDatabase(databaseUri, mainCredential)
|
mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val error = getString(R.string.error_create_database_file)
|
val error = getString(R.string.error_create_database_file)
|
||||||
@@ -442,7 +439,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "FileDbSelectActivity"
|
private const val TAG = "FileDbSelectActivity"
|
||||||
private const val EXTRA_STAY = "EXTRA_STAY"
|
|
||||||
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -461,55 +457,36 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForSearchResult(context: Context,
|
fun launchForSearch(
|
||||||
searchInfo: SearchInfo) {
|
context: Context,
|
||||||
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
searchInfo: SearchInfo
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
) {
|
||||||
searchInfo)
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
|
context = context,
|
||||||
|
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Save Launch
|
* Selection Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForSaveResult(context: Context,
|
fun launchForSelection(
|
||||||
searchInfo: SearchInfo) {
|
context: Context,
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
typeMode: TypeMode,
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
searchInfo: SearchInfo? = null,
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Keyboard Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
fun launchForKeyboardSelectionResult(activity: Activity,
|
|
||||||
searchInfo: SearchInfo? = null) {
|
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Autofill Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
) {
|
||||||
searchInfo: SearchInfo? = null) {
|
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
context = context,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
activityResultLauncher,
|
searchInfo = searchInfo,
|
||||||
autofillComponent,
|
typeMode = typeMode,
|
||||||
searchInfo)
|
activityResultLauncher = activityResultLauncher
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -517,11 +494,19 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(
|
||||||
registerInfo: RegisterInfo? = null) {
|
context: Context,
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
typeMode: TypeMode,
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
registerInfo: RegisterInfo? = null,
|
||||||
registerInfo)
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
) {
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
|
context = context,
|
||||||
|
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = typeMode,
|
||||||
|
activityResultLauncher = activityResultLauncher
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,7 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
import com.kunzisoft.keepass.view.asError
|
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 com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
@@ -78,6 +78,8 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -174,10 +176,10 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
if (database?.allowCustomIcons == true) {
|
if (database.allowCustomIcons) {
|
||||||
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
} else {
|
} else {
|
||||||
uploadButton.visibility = View.GONE
|
uploadButton.visibility = View.GONE
|
||||||
@@ -212,7 +214,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Padding if lock button visible
|
// Padding if lock button visible
|
||||||
toolbar.updateLockPaddingLeft()
|
toolbar.updateLockPaddingStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
private lateinit var imageView: ImageView
|
private lateinit var imageView: ImageView
|
||||||
private lateinit var progressView: View
|
private lateinit var progressView: View
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -101,7 +103,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -119,7 +121,6 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
resources.displayMetrics.heightPixels * 2
|
resources.displayMetrics.heightPixels * 2
|
||||||
)
|
)
|
||||||
|
|
||||||
database?.let { database ->
|
|
||||||
BinaryDatabaseManager.loadBitmap(
|
BinaryDatabaseManager.loadBitmap(
|
||||||
database,
|
database,
|
||||||
attachment.binaryData,
|
attachment.binaryData,
|
||||||
@@ -132,7 +133,6 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
imageView.setImageBitmap(bitmapLoaded)
|
imageView.setImageBitmap(bitmapLoaded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} ?: finish()
|
} ?: finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to view the binary", e)
|
Log.e(TAG, "Unable to view the binary", e)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
class KeyGeneratorActivity : DatabaseLockActivity() {
|
class KeyGeneratorActivity : DatabaseLockActivity() {
|
||||||
@@ -28,6 +28,8 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
|
|||||||
private lateinit var validationButton: View
|
private lateinit var validationButton: View
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = true
|
||||||
|
|
||||||
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
|
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -84,7 +86,7 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Padding if lock button visible
|
// Padding if lock button visible
|
||||||
toolbar.updateLockPaddingLeft()
|
toolbar.updateLockPaddingStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
|||||||
@@ -32,43 +32,49 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.commit
|
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.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
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.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.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
import com.kunzisoft.keepass.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.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
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.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_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.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity
|
|
||||||
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
|
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
|
||||||
|
import com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
@@ -79,26 +85,31 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|||||||
import com.kunzisoft.keepass.view.MainCredentialView
|
import com.kunzisoft.keepass.view.MainCredentialView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
|
||||||
class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
class MainCredentialActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var filenameView: TextView? = null
|
private var filenameView: TextView? = null
|
||||||
private var logotypeButton: View? = null
|
private var logotypeButton: View? = null
|
||||||
private var advancedUnlockButton: View? = null
|
private var deviceUnlockButton: View? = null
|
||||||
private var mainCredentialView: MainCredentialView? = null
|
private var mainCredentialView: MainCredentialView? = null
|
||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
||||||
|
|
||||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
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)
|
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||||
|
|
||||||
@@ -113,10 +124,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
private var mReadOnly: Boolean = false
|
private var mReadOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -131,7 +139,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
|
|
||||||
filenameView = findViewById(R.id.filename)
|
filenameView = findViewById(R.id.filename)
|
||||||
logotypeButton = findViewById(R.id.activity_password_logotype)
|
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)
|
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
||||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
@@ -140,7 +148,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||||
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||||
} else {
|
} else {
|
||||||
PreferencesUtil.enableReadOnlyDatabase(this)
|
false
|
||||||
}
|
}
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
|
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
|
||||||
@@ -165,32 +173,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
startActivity(Intent(this, AppearanceSettingsActivity::class.java))
|
startActivity(Intent(this, AppearanceSettingsActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init Biometric elements
|
|
||||||
advancedUnlockFragment = supportFragmentManager
|
|
||||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
|
||||||
if (advancedUnlockFragment == null) {
|
|
||||||
advancedUnlockFragment = AdvancedUnlockFragment()
|
|
||||||
supportFragmentManager.commit {
|
|
||||||
replace(R.id.fragment_advanced_unlock_container_view,
|
|
||||||
advancedUnlockFragment!!,
|
|
||||||
UNLOCK_FRAGMENT_TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen password checkbox to init advanced unlock and confirmation button
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
mainCredentialView?.onPasswordChecked =
|
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
||||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
mDeviceUnlockViewModel?.checkConditionToStoreCredential(
|
||||||
enableConfirmationButton()
|
condition = verified
|
||||||
|
)
|
||||||
}
|
}
|
||||||
mainCredentialView?.onKeyFileChecked =
|
// TODO Async by ViewModel
|
||||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
|
||||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
|
||||||
enableConfirmationButton()
|
|
||||||
}
|
|
||||||
mainCredentialView?.onHardwareKeyChecked =
|
|
||||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
|
||||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
|
||||||
enableConfirmationButton()
|
enableConfirmationButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +204,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
mForceReadOnly = databaseFileNotExists
|
mForceReadOnly = databaseFileNotExists
|
||||||
|
|
||||||
|
// Restore read-only state from database file if not forced
|
||||||
|
if (!mForceReadOnly) {
|
||||||
|
databaseFile?.readOnly?.let { savedReadOnlyState ->
|
||||||
|
mReadOnly = savedReadOnlyState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
// Post init uri with KeyFile only if needed
|
// Post init uri with KeyFile only if needed
|
||||||
@@ -240,11 +237,63 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
|
|
||||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
// Init Biometric elements only if allowed
|
||||||
|
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_device_unlock_container_view,
|
||||||
|
it,
|
||||||
|
UNLOCK_FRAGMENT_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
||||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
|
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
|
||||||
|
|
||||||
@@ -253,23 +302,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
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 ->
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
launchGroupActivityIfLoaded(database)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
if (database != null) {
|
|
||||||
// Trying to load another database
|
// Trying to load another database
|
||||||
if (mDatabaseFileUri != null
|
if (mDatabaseFileUri != null
|
||||||
&& database.fileUri != null
|
&& database.fileUri != null
|
||||||
@@ -281,7 +320,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
@@ -291,9 +329,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
// Recheck advanced unlock if error
|
|
||||||
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
} else {
|
} else {
|
||||||
@@ -390,28 +425,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher
|
mCredentialActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
|
||||||
override fun passwordToStore(password: String?): ByteArray? {
|
override fun passwordToStore(password: String?): ByteArray? {
|
||||||
return password?.toByteArray()
|
return password?.toByteArray()
|
||||||
@@ -428,7 +446,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
|
// Load the database if password is retrieve from biometric
|
||||||
// Retrieve from biometric
|
// Retrieve from biometric
|
||||||
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||||
@@ -472,15 +503,18 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||||
// Consume the intent extra password
|
// Consume the intent extra password
|
||||||
intent.removeExtra(KEY_PASSWORD)
|
intent.removeExtra(KEY_PASSWORD)
|
||||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
mainCredentialView?.populatePasswordTextView(password)
|
mainCredentialView?.populatePasswordTextView(password)
|
||||||
}
|
}
|
||||||
|
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||||
|
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
|
||||||
if (launchImmediately) {
|
if (launchImmediately) {
|
||||||
loadDatabase()
|
loadDatabase()
|
||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
mDeviceUnlockViewModel?.connect(databaseFileUri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enableConfirmationButton()
|
enableConfirmationButton()
|
||||||
@@ -508,13 +542,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) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
@@ -535,13 +562,10 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mReadOnly && (
|
if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
|
||||||
mSpecialMode == SpecialMode.SAVE
|
Log.e(TAG, getString(R.string.error_save_read_only))
|
||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|
||||||
) {
|
|
||||||
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
|
||||||
Snackbar.make(coordinatorLayout,
|
Snackbar.make(coordinatorLayout,
|
||||||
R.string.autofill_read_only_save,
|
R.string.error_save_read_only,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
} else {
|
} else {
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
@@ -562,7 +586,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
loadDatabase(
|
mDatabaseViewModel.loadDatabase(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
mainCredential,
|
mainCredential,
|
||||||
readOnly,
|
readOnly,
|
||||||
@@ -630,7 +654,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
try {
|
try {
|
||||||
menu.findItem(R.id.menu_open_file_read_mode_key)
|
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to find read mode menu")
|
Log.e(TAG, "Unable to find read mode menu", e)
|
||||||
}
|
}
|
||||||
performedNextEducation(menu)
|
performedNextEducation(menu)
|
||||||
},
|
},
|
||||||
@@ -640,17 +664,17 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
&& !readOnlyEducationPerformed) {
|
&& !readOnlyEducationPerformed) {
|
||||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
|
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this)
|
||||||
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
&& advancedUnlockButton != null) {
|
&& deviceUnlockButton != null) {
|
||||||
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
|
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
|
||||||
advancedUnlockButton!!,
|
deviceUnlockButton!!,
|
||||||
{
|
{
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(
|
Intent(
|
||||||
this,
|
this,
|
||||||
AdvancedUnlockSettingsActivity::class.java
|
DeviceUnlockSettingsActivity::class.java
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -659,7 +683,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ignored: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,6 +704,12 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
R.id.menu_open_file_read_mode_key -> {
|
R.id.menu_open_file_read_mode_key -> {
|
||||||
mReadOnly = !mReadOnly
|
mReadOnly = !mReadOnly
|
||||||
changeOpenFileReadIcon(item)
|
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)
|
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
}
|
}
|
||||||
@@ -687,6 +717,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
mDeviceUnlockViewModel?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = MainCredentialActivity::class.java.name
|
private val TAG = MainCredentialActivity::class.java.name
|
||||||
@@ -702,11 +739,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
private const val KEY_PASSWORD = "password"
|
private const val KEY_PASSWORD = "password"
|
||||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
|
|
||||||
private fun buildAndLaunchIntent(activity: Activity,
|
private fun buildAndLaunchIntent(
|
||||||
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit
|
||||||
|
) {
|
||||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||||
if (keyFile != null)
|
if (keyFile != null)
|
||||||
@@ -723,10 +762,12 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launch(activity: Activity,
|
fun launch(
|
||||||
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?) {
|
hardwareKey: HardwareKey?
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
@@ -739,81 +780,46 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForSearchResult(activity: Activity,
|
fun launchForSearchResult(
|
||||||
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
activity,
|
context = activity,
|
||||||
intent,
|
intent = intent,
|
||||||
searchInfo)
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Save Launch
|
* Selection Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForSaveResult(activity: Activity,
|
fun launchForSelection(
|
||||||
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
searchInfo: SearchInfo) {
|
typeMode: TypeMode,
|
||||||
|
searchInfo: SearchInfo?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||||
activity,
|
context = activity,
|
||||||
intent,
|
intent = intent,
|
||||||
searchInfo)
|
typeMode = typeMode,
|
||||||
}
|
searchInfo = searchInfo,
|
||||||
}
|
activityResultLauncher = activityResultLauncher
|
||||||
|
)
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Keyboard Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForKeyboardResult(activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Autofill Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
activityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,102 +828,25 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(activity: Activity,
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForRegistration(
|
||||||
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
registerInfo: RegisterInfo?) {
|
typeMode: TypeMode,
|
||||||
|
registerInfo: RegisterInfo?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
activity,
|
context = activity,
|
||||||
intent,
|
intent = intent,
|
||||||
registerInfo)
|
typeMode = typeMode,
|
||||||
}
|
registerInfo = registerInfo,
|
||||||
}
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Global Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
fun launch(activity: AppCompatActivity,
|
|
||||||
databaseUri: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
hardwareKey: HardwareKey?,
|
|
||||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
|
||||||
onCancelSpecialMode: () -> Unit,
|
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
|
||||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
|
||||||
{
|
|
||||||
launch(
|
|
||||||
activity,
|
|
||||||
databaseUri,
|
|
||||||
keyFile,
|
|
||||||
hardwareKey
|
|
||||||
)
|
)
|
||||||
},
|
|
||||||
{ searchInfo -> // Search Action
|
|
||||||
launchForSearchResult(
|
|
||||||
activity,
|
|
||||||
databaseUri,
|
|
||||||
keyFile,
|
|
||||||
hardwareKey,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
{ searchInfo -> // Save Action
|
|
||||||
launchForSaveResult(
|
|
||||||
activity,
|
|
||||||
databaseUri,
|
|
||||||
keyFile,
|
|
||||||
hardwareKey,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
{ searchInfo -> // Keyboard Selection Action
|
|
||||||
launchForKeyboardResult(
|
|
||||||
activity,
|
|
||||||
databaseUri,
|
|
||||||
keyFile,
|
|
||||||
hardwareKey,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
launchForAutofillResult(
|
|
||||||
activity,
|
|
||||||
databaseUri,
|
|
||||||
keyFile,
|
|
||||||
hardwareKey,
|
|
||||||
autofillActivityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
} else {
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ registerInfo -> // Registration Action
|
|
||||||
launchForRegistration(
|
|
||||||
activity,
|
|
||||||
databaseUri,
|
|
||||||
keyFile,
|
|
||||||
hardwareKey,
|
|
||||||
registerInfo
|
|
||||||
)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(OLD_FILE_DATABASE_INFO)
|
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(OLD_FILE_DATABASE_INFO)
|
||||||
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(NEW_FILE_DATABASE_INFO)
|
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(NEW_FILE_DATABASE_INFO)
|
||||||
|
val readOnlyDatabase: Boolean = arguments?.getBoolean(READ_ONLY_DATABASE) ?: true
|
||||||
|
|
||||||
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
|
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
@@ -54,13 +55,19 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
|
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
|
||||||
+ "\n→\n" +
|
+ "\n→\n" +
|
||||||
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
|
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
|
||||||
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
|
stringBuilder.append(getString(
|
||||||
|
if (readOnlyDatabase) {
|
||||||
|
R.string.warning_database_info_changed_options_read_only
|
||||||
|
} else {
|
||||||
|
R.string.warning_database_info_changed_options
|
||||||
|
}
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(getString(R.string.warning_database_revoked))
|
stringBuilder.append(getString(R.string.warning_database_revoked))
|
||||||
}
|
}
|
||||||
builder.setMessage(stringBuilder)
|
builder.setMessage(stringBuilder)
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
actionDatabaseListener?.validateDatabaseChanged()
|
actionDatabaseListener?.onDatabaseChangeValidated()
|
||||||
}
|
}
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
@@ -69,7 +76,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ActionDatabaseChangedListener {
|
interface ActionDatabaseChangedListener {
|
||||||
fun validateDatabaseChanged()
|
fun onDatabaseChangeValidated()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -77,15 +84,19 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
|
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
|
||||||
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
|
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
|
||||||
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||||
|
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
|
||||||
|
|
||||||
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
fun getInstance(
|
||||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo
|
oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
readOnly: Boolean
|
||||||
)
|
)
|
||||||
: DatabaseChangedDialogFragment {
|
: DatabaseChangedDialogFragment {
|
||||||
val fragment = DatabaseChangedDialogFragment()
|
val fragment = DatabaseChangedDialogFragment()
|
||||||
fragment.arguments = Bundle().apply {
|
fragment.arguments = Bundle().apply {
|
||||||
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
|
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
|
||||||
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
|
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
|
||||||
|
putBoolean(READ_ONLY_DATABASE, readOnly)
|
||||||
}
|
}
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,78 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
private var mDatabase: ContextualDatabase? = null
|
private val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch {
|
||||||
mDatabaseViewModel.database.observe(this) { database ->
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
this.mDatabase = database
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
resetAppTimeoutOnTouchOrFocus()
|
when (uiState) {
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
database?.let {
|
||||||
onDatabaseRetrieved(database)
|
onDatabaseRetrieved(database)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
// Screenshot mode or hide views
|
||||||
|
context?.let {
|
||||||
|
if (PreferencesUtil.isScreenshotModeEnabled(it)) {
|
||||||
|
dialog?.window?.clearFlags(FLAG_SECURE)
|
||||||
|
} else {
|
||||||
|
dialog?.window?.setFlags(FLAG_SECURE, FLAG_SECURE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated(message = "")
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
resetAppTimeoutOnTouchOrFocus()
|
resetAppTimeoutOnTouchOrFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Can be overridden by a subclass
|
// Can be overridden by a subclass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
|
|||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.view.DateTimeFieldView
|
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||||
|
|
||||||
@@ -62,14 +62,14 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
|||||||
private lateinit var uuidContainerView: ViewGroup
|
private lateinit var uuidContainerView: ViewGroup
|
||||||
private lateinit var uuidReferenceView: TextView
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
}
|
}
|
||||||
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||||
|
|
||||||
if (database?.allowCustomSearchableGroup() == true) {
|
if (database.allowCustomSearchableGroup()) {
|
||||||
searchableLabelView.visibility = View.VISIBLE
|
searchableLabelView.visibility = View.VISIBLE
|
||||||
searchableView.visibility = View.VISIBLE
|
searchableView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
@@ -155,7 +155,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
|||||||
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
||||||
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
||||||
mGroupInfo.defaultAutoTypeSequence)
|
mGroupInfo.defaultAutoTypeSequence)
|
||||||
val uuid = UuidUtil.toHexString(mGroupInfo.id)
|
val uuid = mGroupInfo.id?.asHexString()
|
||||||
if (uuid == null || uuid.isEmpty()) {
|
if (uuid == null || uuid.isEmpty()) {
|
||||||
uuidContainerView.visibility = View.GONE
|
uuidContainerView.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import com.kunzisoft.keepass.view.InheritedCompletionView
|
|||||||
import com.kunzisoft.keepass.view.TagsCompletionView
|
import com.kunzisoft.keepass.view.TagsCompletionView
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||||
import com.tokenautocomplete.FilteredArrayAdapter
|
import com.tokenautocomplete.FilteredArrayAdapter
|
||||||
import org.joda.time.DateTime
|
|
||||||
|
|
||||||
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
@@ -90,27 +89,21 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
mGroupEditViewModel.onDateSelected.observe(this) { dateMilliseconds ->
|
mGroupEditViewModel.onDateSelected.observe(this) { date ->
|
||||||
// Save the date
|
// Save the date
|
||||||
mGroupInfo.expiryTime = DateInstant(
|
mGroupInfo.expiryTime.setDate(date.year, date.month, date.day)
|
||||||
DateTime(mGroupInfo.expiryTime.date)
|
|
||||||
.withMillis(dateMilliseconds)
|
|
||||||
.toDate())
|
|
||||||
expirationView.dateTime = mGroupInfo.expiryTime
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
||||||
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
|
|
||||||
// Trick to recall selection with time
|
// Trick to recall selection with time
|
||||||
mGroupEditViewModel.requestDateTimeSelection(instantTime)
|
mGroupEditViewModel.requestDateTimeSelection(
|
||||||
|
DateInstant(mGroupInfo.expiryTime.instant, DateInstant.Type.TIME)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
|
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
|
||||||
// Save the time
|
// Save the time
|
||||||
mGroupInfo.expiryTime = DateInstant(
|
mGroupInfo.expiryTime.setTime(viewModelTime.hour, viewModelTime.minute)
|
||||||
DateTime(mGroupInfo.expiryTime.date)
|
|
||||||
.withHourOfDay(viewModelTime.hours)
|
|
||||||
.withMinuteOfHour(viewModelTime.minutes)
|
|
||||||
.toDate(), mGroupInfo.expiryTime.type)
|
|
||||||
expirationView.dateTime = mGroupInfo.expiryTime
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,32 +112,32 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
}
|
}
|
||||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
|
|
||||||
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
|
searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (database?.allowAutoType() == true) {
|
if (database.allowAutoType()) {
|
||||||
autoTypeContainerView.visibility = View.VISIBLE
|
autoTypeContainerView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
autoTypeContainerView.visibility = View.GONE
|
autoTypeContainerView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||||
tagsCompletionView.apply {
|
tagsCompletionView.apply {
|
||||||
threshold = 1
|
threshold = 1
|
||||||
setAdapter(tagsAdapter)
|
setAdapter(tagsAdapter)
|
||||||
}
|
}
|
||||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
@@ -255,11 +248,7 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
private fun retrieveGroupInfoFromViews() {
|
private fun retrieveGroupInfoFromViews() {
|
||||||
mGroupInfo.title = nameTextView.text.toString()
|
mGroupInfo.title = nameTextView.text.toString()
|
||||||
// Only if there
|
mGroupInfo.notes = notesTextView.text?.toString()
|
||||||
val newNotes = notesTextView.text.toString()
|
|
||||||
if (newNotes.isNotEmpty()) {
|
|
||||||
mGroupInfo.notes = newNotes
|
|
||||||
}
|
|
||||||
mGroupInfo.expires = expirationView.activation
|
mGroupInfo.expires = expirationView.activation
|
||||||
mGroupInfo.expiryTime = expirationView.dateTime
|
mGroupInfo.expiryTime = expirationView.dateTime
|
||||||
mGroupInfo.searchable = searchableView.getValue()
|
mGroupInfo.searchable = searchableView.getValue()
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
private var mCustomIcon: IconImageCustom? = null
|
private var mCustomIcon: IconImageCustom? = null
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
|
||||||
}
|
}
|
||||||
mCustomIcon?.let { customIcon ->
|
mCustomIcon?.let { customIcon ->
|
||||||
populateViewsWithCustomIcon(customIcon)
|
populateViewsWithCustomIcon(customIcon)
|
||||||
|
|||||||
@@ -35,16 +35,21 @@ import com.google.android.material.textfield.TextInputLayout
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
|
||||||
import com.kunzisoft.keepass.password.PasswordEntropy
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
import com.kunzisoft.keepass.view.HardwareKeySelectionView
|
import com.kunzisoft.keepass.view.HardwareKeySelectionView
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.PassKeyView
|
import com.kunzisoft.keepass.view.PasswordEditView
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
|
||||||
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
@@ -55,11 +60,12 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
private lateinit var rootView: View
|
private lateinit var rootView: View
|
||||||
|
|
||||||
private lateinit var passwordCheckBox: CompoundButton
|
private lateinit var passwordCheckBox: CompoundButton
|
||||||
private lateinit var passwordView: PassKeyView
|
private lateinit var passwordEditView: PasswordEditView
|
||||||
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
|
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
|
||||||
private lateinit var passwordRepeatView: TextView
|
private lateinit var passwordRepeatView: TextView
|
||||||
|
|
||||||
private lateinit var keyFileCheckBox: CompoundButton
|
private lateinit var keyFileCheckBox: CompoundButton
|
||||||
|
private lateinit var keyFileGenerateButton: View
|
||||||
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
||||||
|
|
||||||
private lateinit var hardwareKeyCheckBox: CompoundButton
|
private lateinit var hardwareKeyCheckBox: CompoundButton
|
||||||
@@ -141,29 +147,39 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
|
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
|
||||||
passwordView = rootView.findViewById(R.id.password_view)
|
passwordEditView = rootView.findViewById(R.id.password_view)
|
||||||
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
|
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
|
||||||
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
|
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
|
||||||
passwordRepeatView.applyFontVisibility()
|
passwordRepeatView.applyFontVisibility()
|
||||||
|
|
||||||
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
|
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
|
||||||
|
keyFileGenerateButton = rootView.findViewById(R.id.keyfile_generate)
|
||||||
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
|
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
|
||||||
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
|
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||||
|
createdFileUri?.let { uri ->
|
||||||
|
createKeyFile(uri)
|
||||||
|
keyFileSelectionView.error = null
|
||||||
|
keyFileCheckBox.isChecked = true
|
||||||
|
keyFileSelectionView.uri = uri
|
||||||
|
}
|
||||||
|
}
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
pathUri.getDocumentFile(requireContext())?.length()?.let { lengthFile ->
|
pathUri.getDocumentFile(requireContext())?.length()?.let { lengthFile ->
|
||||||
keyFileSelectionView.error = null
|
keyFileSelectionView.error = null
|
||||||
keyFileCheckBox.isChecked = true
|
keyFileCheckBox.isChecked = true
|
||||||
keyFileSelectionView.uri = pathUri
|
keyFileSelectionView.uri = pathUri
|
||||||
if (lengthFile <= 0L) {
|
showLengthKeyFileConfirmationDialog(lengthFile)
|
||||||
showEmptyKeyFileConfirmationDialog()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
keyFileGenerateButton.setOnClickListener {
|
||||||
|
mExternalFileHelper?.createDocument(DEFAULT_KEYFILE_NAME)
|
||||||
}
|
}
|
||||||
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
|
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
@@ -202,6 +218,16 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createKeyFile(uri: Uri) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
activity?.contentResolver?.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
val randomBytes = ByteArray(DEFAULT_KEYFILE_SIZE)
|
||||||
|
SecureRandom().nextBytes(randomBytes)
|
||||||
|
outputStream.write(randomBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun approveMainCredential() {
|
private fun approveMainCredential() {
|
||||||
val errorPassword = verifyPassword()
|
val errorPassword = verifyPassword()
|
||||||
val errorKeyFile = verifyKeyFile()
|
val errorKeyFile = verifyKeyFile()
|
||||||
@@ -232,8 +258,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
showEmptyPasswordConfirmationDialog()
|
showEmptyPasswordConfirmationDialog()
|
||||||
} else if (!error
|
} else if (!error
|
||||||
&& hardwareKey != null
|
&& hardwareKey != null
|
||||||
&& !HardwareKeyActivity.isHardwareKeyAvailable(
|
&& !HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)
|
||||||
requireActivity(), hardwareKey, false)
|
|
||||||
) {
|
) {
|
||||||
// show hardware driver dialog if required
|
// show hardware driver dialog if required
|
||||||
error = true
|
error = true
|
||||||
@@ -250,7 +275,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
var error = false
|
var error = false
|
||||||
passwordRepeatTextInputLayout.error = null
|
passwordRepeatTextInputLayout.error = null
|
||||||
if (passwordCheckBox.isChecked) {
|
if (passwordCheckBox.isChecked) {
|
||||||
mMasterPassword = passwordView.passwordString
|
mMasterPassword = passwordEditView.passwordString
|
||||||
val confPassword = passwordRepeatView.text.toString()
|
val confPassword = passwordRepeatView.text.toString()
|
||||||
|
|
||||||
// Verify that passwords match
|
// Verify that passwords match
|
||||||
@@ -302,13 +327,13 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// To check checkboxes if a text is present
|
// To check checkboxes if a text is present
|
||||||
passwordView.addTextChangedListener(passwordTextWatcher)
|
passwordEditView.addTextChangedListener(passwordTextWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
passwordView.removeTextChangedListener(passwordTextWatcher)
|
passwordEditView.removeTextChangedListener(passwordTextWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showEmptyPasswordConfirmationDialog() {
|
private fun showEmptyPasswordConfirmationDialog() {
|
||||||
@@ -339,15 +364,25 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showEmptyKeyFileConfirmationDialog() {
|
private fun showLengthKeyFileConfirmationDialog(length: Long) {
|
||||||
activity?.let {
|
activity?.let {
|
||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
builder.setMessage(SpannableStringBuilder().apply {
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
append(getString(R.string.warning_empty_keyfile))
|
|
||||||
append("\n\n")
|
|
||||||
append(getString(R.string.warning_empty_keyfile_explanation))
|
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||||
|
var warning = false
|
||||||
|
if (length <= 0L) {
|
||||||
|
warning = true
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_empty_keyfile))
|
||||||
|
} else if (length > 10485760L) {
|
||||||
|
warning = true
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_large_keyfile))
|
||||||
|
}
|
||||||
|
if (warning) {
|
||||||
append("\n\n")
|
append("\n\n")
|
||||||
append(getString(R.string.warning_sure_add_file))
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
@@ -362,6 +397,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||||
|
private const val DEFAULT_KEYFILE_NAME = "keyfile.bin"
|
||||||
|
private const val DEFAULT_KEYFILE_SIZE = 128
|
||||||
|
|
||||||
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
|
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
|
||||||
val fragment = SetMainCredentialDialogFragment()
|
val fragment = SetMainCredentialDialogFragment()
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.*
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -40,14 +44,15 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS
|
|||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
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_HOTP_COUNTER
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
||||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
@@ -224,6 +229,9 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
|
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
|
// Set the default value of OTP element
|
||||||
upgradeType()
|
upgradeType()
|
||||||
upgradeTokenType()
|
upgradeTokenType()
|
||||||
@@ -310,12 +318,17 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
|
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
s?.toString()?.let { userString ->
|
s?.toString()?.let { userString ->
|
||||||
|
if (userString.length >= MIN_OTP_SECRET) {
|
||||||
try {
|
try {
|
||||||
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
||||||
otpSecretContainer?.error = null
|
otpSecretContainer?.error = null
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
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
|
mSecretWellFormed = otpSecretContainer?.error == null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,21 +176,14 @@ class SortDialogFragment : DatabaseDialogFragment() {
|
|||||||
return bundle
|
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,
|
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||||
ascending: Boolean,
|
ascending: Boolean,
|
||||||
groupsBefore: Boolean,
|
groupsBefore: Boolean,
|
||||||
recycleBinBottom: Boolean): SortDialogFragment {
|
recycleBinBottom: Boolean?): SortDialogFragment {
|
||||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||||
|
recycleBinBottom?.let {
|
||||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||||
|
}
|
||||||
val fragment = SortDialogFragment()
|
val fragment = SortDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
|
|||||||
@@ -4,36 +4,59 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
protected var mDatabase: ContextualDatabase? = null
|
protected val mDatabase: ContextualDatabase?
|
||||||
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
if (mDatabase == null || mDatabase != database) {
|
super.onCreate(savedInstanceState)
|
||||||
this.mDatabase = database
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
database?.let {
|
||||||
onDatabaseRetrieved(database)
|
onDatabaseRetrieved(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
|
|
||||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
||||||
context?.let {
|
context?.let {
|
||||||
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
|
view?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||||
|
context = it,
|
||||||
|
databaseLoaded = mDatabase?.loaded
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +67,4 @@ abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
|||||||
) {
|
) {
|
||||||
// Can be overridden by a subclass
|
// Can be overridden by a subclass
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun buildNewBinaryAttachment(): BinaryData? {
|
|
||||||
return mDatabase?.buildNewBinaryAttachment()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
val attachmentToUploadUri = it.attachmentToUploadUri
|
val attachmentToUploadUri = it.attachmentToUploadUri
|
||||||
val fileName = it.fileName
|
val fileName = it.fileName
|
||||||
|
|
||||||
buildNewBinaryAttachment()?.let { binaryAttachment ->
|
mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
|
||||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
// Ask to replace the current attachment
|
// Ask to replace the current attachment
|
||||||
if ((!mAllowMultipleAttachments
|
if ((!mAllowMultipleAttachments
|
||||||
@@ -273,13 +273,13 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
|
|
||||||
templateView.populateIconMethod = { imageView, icon ->
|
templateView.populateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
|
mAllowMultipleAttachments = database.allowMultipleAttachments == true
|
||||||
|
|
||||||
attachmentsAdapter?.database = database
|
attachmentsAdapter?.database = database
|
||||||
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
@@ -290,12 +290,12 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||||
tagsCompletionView.apply {
|
tagsCompletionView.apply {
|
||||||
threshold = 1
|
threshold = 1
|
||||||
setAdapter(tagsAdapter)
|
setAdapter(tagsAdapter)
|
||||||
}
|
}
|
||||||
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.model.StreamDirection
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.view.TemplateView
|
import com.kunzisoft.keepass.view.TemplateView
|
||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.showByFading
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||||
attachmentsAdapter?.database = database
|
attachmentsAdapter?.database = database
|
||||||
@@ -184,7 +184,7 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
// customDataView.text = entryInfo?.customData?.toString()
|
// customDataView.text = entryInfo?.customData?.toString()
|
||||||
|
|
||||||
// Assign special data
|
// Assign special data
|
||||||
uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
|
uuidReferenceView.text = entryInfo?.id?.asHexString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showClipboardDialog() {
|
private fun showClipboardDialog() {
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
@@ -76,9 +76,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
|
|
||||||
private var mRecycleBinEnable: Boolean = false
|
|
||||||
private var mRecycleBin: Group? = null
|
|
||||||
|
|
||||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
@@ -102,21 +99,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
R.id.menu_sort -> {
|
R.id.menu_sort -> {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
val sortDialogFragment: SortDialogFragment =
|
val sortDialogFragment: SortDialogFragment =
|
||||||
if (mRecycleBinEnable) {
|
|
||||||
SortDialogFragment.getInstance(
|
SortDialogFragment.getInstance(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
PreferencesUtil.getAscendingSort(context),
|
PreferencesUtil.getAscendingSort(context),
|
||||||
PreferencesUtil.getGroupsBeforeSort(context),
|
PreferencesUtil.getGroupsBeforeSort(context),
|
||||||
|
if (mDatabase?.isRecycleBinEnabled == true) {
|
||||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||||
|
} else null
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
SortDialogFragment.getInstance(
|
|
||||||
PreferencesUtil.getListSort(context),
|
|
||||||
PreferencesUtil.getAscendingSort(context),
|
|
||||||
PreferencesUtil.getGroupsBeforeSort(context)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -164,12 +154,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
|
||||||
mRecycleBin = database?.recycleBin
|
|
||||||
|
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
database?.let { database ->
|
|
||||||
mAdapter = NodesAdapter(context, database).apply {
|
mAdapter = NodesAdapter(context, database).apply {
|
||||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||||
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
||||||
@@ -208,7 +194,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
mNodesRecyclerView?.adapter = mAdapter
|
mNodesRecyclerView?.adapter = mAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
@@ -261,7 +246,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
||||||
activity?.intent?.let {
|
activity?.intent?.let {
|
||||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
specialMode = it.retrieveSpecialMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +297,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun containsRecycleBin(database: ContextualDatabase?, nodes: List<Node>): Boolean {
|
||||||
|
return database?.isRecycleBinEnabled == true
|
||||||
|
&& nodes.any { it == database.recycleBin }
|
||||||
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(database: ContextualDatabase,
|
fun actionNodesCallback(database: ContextualDatabase,
|
||||||
nodes: List<Node>,
|
nodes: List<Node>,
|
||||||
menuListener: NodesActionMenuListener?,
|
menuListener: NodesActionMenuListener?,
|
||||||
@@ -336,8 +326,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
// Open and Edit for a single item
|
// Open and Edit for a single item
|
||||||
if (nodes.size == 1) {
|
if (nodes.size == 1) {
|
||||||
// Edition
|
// Edition
|
||||||
if (database.isReadOnly
|
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||||
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
|
|
||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -357,8 +346,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deletion
|
// Deletion
|
||||||
if (database.isReadOnly
|
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||||
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
|
|
||||||
menu?.removeItem(R.id.menu_delete)
|
menu?.removeItem(R.id.menu_delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
|||||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val populateList = launch {
|
val populateList = launch {
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ class IconPickerFragment : DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||||
if (database?.allowCustomIcons == true) 2 else 1)
|
if (database.allowCustomIcons) 2 else 1)
|
||||||
viewPager.adapter = iconPickerPagerAdapter
|
viewPager.adapter = iconPickerPagerAdapter
|
||||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||||
tab.text = when (position) {
|
tab.text = when (position) {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
import com.kunzisoft.keepass.password.PassphraseGenerator
|
import com.kunzisoft.keepass.password.PassphraseGenerator
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.view.PassKeyView
|
import com.kunzisoft.keepass.view.PasswordEditView
|
||||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
class PassphraseGeneratorFragment : DatabaseFragment() {
|
class PassphraseGeneratorFragment : DatabaseFragment() {
|
||||||
|
|
||||||
private lateinit var passKeyView: PassKeyView
|
private lateinit var passwordEditView: PasswordEditView
|
||||||
|
|
||||||
private lateinit var sliderWordCount: Slider
|
private lateinit var sliderWordCount: Slider
|
||||||
private lateinit var wordCountText: EditText
|
private lateinit var wordCountText: EditText
|
||||||
@@ -62,7 +62,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
passKeyView = view.findViewById(R.id.passphrase_view)
|
passwordEditView = view.findViewById(R.id.passphrase_view)
|
||||||
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
|
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
|
||||||
sliderWordCount = view.findViewById(R.id.slider_word_count)
|
sliderWordCount = view.findViewById(R.id.slider_word_count)
|
||||||
wordCountText = view.findViewById(R.id.word_count)
|
wordCountText = view.findViewById(R.id.word_count)
|
||||||
@@ -80,7 +80,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
passphraseCopyView?.setOnClickListener {
|
passphraseCopyView?.setOnClickListener {
|
||||||
clipboardHelper.timeoutCopyToClipboard(
|
clipboardHelper.timeoutCopyToClipboard(
|
||||||
getString(R.string.passphrase),
|
getString(R.string.passphrase),
|
||||||
passKeyView.passwordString,
|
passwordEditView.passwordString,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
generatePassphrase()
|
generatePassphrase()
|
||||||
|
|
||||||
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
|
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
|
||||||
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
|
mKeyGeneratorViewModel.setKeyGenerated(passwordEditView.passwordString)
|
||||||
}
|
}
|
||||||
|
|
||||||
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
|
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
|
||||||
@@ -219,7 +219,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to generate a passphrase", e)
|
Log.e(TAG, "Unable to generate a passphrase", e)
|
||||||
}
|
}
|
||||||
passKeyView.passwordString = passphrase
|
passwordEditView.passwordString = passphrase
|
||||||
charactersCountText.text = getString(R.string.character_count, passphrase.length)
|
charactersCountText.text = getString(R.string.character_count, passphrase.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.view.PassKeyView
|
import com.kunzisoft.keepass.view.PasswordEditView
|
||||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
class PasswordGeneratorFragment : DatabaseFragment() {
|
class PasswordGeneratorFragment : DatabaseFragment() {
|
||||||
|
|
||||||
private lateinit var passKeyView: PassKeyView
|
private lateinit var passwordEditView: PasswordEditView
|
||||||
|
|
||||||
private lateinit var sliderLength: Slider
|
private lateinit var sliderLength: Slider
|
||||||
private lateinit var lengthEditView: EditText
|
private lateinit var lengthEditView: EditText
|
||||||
@@ -74,7 +74,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
passKeyView = view.findViewById(R.id.password_view)
|
passwordEditView = view.findViewById(R.id.password_view)
|
||||||
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
|
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
|
||||||
|
|
||||||
sliderLength = view.findViewById(R.id.slider_length)
|
sliderLength = view.findViewById(R.id.slider_length)
|
||||||
@@ -101,7 +101,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
passwordCopyView?.setOnClickListener {
|
passwordCopyView?.setOnClickListener {
|
||||||
clipboardHelper.timeoutCopyToClipboard(
|
clipboardHelper.timeoutCopyToClipboard(
|
||||||
getString(R.string.password),
|
getString(R.string.password),
|
||||||
passKeyView.passwordString,
|
passwordEditView.passwordString,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
generatePassword()
|
generatePassword()
|
||||||
|
|
||||||
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
|
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
|
||||||
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
|
mKeyGeneratorViewModel.setKeyGenerated(passwordEditView.passwordString)
|
||||||
}
|
}
|
||||||
|
|
||||||
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
|
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
|
||||||
@@ -293,24 +293,26 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
private fun generatePassword() {
|
private fun generatePassword() {
|
||||||
var password = ""
|
var password = ""
|
||||||
try {
|
try {
|
||||||
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
|
password = PasswordGenerator(resources).generatePassword(
|
||||||
uppercaseCompound.isChecked,
|
length = getPasswordLength(),
|
||||||
lowercaseCompound.isChecked,
|
upperCase = uppercaseCompound.isChecked,
|
||||||
digitsCompound.isChecked,
|
lowerCase = lowercaseCompound.isChecked,
|
||||||
minusCompound.isChecked,
|
digits = digitsCompound.isChecked,
|
||||||
underlineCompound.isChecked,
|
minus = minusCompound.isChecked,
|
||||||
spaceCompound.isChecked,
|
underline = underlineCompound.isChecked,
|
||||||
specialsCompound.isChecked,
|
space = spaceCompound.isChecked,
|
||||||
bracketsCompound.isChecked,
|
specials = specialsCompound.isChecked,
|
||||||
extendedCompound.isChecked,
|
brackets = bracketsCompound.isChecked,
|
||||||
getConsiderChars(),
|
extended = extendedCompound.isChecked,
|
||||||
getIgnoreChars(),
|
considerChars = getConsiderChars(),
|
||||||
atLeastOneCompound.isChecked,
|
ignoreChars = getIgnoreChars(),
|
||||||
excludeAmbiguousCompound.isChecked)
|
atLeastOneFromEach = atLeastOneCompound.isChecked,
|
||||||
|
excludeAmbiguousChar = excludeAmbiguousCompound.isChecked
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to generate a password", e)
|
Log.e(TAG, "Unable to generate a password", e)
|
||||||
}
|
}
|
||||||
passKeyView.passwordString = password
|
passwordEditView.passwordString = password
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -318,7 +320,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|
||||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
|
||||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
|
||||||
|
|
||||||
private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
|
|
||||||
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
|
||||||
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
|
||||||
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
|
||||||
|
|
||||||
fun startActivityForSearchModeResult(context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForSaveModeResult(context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SAVE)
|
|
||||||
addTypeModeInIntent(intent, TypeMode.DEFAULT)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForKeyboardSelectionModeResult(context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
|
||||||
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
|
|
||||||
addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForRegistrationModeResult(context: Context,
|
|
||||||
intent: Intent,
|
|
||||||
registerInfo: RegisterInfo?) {
|
|
||||||
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
|
||||||
// At the moment, only autofill for registration
|
|
||||||
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
|
||||||
addRegisterInfoInIntent(intent, registerInfo)
|
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
|
||||||
searchInfo?.let {
|
|
||||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
|
||||||
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
|
||||||
registerInfo?.let {
|
|
||||||
intent.putExtra(KEY_REGISTER_INFO, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
|
||||||
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeInfoFromIntent(intent: Intent) {
|
|
||||||
intent.removeExtra(KEY_SEARCH_INFO)
|
|
||||||
intent.removeExtra(KEY_REGISTER_INFO)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
|
||||||
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
|
||||||
return SpecialMode.SELECTION
|
|
||||||
}
|
|
||||||
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
|
||||||
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
|
||||||
return TypeMode.AUTOFILL
|
|
||||||
}
|
|
||||||
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeModesFromIntent(intent: Intent) {
|
|
||||||
intent.removeExtra(KEY_SPECIAL_MODE)
|
|
||||||
intent.removeExtra(KEY_TYPE_MODE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doSpecialAction(intent: Intent,
|
|
||||||
defaultAction: () -> Unit,
|
|
||||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
|
||||||
saveAction: (searchInfo: SearchInfo) -> Unit,
|
|
||||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
|
||||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
|
||||||
autofillComponent: AutofillComponent) -> Unit,
|
|
||||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
|
||||||
|
|
||||||
when (retrieveSpecialModeFromIntent(intent)) {
|
|
||||||
SpecialMode.DEFAULT -> {
|
|
||||||
removeModesFromIntent(intent)
|
|
||||||
removeInfoFromIntent(intent)
|
|
||||||
defaultAction.invoke()
|
|
||||||
}
|
|
||||||
SpecialMode.SEARCH -> {
|
|
||||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
|
||||||
removeModesFromIntent(intent)
|
|
||||||
removeInfoFromIntent(intent)
|
|
||||||
if (searchInfo != null)
|
|
||||||
searchAction.invoke(searchInfo)
|
|
||||||
else {
|
|
||||||
defaultAction.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SpecialMode.SAVE -> {
|
|
||||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
|
||||||
removeModesFromIntent(intent)
|
|
||||||
removeInfoFromIntent(intent)
|
|
||||||
if (searchInfo != null)
|
|
||||||
saveAction.invoke(searchInfo)
|
|
||||||
else {
|
|
||||||
defaultAction.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SpecialMode.SELECTION -> {
|
|
||||||
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
|
|
||||||
var autofillComponentInit = false
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
|
|
||||||
autofillSelectionAction.invoke(searchInfo, autofillComponent)
|
|
||||||
autofillComponentInit = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!autofillComponentInit) {
|
|
||||||
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
|
||||||
when (retrieveTypeModeFromIntent(intent)) {
|
|
||||||
TypeMode.DEFAULT -> {
|
|
||||||
removeModesFromIntent(intent)
|
|
||||||
if (searchInfo != null)
|
|
||||||
searchAction.invoke(searchInfo)
|
|
||||||
else
|
|
||||||
defaultAction.invoke()
|
|
||||||
}
|
|
||||||
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
|
||||||
else -> {
|
|
||||||
// In this case, error
|
|
||||||
removeModesFromIntent(intent)
|
|
||||||
removeInfoFromIntent(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (searchInfo != null)
|
|
||||||
searchAction.invoke(searchInfo)
|
|
||||||
else
|
|
||||||
defaultAction.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SpecialMode.REGISTRATION -> {
|
|
||||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
|
||||||
removeModesFromIntent(intent)
|
|
||||||
removeInfoFromIntent(intent)
|
|
||||||
autofillRegistrationAction.invoke(registerInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
|
||||||
|
|
||||||
enum class TypeMode {
|
|
||||||
DEFAULT, MAGIKEYBOARD, AUTOFILL
|
|
||||||
}
|
|
||||||
@@ -1,96 +1,307 @@
|
|||||||
package com.kunzisoft.keepass.activities.legacy
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
import android.net.Uri
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider.Companion.startDatabaseService
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||||
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||||
|
import com.kunzisoft.keepass.tasks.ProgressTaskViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
|
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
|
||||||
|
|
||||||
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
protected val mDatabase: ContextualDatabase?
|
||||||
protected var mDatabase: ContextualDatabase? = null
|
get() = mDatabaseViewModel.database
|
||||||
|
|
||||||
|
private val progressTaskViewModel: ProgressTaskViewModel by viewModels()
|
||||||
|
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||||
|
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||||
|
|
||||||
|
private val mActionDatabaseListener =
|
||||||
|
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||||
|
override fun onDatabaseChangeValidated() {
|
||||||
|
mDatabaseViewModel.onDatabaseChangeValidated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
|
||||||
|
private val requestPermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { _ ->
|
||||||
|
// Whether or not the user has accepted, the service can be started,
|
||||||
|
// There just won't be any notification if it's not allowed.
|
||||||
|
tempServiceParameters.removeFirstOrNull()?.let {
|
||||||
|
startDatabaseService(it.first, it.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Useful to only waiting for the activity result and prevent any parallel action
|
||||||
|
*/
|
||||||
|
var credentialResultLaunched = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility activity result launcher,
|
||||||
|
* Used recursively, close each activity with return data
|
||||||
|
*/
|
||||||
|
protected var mCredentialActivityResultLauncher: CredentialActivityResultLauncher =
|
||||||
|
CredentialActivityResultLauncher(
|
||||||
|
registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = false,
|
||||||
|
resultCode = it.resultCode,
|
||||||
|
data = it.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom ActivityResultLauncher to manage the database action
|
||||||
|
*/
|
||||||
|
protected inner class CredentialActivityResultLauncher(
|
||||||
|
val builder: ActivityResultLauncher<Intent>
|
||||||
|
) : ActivityResultLauncher<Intent>() {
|
||||||
|
|
||||||
|
override fun launch(
|
||||||
|
input: Intent?,
|
||||||
|
options: ActivityOptionsCompat?
|
||||||
|
) {
|
||||||
|
credentialResultLaunched = true
|
||||||
|
builder.launch(input, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregister() {
|
||||||
|
builder.unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getContract(): ActivityResultContract<Intent?, *> {
|
||||||
|
return builder.getContract()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(CREDENTIAL_RESULT_LAUNCHER_KEY)
|
||||||
|
) {
|
||||||
|
credentialResultLaunched = savedInstanceState.getBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
lifecycleScope.launch {
|
||||||
val databaseWasReloaded = database?.wasReloaded == true
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
|
mDatabaseViewModel.actionState.collect { uiState ->
|
||||||
|
if (credentialResultLaunched.not()) {
|
||||||
|
when (uiState) {
|
||||||
|
is DatabaseViewModel.ActionState.Wait -> {}
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseReloaded -> {
|
||||||
|
if (finishActivityIfReloadRequested()) {
|
||||||
finish()
|
finish()
|
||||||
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
|
}
|
||||||
database?.wasReloaded = false
|
}
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseInfoChanged -> {
|
||||||
|
if (manageDatabaseInfo()) {
|
||||||
|
showDatabaseChangedDialog(
|
||||||
|
uiState.previousDatabaseInfo,
|
||||||
|
uiState.newDatabaseInfo,
|
||||||
|
uiState.readOnlyDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionRequested -> {
|
||||||
|
startDatabasePermissionService(
|
||||||
|
uiState.bundle,
|
||||||
|
uiState.actionTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionStarted -> {
|
||||||
|
progressTaskViewModel.show(uiState.progressMessage)
|
||||||
|
}
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionUpdated -> {
|
||||||
|
progressTaskViewModel.show(uiState.progressMessage)
|
||||||
|
}
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionStopped -> {
|
||||||
|
progressTaskViewModel.hide()
|
||||||
|
}
|
||||||
|
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||||
|
onDatabaseActionFinished(
|
||||||
|
uiState.database,
|
||||||
|
uiState.actionTask,
|
||||||
|
uiState.result
|
||||||
|
)
|
||||||
|
progressTaskViewModel.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
progressTaskViewModel.progressTaskState.collect { state ->
|
||||||
|
when (state) {
|
||||||
|
is ProgressTaskViewModel.ProgressTaskState.Show ->
|
||||||
|
startDialog()
|
||||||
|
is ProgressTaskViewModel.ProgressTaskState.Hide ->
|
||||||
|
stopDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
mDatabaseViewModel.databaseState.collect { database ->
|
||||||
|
if (credentialResultLaunched.not()) {
|
||||||
|
// Nullable function
|
||||||
|
onUnknownDatabaseRetrieved(database)
|
||||||
|
database?.let {
|
||||||
onDatabaseRetrieved(database)
|
onDatabaseRetrieved(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
|
}
|
||||||
onDatabaseActionFinished(database, actionTask, result)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun showDatabaseDialog(): Boolean {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
return true
|
outState.putBoolean(CREDENTIAL_RESULT_LAUNCHER_KEY, credentialResultLaunched)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
/**
|
||||||
mDatabaseTaskProvider?.destroy()
|
* Nullable function to retrieve a database
|
||||||
mDatabaseTaskProvider = null
|
*/
|
||||||
mDatabase = null
|
open fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
mDatabase = database
|
|
||||||
mDatabaseViewModel.defineDatabase(database)
|
|
||||||
// optional method implementation
|
// optional method implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
|
// optional method implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun manageDatabaseInfo(): Boolean = true
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
mDatabaseViewModel.onActionFinished(database, actionTask, result)
|
|
||||||
// optional method implementation
|
// optional method implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDatabase(
|
private fun startDatabasePermissionService(bundle: Bundle?, actionTask: String) {
|
||||||
databaseUri: Uri,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
mainCredential: MainCredential
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
== PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
startDatabaseService(bundle, actionTask)
|
||||||
}
|
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
|
||||||
|
this,
|
||||||
fun loadDatabase(
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
databaseUri: Uri,
|
)
|
||||||
mainCredential: MainCredential,
|
|
||||||
readOnly: Boolean,
|
|
||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
|
||||||
fixDuplicateUuid: Boolean
|
|
||||||
) {
|
) {
|
||||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
// it's not the first time, so the user deliberately chooses not to display the notification
|
||||||
|
startDatabaseService(bundle, actionTask)
|
||||||
|
} else {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.warning_database_notification_permission)
|
||||||
|
.setNegativeButton(R.string.later) { _, _ ->
|
||||||
|
// Refuses the notification, so start the service
|
||||||
|
startDatabaseService(bundle, actionTask)
|
||||||
|
}
|
||||||
|
.setPositiveButton(R.string.ask) { _, _ ->
|
||||||
|
// Save the temp parameters to ask the permission
|
||||||
|
tempServiceParameters.add(Pair(bundle, actionTask))
|
||||||
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}.create().show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startDatabaseService(bundle, actionTask)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun closeDatabase() {
|
private fun showDatabaseChangedDialog(
|
||||||
mDatabase?.clearAndClose(this.getBinaryDir())
|
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
readOnlyDatabase: Boolean
|
||||||
|
) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (databaseChangedDialogFragment == null) {
|
||||||
|
databaseChangedDialogFragment = supportFragmentManager
|
||||||
|
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||||
|
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||||
|
mActionDatabaseListener
|
||||||
|
}
|
||||||
|
if (progressTaskDialogFragment == null) {
|
||||||
|
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||||
|
previousDatabaseInfo,
|
||||||
|
newDatabaseInfo,
|
||||||
|
readOnlyDatabase
|
||||||
|
)
|
||||||
|
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||||
|
mActionDatabaseListener
|
||||||
|
databaseChangedDialogFragment?.show(
|
||||||
|
supportFragmentManager,
|
||||||
|
DATABASE_CHANGED_DIALOG_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
private fun startDialog() {
|
||||||
super.onResume()
|
lifecycleScope.launch {
|
||||||
mDatabaseTaskProvider?.registerProgressTask()
|
if (showDatabaseDialog()) {
|
||||||
|
if (progressTaskDialogFragment == null) {
|
||||||
|
progressTaskDialogFragment = supportFragmentManager
|
||||||
|
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||||
|
}
|
||||||
|
if (progressTaskDialogFragment == null) {
|
||||||
|
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||||
|
progressTaskDialogFragment?.show(
|
||||||
|
supportFragmentManager,
|
||||||
|
PROGRESS_TASK_DIALOG_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
private fun stopDialog() {
|
||||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
progressTaskDialogFragment?.dismissAllowingStateLoss()
|
||||||
super.onPause()
|
progressTaskDialogFragment = null
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun showDatabaseDialog(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CREDENTIAL_RESULT_LAUNCHER_KEY = "com.kunzisoft.keepass.CREDENTIAL_RESULT_LAUNCHER_KEY"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,8 +34,8 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
@@ -47,10 +47,14 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
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.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||||
PasswordEncodingDialogFragment.Listener {
|
PasswordEncodingDialogFragment.Listener {
|
||||||
@@ -83,109 +87,24 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
deleteDatabaseNodes(nodes)
|
deleteDatabaseNodes(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.saveDatabase.observe(this) { save ->
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
|
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge(save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
|
||||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveName.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveDescription.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveColor.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveCompression.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.removeUnlinkData.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveRecycleBin.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveEncryption.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveKeyDerivation.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveIterations.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveMemoryUsage.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseViewModel.saveParallelism.observe(this) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
|
|
||||||
}
|
|
||||||
|
|
||||||
mExitLock = false
|
mExitLock = false
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
// End activity if database not loaded
|
// End activity if database not loaded
|
||||||
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
|
if (database.loaded.not())
|
||||||
finish()
|
finish()
|
||||||
}
|
|
||||||
|
|
||||||
// Focus view to reinitialize timeout,
|
// Focus view to reinitialize timeout,
|
||||||
// view is not necessary loaded so retry later in resume
|
// view is not necessary loaded so retry later in resume
|
||||||
viewToInvalidateTimeout()
|
viewToInvalidateTimeout()
|
||||||
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
|
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
|
||||||
|
|
||||||
database?.let {
|
|
||||||
// check timeout
|
// check timeout
|
||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
if (mLockReceiver == null) {
|
if (mLockReceiver == null) {
|
||||||
mLockReceiver = LockReceiver {
|
mLockReceiver = LockReceiver {
|
||||||
mDatabase = null
|
|
||||||
closeDatabase(database)
|
closeDatabase(database)
|
||||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
|
||||||
mExitLock = true
|
mExitLock = true
|
||||||
closeOptionsMenu()
|
closeOptionsMenu()
|
||||||
finish()
|
finish()
|
||||||
@@ -207,7 +126,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
checkRegister()
|
checkRegister()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// To fix weird crash
|
// To fix weird crash
|
||||||
@@ -225,7 +143,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
@@ -247,24 +164,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
databaseUri: Uri?,
|
databaseUri: Uri?,
|
||||||
mainCredential: MainCredential
|
mainCredential: MainCredential
|
||||||
) {
|
) {
|
||||||
assignDatabasePassword(databaseUri, mainCredential)
|
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignDatabasePassword(
|
fun assignMainCredential(mainCredential: MainCredential) {
|
||||||
databaseUri: Uri?,
|
|
||||||
mainCredential: MainCredential
|
|
||||||
) {
|
|
||||||
if (databaseUri != null) {
|
|
||||||
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assignPassword(mainCredential: MainCredential) {
|
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
database.fileUri?.let { databaseUri ->
|
database.fileUri?.let { databaseUri ->
|
||||||
// Show the progress dialog now or after dialog confirmation
|
// Show the progress dialog now or after dialog confirmation
|
||||||
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
|
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
|
||||||
assignDatabasePassword(databaseUri, mainCredential)
|
mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
|
||||||
} else {
|
} else {
|
||||||
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
||||||
.show(supportFragmentManager, "passwordEncodingTag")
|
.show(supportFragmentManager, "passwordEncodingTag")
|
||||||
@@ -274,45 +182,51 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveDatabase() {
|
fun saveDatabase() {
|
||||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
mDatabaseViewModel.saveDatabase(save = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDatabaseTo(uri: Uri) {
|
fun saveDatabaseTo(uri: Uri) {
|
||||||
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
|
mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeDatabase() {
|
fun mergeDatabase() {
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
|
mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
|
mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadDatabase() {
|
fun reloadDatabase() {
|
||||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
|
||||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createEntry(newEntry: Entry,
|
fun createEntry(
|
||||||
parent: Group) {
|
newEntry: Entry,
|
||||||
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
|
parent: Group
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateEntry(oldEntry: Entry,
|
fun updateEntry(
|
||||||
entryToUpdate: Entry) {
|
oldEntry: Entry,
|
||||||
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
entryToUpdate: Entry
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyNodes(nodesToCopy: List<Node>,
|
fun copyNodes(
|
||||||
newParent: Group) {
|
nodesToCopy: List<Node>,
|
||||||
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
newParent: Group
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveNodes(nodesToMove: List<Node>,
|
fun moveNodes(
|
||||||
newParent: Group) {
|
nodesToMove: List<Node>,
|
||||||
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
newParent: Group
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
|
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
|
||||||
@@ -328,6 +242,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
|
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
|
||||||
|
// TODO Move in ViewModel
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
// If recycle bin enabled, ensure it exists
|
// If recycle bin enabled, ensure it exists
|
||||||
if (database.isRecycleBinEnabled) {
|
if (database.isRecycleBinEnabled) {
|
||||||
@@ -348,11 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
||||||
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
|
mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createGroup(parent: Group,
|
fun createGroup(
|
||||||
groupInfo: GroupInfo?) {
|
parent: Group,
|
||||||
|
groupInfo: GroupInfo?
|
||||||
|
) {
|
||||||
|
// TODO Move in ViewModel
|
||||||
// Build the group
|
// Build the group
|
||||||
mDatabase?.createGroup()?.let { newGroup ->
|
mDatabase?.createGroup()?.let { newGroup ->
|
||||||
groupInfo?.let { info ->
|
groupInfo?.let { info ->
|
||||||
@@ -360,12 +278,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
// Not really needed here because added in runnable but safe
|
// Not really needed here because added in runnable but safe
|
||||||
newGroup.parent = parent
|
newGroup.parent = parent
|
||||||
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
|
mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateGroup(oldGroup: Group,
|
fun updateGroup(
|
||||||
groupInfo: GroupInfo) {
|
oldGroup: Group,
|
||||||
|
groupInfo: GroupInfo
|
||||||
|
) {
|
||||||
|
// TODO Move in ViewModel
|
||||||
// If group updated save it in the database
|
// If group updated save it in the database
|
||||||
val updateGroup = Group(oldGroup).let { updateGroup ->
|
val updateGroup = Group(oldGroup).let { updateGroup ->
|
||||||
updateGroup.apply {
|
updateGroup.apply {
|
||||||
@@ -375,27 +296,28 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
this.setGroupInfo(groupInfo)
|
this.setGroupInfo(groupInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
|
fun restoreEntryHistory(
|
||||||
entryHistoryPosition: Int) {
|
mainEntryId: NodeId<UUID>,
|
||||||
mDatabaseTaskProvider
|
entryHistoryPosition: Int
|
||||||
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
) {
|
||||||
|
mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
|
fun deleteEntryHistory(
|
||||||
entryHistoryPosition: Int) {
|
mainEntryId: NodeId<UUID>,
|
||||||
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
entryHistoryPosition: Int
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkRegister() {
|
private fun checkRegister() {
|
||||||
// If in ave or registration mode, don't allow read only
|
// If in registration mode, don't allow read only
|
||||||
if ((mSpecialMode == SpecialMode.SAVE
|
if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
|
||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|
||||||
&& mDatabaseReadOnly) {
|
|
||||||
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,8 +335,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
||||||
@@ -429,8 +349,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
@@ -452,9 +370,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.lock) { _, _ ->
|
.setPositiveButton(R.string.lock) { _, _ ->
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
finish()
|
||||||
}.create().show()
|
}.create().show()
|
||||||
} else {
|
} else {
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,9 +400,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
|
|
||||||
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
|
||||||
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,16 @@ import android.view.View
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeInfo
|
||||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveTypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.ToolbarSpecial
|
import com.kunzisoft.keepass.view.ToolbarSpecial
|
||||||
@@ -19,7 +26,7 @@ import com.kunzisoft.keepass.view.ToolbarSpecial
|
|||||||
abstract class DatabaseModeActivity : DatabaseActivity() {
|
abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||||
|
|
||||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||||
|
|
||||||
private var mToolbarSpecial: ToolbarSpecial? = null
|
private var mToolbarSpecial: ToolbarSpecial? = null
|
||||||
|
|
||||||
@@ -42,20 +49,14 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
/**
|
/**
|
||||||
* Intent sender uses special retains data in callback
|
* Intent sender uses special retains data in callback
|
||||||
*/
|
*/
|
||||||
private fun isIntentSender(): Boolean {
|
protected fun isIntentSender(): Boolean {
|
||||||
return (mSpecialMode == SpecialMode.SELECTION
|
return isIntentSenderMode(mSpecialMode, mTypeMode)
|
||||||
&& mTypeMode == TypeMode.AUTOFILL)
|
|
||||||
/* TODO Registration callback #765
|
|
||||||
|| (mSpecialMode == SpecialMode.REGISTRATION
|
|
||||||
&& mTypeMode == TypeMode.AUTOFILL
|
|
||||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLaunchActivitySpecialMode() {
|
fun onLaunchActivitySpecialMode() {
|
||||||
if (!isIntentSender()) {
|
if (!isIntentSender()) {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,8 +65,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
if (isIntentSender()) {
|
if (isIntentSender()) {
|
||||||
super.finish()
|
super.finish()
|
||||||
} else {
|
} else {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
backToTheMainAppAndFinish()
|
backToTheMainAppAndFinish()
|
||||||
}
|
}
|
||||||
@@ -77,8 +78,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
// To get the app caller, only for IntentSender
|
// To get the app caller, only for IntentSender
|
||||||
onRegularBackPressed()
|
onRegularBackPressed()
|
||||||
} else {
|
} else {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
intent.removeModes()
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
intent.removeInfo()
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
backToTheMainAppAndFinish()
|
backToTheMainAppAndFinish()
|
||||||
}
|
}
|
||||||
@@ -109,17 +110,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
mSpecialMode = intent.retrieveSpecialMode()
|
||||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
mTypeMode = intent.retrieveTypeMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
mSpecialMode = intent.retrieveSpecialMode()
|
||||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
mTypeMode = intent.retrieveTypeMode()
|
||||||
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
|
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
|
||||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
||||||
|
?: intent.retrieveSearchInfo()
|
||||||
|
|
||||||
// To show the selection mode
|
// To show the selection mode
|
||||||
mToolbarSpecial = findViewById(R.id.special_mode_view)
|
mToolbarSpecial = findViewById(R.id.special_mode_view)
|
||||||
@@ -128,26 +130,25 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
val selectionModeStringId = when (mSpecialMode) {
|
val selectionModeStringId = when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT, // Not important because hidden
|
SpecialMode.DEFAULT, // Not important because hidden
|
||||||
SpecialMode.SEARCH -> R.string.search_mode
|
SpecialMode.SEARCH -> R.string.search_mode
|
||||||
SpecialMode.SAVE -> R.string.save_mode
|
|
||||||
SpecialMode.SELECTION -> R.string.selection_mode
|
SpecialMode.SELECTION -> R.string.selection_mode
|
||||||
SpecialMode.REGISTRATION -> R.string.registration_mode
|
SpecialMode.REGISTRATION -> R.string.save_mode // Save is registration mode
|
||||||
}
|
}
|
||||||
val typeModeStringId = when (mTypeMode) {
|
val typeModeStringId = when (mTypeMode) {
|
||||||
TypeMode.DEFAULT, // Not important because hidden
|
TypeMode.DEFAULT, // Not important because hidden
|
||||||
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
||||||
TypeMode.AUTOFILL -> R.string.autofill
|
TypeMode.AUTOFILL -> R.string.autofill
|
||||||
|
TypeMode.PASSKEY -> R.string.passkey
|
||||||
}
|
}
|
||||||
title = getString(selectionModeStringId)
|
title = getString(selectionModeStringId)
|
||||||
if (mTypeMode != TypeMode.DEFAULT)
|
if (mTypeMode != TypeMode.DEFAULT)
|
||||||
title = "$title (${getString(typeModeStringId)})"
|
title = "$title (${getString(typeModeStringId)})"
|
||||||
// Populate subtitle
|
// Populate subtitle
|
||||||
subtitle = searchInfo?.getName(resources)
|
subtitle = registerInfo?.getName(resources) ?: searchInfo?.getName(resources)
|
||||||
|
|
||||||
// Show the toolbar or not
|
// Show the toolbar or not
|
||||||
visible = when (mSpecialMode) {
|
visible = when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> false
|
SpecialMode.DEFAULT -> false
|
||||||
SpecialMode.SEARCH -> true
|
SpecialMode.SEARCH -> true
|
||||||
SpecialMode.SAVE -> true
|
|
||||||
SpecialMode.SELECTION -> true
|
SpecialMode.SELECTION -> true
|
||||||
SpecialMode.REGISTRATION -> true
|
SpecialMode.REGISTRATION -> true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
|
||||||
interface DatabaseRetrieval {
|
interface DatabaseRetrieval {
|
||||||
fun onDatabaseRetrieved(database: ContextualDatabase?)
|
fun onDatabaseRetrieved(database: ContextualDatabase)
|
||||||
fun onDatabaseActionFinished(database: ContextualDatabase,
|
|
||||||
|
fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result)
|
result: ActionRunnable.Result
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.stylish
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -77,7 +78,18 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
finish()
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
@@ -17,7 +18,7 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
|
|
||||||
class BreadcrumbAdapter(val context: Context)
|
class BreadcrumbAdapter(val context: Context, val database: Database?)
|
||||||
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
|
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
@@ -31,6 +32,8 @@ class BreadcrumbAdapter(val context: Context)
|
|||||||
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||||
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||||
|
|
||||||
|
private var mNodeFilter: NodeFilter = NodeFilter(context, database)
|
||||||
|
|
||||||
private var mShowNumberEntries = false
|
private var mShowNumberEntries = false
|
||||||
private var mShowUUID = false
|
private var mShowUUID = false
|
||||||
private var mIconColor: Int = 0
|
private var mIconColor: Int = 0
|
||||||
@@ -112,12 +115,10 @@ class BreadcrumbAdapter(val context: Context)
|
|||||||
|
|
||||||
holder.groupNumbersView?.apply {
|
holder.groupNumbersView?.apply {
|
||||||
if (mShowNumberEntries) {
|
if (mShowNumberEntries) {
|
||||||
group.refreshNumberOfChildEntries(
|
text = group.getNumberOfChildEntries(
|
||||||
Group.ChildFilter.getDefaults(
|
mNodeFilter.recursiveNumberOfEntries,
|
||||||
PreferencesUtil.showExpiredEntries(context)
|
mNodeFilter.filter
|
||||||
)
|
).toString()
|
||||||
)
|
|
||||||
text = group.recursiveNumberOfChildEntries.toString()
|
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
class NodeFilter(
|
||||||
|
context: Context,
|
||||||
|
var database: Database? = null
|
||||||
|
) {
|
||||||
|
var recursiveNumberOfEntries = PreferencesUtil.recursiveNumberEntries(context)
|
||||||
|
private set
|
||||||
|
private var showExpired = PreferencesUtil.showExpiredEntries(context)
|
||||||
|
private var showTemplate = PreferencesUtil.showTemplates(context)
|
||||||
|
|
||||||
|
val filter: (Node) -> Boolean = { node ->
|
||||||
|
when (node) {
|
||||||
|
is Entry -> {
|
||||||
|
node.entryKDB?.isMetaStream() != true
|
||||||
|
}
|
||||||
|
is Group -> {
|
||||||
|
showTemplate || database?.templatesGroup != node
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
} && (showExpired || !node.isCurrentlyExpires)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,7 @@ class NodesAdapter (
|
|||||||
private val mNodeSortedListCallback: NodeSortedListCallback
|
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||||
private val mNodeSortedList: SortedList<Node>
|
private val mNodeSortedList: SortedList<Node>
|
||||||
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
private val mNodeFilter: NodeFilter = NodeFilter(context, database)
|
||||||
|
|
||||||
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
||||||
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||||
@@ -82,7 +83,7 @@ class NodesAdapter (
|
|||||||
private var mShowNumberEntries: Boolean = true
|
private var mShowNumberEntries: Boolean = true
|
||||||
private var mShowOTP: Boolean = false
|
private var mShowOTP: Boolean = false
|
||||||
private var mShowUUID: Boolean = false
|
private var mShowUUID: Boolean = false
|
||||||
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
private var mNodeFilters: NodeFilter? = null
|
||||||
private var mOldVirtualGroup = false
|
private var mOldVirtualGroup = false
|
||||||
private var mVirtualGroup = false
|
private var mVirtualGroup = false
|
||||||
|
|
||||||
@@ -161,9 +162,7 @@ class NodesAdapter (
|
|||||||
this.mShowOTP = PreferencesUtil.showOTPToken(context)
|
this.mShowOTP = PreferencesUtil.showOTPToken(context)
|
||||||
this.mShowUUID = PreferencesUtil.showUUID(context)
|
this.mShowUUID = PreferencesUtil.showUUID(context)
|
||||||
|
|
||||||
this.mEntryFilters = Group.ChildFilter.getDefaults(
|
this.mNodeFilters = NodeFilter(context, database)
|
||||||
PreferencesUtil.showExpiredEntries(context)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reinit textSize for all view type
|
// Reinit textSize for all view type
|
||||||
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
||||||
@@ -176,7 +175,7 @@ class NodesAdapter (
|
|||||||
mOldVirtualGroup = mVirtualGroup
|
mOldVirtualGroup = mVirtualGroup
|
||||||
mVirtualGroup = group.isVirtual
|
mVirtualGroup = group.isVirtual
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
mNodeSortedList.replaceAll(group.getChildren(mNodeFilter.filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
||||||
@@ -205,6 +204,11 @@ class NodesAdapter (
|
|||||||
&& oldItem.type == newItem.type
|
&& oldItem.type == newItem.type
|
||||||
&& oldItem.title == newItem.title
|
&& oldItem.title == newItem.title
|
||||||
&& oldItem.icon == newItem.icon
|
&& oldItem.icon == newItem.icon
|
||||||
|
&& oldItem.creationTime == newItem.creationTime
|
||||||
|
&& oldItem.lastModificationTime == newItem.lastModificationTime
|
||||||
|
&& oldItem.lastAccessTime == newItem.lastAccessTime
|
||||||
|
&& oldItem.expiryTime == newItem.expiryTime
|
||||||
|
&& oldItem.expires == newItem.expires
|
||||||
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
|
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,6 +417,7 @@ class NodesAdapter (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OTP
|
||||||
val otpElement = entry.getOtpElement()
|
val otpElement = entry.getOtpElement()
|
||||||
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
|
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
|
||||||
if (otpElement != null
|
if (otpElement != null
|
||||||
@@ -435,6 +440,10 @@ class NodesAdapter (
|
|||||||
holder.attachmentIcon?.visibility =
|
holder.attachmentIcon?.visibility =
|
||||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// Passkey
|
||||||
|
holder.passkeyIcon?.visibility =
|
||||||
|
if (entry.getPasskey() != null) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
// Assign colors
|
// Assign colors
|
||||||
assignBackgroundColor(holder.container, entry)
|
assignBackgroundColor(holder.container, entry)
|
||||||
assignBackgroundColor(holder.otpContainer, entry)
|
assignBackgroundColor(holder.otpContainer, entry)
|
||||||
@@ -446,6 +455,7 @@ class NodesAdapter (
|
|||||||
holder.otpToken?.setTextColor(foregroundColor)
|
holder.otpToken?.setTextColor(foregroundColor)
|
||||||
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
||||||
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
||||||
|
holder.passkeyIcon?.setColorFilter(foregroundColor)
|
||||||
holder.meta.setTextColor(foregroundColor)
|
holder.meta.setTextColor(foregroundColor)
|
||||||
iconColor = foregroundColor
|
iconColor = foregroundColor
|
||||||
} else {
|
} else {
|
||||||
@@ -454,6 +464,7 @@ class NodesAdapter (
|
|||||||
holder.otpToken?.setTextColor(mTextColorSecondary)
|
holder.otpToken?.setTextColor(mTextColorSecondary)
|
||||||
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
||||||
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
||||||
|
holder.passkeyIcon?.setColorFilter(mTextColorSecondary)
|
||||||
holder.meta.setTextColor(mTextColor)
|
holder.meta.setTextColor(mTextColor)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -462,6 +473,7 @@ class NodesAdapter (
|
|||||||
holder.otpToken?.setTextColor(mColorOnSecondary)
|
holder.otpToken?.setTextColor(mColorOnSecondary)
|
||||||
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
||||||
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
||||||
|
holder.passkeyIcon?.setColorFilter(mColorOnSecondary)
|
||||||
holder.meta.setTextColor(mColorOnSecondary)
|
holder.meta.setTextColor(mColorOnSecondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +485,10 @@ class NodesAdapter (
|
|||||||
if (mShowNumberEntries) {
|
if (mShowNumberEntries) {
|
||||||
holder.numberChildren?.apply {
|
holder.numberChildren?.apply {
|
||||||
text = (subNode as Group)
|
text = (subNode as Group)
|
||||||
.recursiveNumberOfChildEntries
|
.getNumberOfChildEntries(
|
||||||
|
mNodeFilter.recursiveNumberOfEntries,
|
||||||
|
mNodeFilter.filter
|
||||||
|
)
|
||||||
.toString()
|
.toString()
|
||||||
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@@ -522,6 +537,8 @@ class NodesAdapter (
|
|||||||
holder?.otpToken?.apply {
|
holder?.otpToken?.apply {
|
||||||
text = otpElement?.tokenString
|
text = otpElement?.tokenString
|
||||||
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
|
textDirection = View.TEXT_DIRECTION_LTR
|
||||||
|
|
||||||
}
|
}
|
||||||
holder?.otpContainer?.setOnClickListener {
|
holder?.otpContainer?.setOnClickListener {
|
||||||
otpElement?.token?.let { token ->
|
otpElement?.token?.let { token ->
|
||||||
@@ -601,6 +618,7 @@ class NodesAdapter (
|
|||||||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||||
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||||
|
var passkeyIcon: ImageView? = itemView.findViewById(R.id.node_passkey_icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -19,15 +19,53 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.app
|
package com.kunzisoft.keepass.app
|
||||||
|
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
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() {
|
class App : MultiDexApplication() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver)
|
||||||
|
|
||||||
Stylish.load(this)
|
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
|
import androidx.room.AutoMigration
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
version = 2,
|
version = 3,
|
||||||
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
|
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration (from = 1, to = 2)
|
AutoMigration (from = 1, to = 2),
|
||||||
|
AutoMigration (from = 2, to = 3)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ import android.net.Uri
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
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.database.element.binary.BinaryData.Companion.BASE64_FLAG
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
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.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.IOActionTask
|
import com.kunzisoft.keepass.utils.IOActionTask
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
@@ -42,19 +44,19 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
AppDatabase.getDatabase(applicationContext).cipherDatabaseDao()
|
AppDatabase.getDatabase(applicationContext).cipherDatabaseDao()
|
||||||
|
|
||||||
// Temp DAO to easily remove content if object no longer in memory
|
// 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 mServiceConnection: ServiceConnection? = null
|
||||||
|
|
||||||
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
|
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
|
||||||
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
|
private var mDeviceUnlockBroadcastReceiver = DeviceUnlockNotificationService.DeviceUnlockReceiver {
|
||||||
deleteAll()
|
deleteAll()
|
||||||
removeAllDataAndDetach()
|
removeAllDataAndDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadPreferences() {
|
private fun reloadPreferences() {
|
||||||
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
useTempDao = PreferencesUtil.isTempDeviceUnlockEnable(applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -69,13 +71,15 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun attachService(performedAction: () -> Unit) {
|
private fun attachService(performedAction: () -> Unit) {
|
||||||
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
|
ContextCompat.registerReceiver(applicationContext, mDeviceUnlockBroadcastReceiver,
|
||||||
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
|
IntentFilter().apply {
|
||||||
})
|
addAction(DeviceUnlockNotificationService.REMOVE_DEVICE_UNLOCK_KEY_ACTION)
|
||||||
|
}, ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
|
||||||
mServiceConnection = object : ServiceConnection {
|
mServiceConnection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
|
mBinder = (serviceBinder as DeviceUnlockNotificationService.DeviceUnlockBinder)
|
||||||
performedAction.invoke()
|
performedAction.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +88,7 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
AdvancedUnlockNotificationService.bindService(applicationContext,
|
DeviceUnlockNotificationService.bindService(applicationContext,
|
||||||
mServiceConnection!!,
|
mServiceConnection!!,
|
||||||
Context.BIND_AUTO_CREATE)
|
Context.BIND_AUTO_CREATE)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -96,11 +100,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
private fun detachService() {
|
private fun detachService() {
|
||||||
try {
|
try {
|
||||||
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
|
applicationContext.unregisterReceiver(mDeviceUnlockBroadcastReceiver)
|
||||||
} catch (e: Exception) {}
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
mServiceConnection?.let {
|
mServiceConnection?.let {
|
||||||
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
|
DeviceUnlockNotificationService.unbindService(applicationContext, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,23 +124,27 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
private fun onClear() {
|
private fun onClear() {
|
||||||
mBinder = null
|
mBinder = null
|
||||||
mServiceConnection = null
|
mServiceConnection = null
|
||||||
mDatabaseListeners.forEach {
|
mDatabaseListeners.forEach { listener ->
|
||||||
it.onCipherDatabaseCleared()
|
listener.onCipherDatabaseCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CipherDatabaseListener {
|
interface CipherDatabaseListener {
|
||||||
|
fun onCipherDatabaseRetrieved(databaseUri: Uri, cipherDatabase: CipherEncryptDatabase?)
|
||||||
|
fun onCipherDatabaseAddedOrUpdated(cipherDatabase: CipherEncryptDatabase)
|
||||||
|
fun onCipherDatabaseDeleted(databaseUri: Uri)
|
||||||
|
fun onAllCipherDatabasesDeleted()
|
||||||
fun onCipherDatabaseCleared()
|
fun onCipherDatabaseCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCipherDatabase(databaseUri: Uri,
|
fun getCipherDatabase(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
|
cipherDatabaseResultListener: ((CipherEncryptDatabase?) -> Unit)? = null) {
|
||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
serviceActionTask {
|
serviceActionTask {
|
||||||
var cipherDatabase: CipherEncryptDatabase? = null
|
var cipherDatabase: CipherEncryptDatabase? = null
|
||||||
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
|
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
|
||||||
cipherDatabase = CipherEncryptDatabase().apply {
|
cipherDatabase = CipherEncryptDatabase().apply {
|
||||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
this.databaseUri = cipherDatabaseEntity.databaseUri.toUri()
|
||||||
this.encryptedValue = Base64.decode(
|
this.encryptedValue = Base64.decode(
|
||||||
cipherDatabaseEntity.encryptedValue,
|
cipherDatabaseEntity.encryptedValue,
|
||||||
BASE64_FLAG
|
BASE64_FLAG
|
||||||
@@ -147,7 +155,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cipherDatabaseResultListener.invoke(cipherDatabase)
|
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
@@ -155,7 +167,7 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||||
?.let { cipherDatabaseEntity ->
|
?.let { cipherDatabaseEntity ->
|
||||||
CipherEncryptDatabase().apply {
|
CipherEncryptDatabase().apply {
|
||||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
this.databaseUri = cipherDatabaseEntity.databaseUri.toUri()
|
||||||
this.encryptedValue = Base64.decode(
|
this.encryptedValue = Base64.decode(
|
||||||
cipherDatabaseEntity.encryptedValue,
|
cipherDatabaseEntity.encryptedValue,
|
||||||
Base64.NO_WRAP
|
Base64.NO_WRAP
|
||||||
@@ -167,19 +179,35 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ cipherDatabase ->
|
||||||
cipherDatabaseResultListener.invoke(it)
|
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCipherDatabase(databaseUri: Uri,
|
private fun containsCipherDatabase(databaseUri: Uri?,
|
||||||
contains: (Boolean) -> Unit) {
|
contains: (Boolean) -> Unit) {
|
||||||
|
if (databaseUri == null) {
|
||||||
|
contains.invoke(false)
|
||||||
|
} else {
|
||||||
getCipherDatabase(databaseUri) {
|
getCipherDatabase(databaseUri) {
|
||||||
contains.invoke(it != null)
|
contains.invoke(it != null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetCipherParameters(databaseUri: Uri?) {
|
||||||
|
containsCipherDatabase(databaseUri) { contains ->
|
||||||
|
if (contains) {
|
||||||
|
mBinder?.resetTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase,
|
fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
@@ -195,7 +223,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
// The only case to create service (not needed to get an info)
|
// The only case to create service (not needed to get an info)
|
||||||
serviceActionTask(true) {
|
serviceActionTask(true) {
|
||||||
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke() ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
@@ -210,7 +242,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke() ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
@@ -222,7 +258,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
serviceActionTask {
|
serviceActionTask {
|
||||||
mBinder?.deleteByDatabaseUri(databaseUri)
|
mBinder?.deleteByDatabaseUri(databaseUri)
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke() ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseDeleted(databaseUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
@@ -230,10 +270,15 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke() ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseDeleted(databaseUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
reloadPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
@@ -248,8 +293,12 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
cipherDatabaseDao.deleteAll()
|
cipherDatabaseDao.deleteAll()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onAllCipherDatabasesDeleted()
|
||||||
|
}
|
||||||
// Unbind
|
// Unbind
|
||||||
removeAllDataAndDetach()
|
removeAllDataAndDetach()
|
||||||
|
reloadPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
databaseUri,
|
databaseUri,
|
||||||
fileDatabaseHistoryEntity?.keyFileUri?.parseUri(),
|
fileDatabaseHistoryEntity?.keyFileUri?.parseUri(),
|
||||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
|
||||||
|
fileDatabaseHistoryEntity?.readOnly,
|
||||||
fileDatabaseHistoryEntity?.databaseUri?.decodeUri(),
|
fileDatabaseHistoryEntity?.databaseUri?.decodeUri(),
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias
|
||||||
?: ""),
|
?: ""),
|
||||||
@@ -99,6 +100,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
fileDatabaseHistoryEntity.databaseUri.parseUri(),
|
fileDatabaseHistoryEntity.databaseUri.parseUri(),
|
||||||
fileDatabaseHistoryEntity.keyFileUri?.parseUri(),
|
fileDatabaseHistoryEntity.keyFileUri?.parseUri(),
|
||||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
|
||||||
|
fileDatabaseHistoryEntity.readOnly,
|
||||||
fileDatabaseHistoryEntity.databaseUri.decodeUri(),
|
fileDatabaseHistoryEntity.databaseUri.decodeUri(),
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||||
fileDatabaseInfo.exists,
|
fileDatabaseInfo.exists,
|
||||||
@@ -147,6 +149,8 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
?: "",
|
?: "",
|
||||||
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||||
databaseFileToAddOrUpdate.hardwareKey?.value,
|
databaseFileToAddOrUpdate.hardwareKey?.value,
|
||||||
|
databaseFileToAddOrUpdate.readOnly
|
||||||
|
?: fileDatabaseHistoryRetrieve?.readOnly,
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -168,6 +172,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
fileDatabaseHistory.databaseUri.parseUri(),
|
fileDatabaseHistory.databaseUri.parseUri(),
|
||||||
fileDatabaseHistory.keyFileUri?.parseUri(),
|
fileDatabaseHistory.keyFileUri?.parseUri(),
|
||||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||||
|
fileDatabaseHistory.readOnly,
|
||||||
fileDatabaseHistory.databaseUri.decodeUri(),
|
fileDatabaseHistory.databaseUri.decodeUri(),
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||||
fileDatabaseInfo.exists,
|
fileDatabaseInfo.exists,
|
||||||
@@ -195,6 +200,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
fileDatabaseHistory.databaseUri.parseUri(),
|
fileDatabaseHistory.databaseUri.parseUri(),
|
||||||
fileDatabaseHistory.keyFileUri?.parseUri(),
|
fileDatabaseHistory.keyFileUri?.parseUri(),
|
||||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||||
|
fileDatabaseHistory.readOnly,
|
||||||
fileDatabaseHistory.databaseUri.decodeUri(),
|
fileDatabaseHistory.databaseUri.decodeUri(),
|
||||||
databaseFileToDelete.databaseAlias
|
databaseFileToDelete.databaseAlias
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ data class FileDatabaseHistoryEntity(
|
|||||||
@ColumnInfo(name = "hardware_key")
|
@ColumnInfo(name = "hardware_key")
|
||||||
var hardwareKey: String?,
|
var hardwareKey: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "read_only")
|
||||||
|
var readOnly: Boolean?,
|
||||||
|
|
||||||
@ColumnInfo(name = "updated")
|
@ColumnInfo(name = "updated")
|
||||||
val updated: Long
|
val updated: Long
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.autofill
|
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
|
||||||
|
|
||||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
|
||||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.autofill
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.PendingIntent
|
|
||||||
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.util.Log
|
|
||||||
import android.view.autofill.AutofillId
|
|
||||||
import android.widget.RemoteViews
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
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.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
|
||||||
import com.kunzisoft.keepass.model.CreditCard
|
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.utils.WebDomain
|
|
||||||
import org.joda.time.DateTime
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
class KeeAutofillService : AutofillService() {
|
|
||||||
|
|
||||||
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
|
||||||
private var mDatabase: ContextualDatabase? = null
|
|
||||||
private var applicationIdBlocklist: Set<String>? = null
|
|
||||||
private var webDomainBlocklist: Set<String>? = null
|
|
||||||
private var askToSaveData: Boolean = false
|
|
||||||
private var autofillInlineSuggestionsEnabled: Boolean = false
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
|
||||||
mDatabaseTaskProvider?.registerProgressTask()
|
|
||||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
|
||||||
this.mDatabase = database
|
|
||||||
}
|
|
||||||
|
|
||||||
getPreferences()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
|
||||||
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPreferences() {
|
|
||||||
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
|
||||||
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
|
||||||
askToSaveData = PreferencesUtil.askToSaveAutofillData(this)
|
|
||||||
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFillRequest(request: FillRequest,
|
|
||||||
cancellationSignal: CancellationSignal,
|
|
||||||
callback: FillCallback) {
|
|
||||||
|
|
||||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
|
||||||
|
|
||||||
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
|
|
||||||
Log.d(TAG, "Autofill requested in compatibility mode")
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Autofill requested in native mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check user's settings for authenticating Responses and Datasets.
|
|
||||||
val latestStructure = request.fillContexts.last().structure
|
|
||||||
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)) {
|
|
||||||
val searchInfo = SearchInfo().apply {
|
|
||||||
applicationId = parseResult.applicationId
|
|
||||||
webDomain = parseResult.webDomain
|
|
||||||
webScheme = parseResult.webScheme
|
|
||||||
}
|
|
||||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
|
||||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
|
||||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
|
||||||
CompatInlineSuggestionsRequest(request)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
launchSelection(mDatabase,
|
|
||||||
searchInfo,
|
|
||||||
parseResult,
|
|
||||||
inlineSuggestionsRequest,
|
|
||||||
callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchSelection(database: ContextualDatabase?,
|
|
||||||
searchInfo: SearchInfo,
|
|
||||||
parseResult: StructureParser.Result,
|
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
|
||||||
callback: FillCallback) {
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
database,
|
|
||||||
searchInfo,
|
|
||||||
{ openedDatabase, items ->
|
|
||||||
callback.onSuccess(
|
|
||||||
AutofillHelper.buildResponse(this, openedDatabase,
|
|
||||||
items, parseResult, inlineSuggestionsRequest)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{ openedDatabase ->
|
|
||||||
// Show UI if no search result
|
|
||||||
showUIForEntrySelection(parseResult, openedDatabase,
|
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Show UI if database not open
|
|
||||||
showUIForEntrySelection(parseResult, null,
|
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
|
||||||
database: ContextualDatabase?,
|
|
||||||
searchInfo: SearchInfo,
|
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
|
||||||
callback: FillCallback) {
|
|
||||||
var success = false
|
|
||||||
parseResult.allAutofillIds().let { autofillIds ->
|
|
||||||
if (autofillIds.isNotEmpty()) {
|
|
||||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
|
||||||
// to generate Response.
|
|
||||||
val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
|
|
||||||
searchInfo, inlineSuggestionsRequest).intentSender
|
|
||||||
val responseBuilder = FillResponse.Builder()
|
|
||||||
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
|
||||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
|
||||||
RemoteViews(
|
|
||||||
packageName,
|
|
||||||
R.layout.item_autofill_unlock_web_domain
|
|
||||||
).apply {
|
|
||||||
setTextViewText(
|
|
||||||
R.id.autofill_web_domain_text,
|
|
||||||
parseResult.webDomain
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
|
||||||
setTextViewText(
|
|
||||||
R.id.autofill_app_id_text,
|
|
||||||
parseResult.applicationId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
|
||||||
RemoteViews(
|
|
||||||
packageName,
|
|
||||||
R.layout.item_autofill_select_entry_web_domain
|
|
||||||
).apply {
|
|
||||||
setTextViewText(
|
|
||||||
R.id.autofill_web_domain_text,
|
|
||||||
parseResult.webDomain
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
|
||||||
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
|
|
||||||
setTextViewText(
|
|
||||||
R.id.autofill_app_id_text,
|
|
||||||
parseResult.applicationId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RemoteViews(packageName, R.layout.item_autofill_select_entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell the autofill framework the interest to save credentials
|
|
||||||
if (askToSaveData) {
|
|
||||||
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
|
||||||
val requiredIds = ArrayList<AutofillId>()
|
|
||||||
val optionalIds = ArrayList<AutofillId>()
|
|
||||||
|
|
||||||
// Only if at least a password
|
|
||||||
parseResult.passwordId?.let { passwordInfo ->
|
|
||||||
parseResult.usernameId?.let { usernameInfo ->
|
|
||||||
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
|
||||||
requiredIds.add(usernameInfo)
|
|
||||||
}
|
|
||||||
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
|
||||||
requiredIds.add(passwordInfo)
|
|
||||||
}
|
|
||||||
// or a credit card form
|
|
||||||
if (requiredIds.isEmpty()) {
|
|
||||||
parseResult.creditCardNumberId?.let { numberId ->
|
|
||||||
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
|
|
||||||
requiredIds.add(numberId)
|
|
||||||
Log.d(TAG, "Asking to save credit card number")
|
|
||||||
}
|
|
||||||
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
|
|
||||||
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
|
|
||||||
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
|
|
||||||
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
|
|
||||||
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
|
|
||||||
}
|
|
||||||
if (requiredIds.isNotEmpty()) {
|
|
||||||
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
|
|
||||||
if (optionalIds.isNotEmpty()) {
|
|
||||||
builder.setOptionalIds(optionalIds.toTypedArray())
|
|
||||||
}
|
|
||||||
responseBuilder.setSaveInfo(builder.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build inline presentation
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
|
||||||
var inlinePresentation: InlinePresentation? = null
|
|
||||||
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
|
||||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
|
||||||
&& inlinePresentationSpecs.size > 0) {
|
|
||||||
val inlinePresentationSpec = inlinePresentationSpecs[0]
|
|
||||||
|
|
||||||
// Make sure that the IME spec claims support for v1 UI template.
|
|
||||||
val imeStyle = inlinePresentationSpec.style
|
|
||||||
if (UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) {
|
|
||||||
// Build the content for IME UI
|
|
||||||
inlinePresentation = InlinePresentation(
|
|
||||||
InlineSuggestionUi.newContentBuilder(
|
|
||||||
PendingIntent.getActivity(this,
|
|
||||||
0,
|
|
||||||
Intent(this, AutofillSettingsActivity::class.java),
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
})
|
|
||||||
).apply {
|
|
||||||
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
|
||||||
setTitle(getString(R.string.autofill_sign_in_prompt))
|
|
||||||
setStartIcon(Icon.createWithResource(this@KeeAutofillService, R.mipmap.ic_launcher_round).apply {
|
|
||||||
setTintBlendMode(BlendMode.DST)
|
|
||||||
})
|
|
||||||
}.build().slice, inlinePresentationSpec, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build response
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
try {
|
|
||||||
// Buggy method on some API 33 devices
|
|
||||||
responseBuilder.setAuthentication(
|
|
||||||
autofillIds,
|
|
||||||
intentSender,
|
|
||||||
Presentations.Builder().apply {
|
|
||||||
inlinePresentation?.let {
|
|
||||||
setInlinePresentation(it)
|
|
||||||
}
|
|
||||||
setDialogPresentation(remoteViewsUnlock)
|
|
||||||
}.build()
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to use the new setAuthentication method.", e)
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
|
||||||
}
|
|
||||||
success = true
|
|
||||||
callback.onSuccess(responseBuilder.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!success)
|
|
||||||
callback.onFailure("Unable to get Autofill ids for UI selection")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
|
||||||
var success = false
|
|
||||||
if (askToSaveData) {
|
|
||||||
val latestStructure = request.fillContexts.last().structure
|
|
||||||
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
|
||||||
|
|
||||||
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
|
||||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
|
||||||
Log.d(TAG, "autofill onSaveRequest password")
|
|
||||||
|
|
||||||
// Build expiration from date or from year and month
|
|
||||||
var expiration: DateTime? = parseResult.creditCardExpirationValue
|
|
||||||
if (parseResult.creditCardExpirationValue == null
|
|
||||||
&& parseResult.creditCardExpirationYearValue != 0
|
|
||||||
&& parseResult.creditCardExpirationMonthValue != 0) {
|
|
||||||
expiration = DateTime()
|
|
||||||
.withYear(parseResult.creditCardExpirationYearValue)
|
|
||||||
.withMonthOfYear(parseResult.creditCardExpirationMonthValue)
|
|
||||||
if (parseResult.creditCardExpirationDayValue != 0) {
|
|
||||||
expiration = expiration.withDayOfMonth(parseResult.creditCardExpirationDayValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show UI to save data
|
|
||||||
val registerInfo = RegisterInfo(
|
|
||||||
SearchInfo().apply {
|
|
||||||
applicationId = parseResult.applicationId
|
|
||||||
webDomain = parseResult.webDomain
|
|
||||||
webScheme = parseResult.webScheme
|
|
||||||
},
|
|
||||||
parseResult.usernameValue?.textValue?.toString(),
|
|
||||||
parseResult.passwordValue?.textValue?.toString(),
|
|
||||||
CreditCard(
|
|
||||||
parseResult.creditCardHolder,
|
|
||||||
parseResult.creditCardNumber,
|
|
||||||
expiration,
|
|
||||||
parseResult.cardVerificationValue
|
|
||||||
))
|
|
||||||
|
|
||||||
// TODO Callback in each activity #765
|
|
||||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
|
||||||
// registerInfo))
|
|
||||||
//} else {
|
|
||||||
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
|
||||||
success = true
|
|
||||||
callback.onSuccess()
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!success) {
|
|
||||||
callback.onFailure("Saving form values is not allowed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConnected() {
|
|
||||||
Log.d(TAG, "onConnected")
|
|
||||||
getPreferences()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDisconnected() {
|
|
||||||
Log.d(TAG, "onDisconnected")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG = KeeAutofillService::class.java.name
|
|
||||||
|
|
||||||
fun autofillAllowedFor(element: String?, blockList: Set<String>?): Boolean {
|
|
||||||
element?.let { elementNotNull ->
|
|
||||||
if (blockList?.any { appIdBlocked ->
|
|
||||||
elementNotNull.contains(appIdBlocked)
|
|
||||||
} == true
|
|
||||||
) {
|
|
||||||
Log.d(TAG, "Autofill not allowed for $elementNotNull")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,677 +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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
||||||
|
}
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.getEnum
|
||||||
|
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableList
|
||||||
|
import com.kunzisoft.keepass.utils.putEnum
|
||||||
|
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||||
|
import com.kunzisoft.keepass.utils.putParcelableList
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
object EntrySelectionHelper {
|
||||||
|
|
||||||
|
private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
|
||||||
|
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
||||||
|
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||||
|
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||||
|
private const val EXTRA_NODES_IDS = "com.kunzisoft.keepass.extra.NODES_IDS"
|
||||||
|
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.NODE_ID"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finish the activity by passing the result code and by locking the database if necessary
|
||||||
|
*/
|
||||||
|
fun Activity.setActivityResult(
|
||||||
|
lockDatabase: Boolean = false,
|
||||||
|
resultCode: Int,
|
||||||
|
data: Intent? = null
|
||||||
|
) {
|
||||||
|
when (resultCode) {
|
||||||
|
Activity.RESULT_OK ->
|
||||||
|
this.setResult(resultCode, data)
|
||||||
|
Activity.RESULT_CANCELED ->
|
||||||
|
this.setResult(resultCode)
|
||||||
|
}
|
||||||
|
this.finish()
|
||||||
|
|
||||||
|
if (lockDatabase) {
|
||||||
|
// Close the database
|
||||||
|
this.sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startActivityForSearchModeResult(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent,
|
||||||
|
searchInfo: SearchInfo
|
||||||
|
) {
|
||||||
|
intent.addSpecialMode(SpecialMode.SEARCH)
|
||||||
|
intent.addSearchInfo(searchInfo)
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startActivityForSelectionModeResult(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent,
|
||||||
|
typeMode: TypeMode,
|
||||||
|
searchInfo: SearchInfo?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||||
|
) {
|
||||||
|
intent.addSpecialMode(SpecialMode.SELECTION)
|
||||||
|
intent.addTypeMode(typeMode)
|
||||||
|
intent.addSearchInfo(searchInfo)
|
||||||
|
if (activityResultLauncher == null) {
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startActivityForRegistrationModeResult(
|
||||||
|
context: Context,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
intent: Intent,
|
||||||
|
registerInfo: RegisterInfo?,
|
||||||
|
typeMode: TypeMode
|
||||||
|
) {
|
||||||
|
intent.addSpecialMode(SpecialMode.REGISTRATION)
|
||||||
|
intent.addTypeMode(typeMode)
|
||||||
|
intent.addRegisterInfo(registerInfo)
|
||||||
|
if (activityResultLauncher == null) {
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
activityResultLauncher?.launch(intent) ?: context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the special mode response for internal entry selection for one entry
|
||||||
|
*/
|
||||||
|
fun Activity.buildSpecialModeResponseAndSetResult(
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
this.buildSpecialModeResponseAndSetResult(listOf(entryInfo), extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the special mode response for internal entry selection for multiple entries
|
||||||
|
*/
|
||||||
|
fun Activity.buildSpecialModeResponseAndSetResult(
|
||||||
|
entriesInfo: List<EntryInfo>,
|
||||||
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(javaClass.name, "Success special mode manual selection")
|
||||||
|
mReplyIntent.addNodesIds(entriesInfo.map { it.id })
|
||||||
|
extras?.let {
|
||||||
|
mReplyIntent.putExtras(it)
|
||||||
|
}
|
||||||
|
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.name, "Unable to add the result", e)
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addSearchInfo(searchInfo: SearchInfo?): Intent {
|
||||||
|
searchInfo?.let {
|
||||||
|
putExtra(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.addSearchInfo(searchInfo: SearchInfo?): Bundle {
|
||||||
|
searchInfo?.let {
|
||||||
|
putParcelable(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
||||||
|
return getParcelableExtraCompat(KEY_SEARCH_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.getSearchInfo(): SearchInfo? {
|
||||||
|
return getParcelableCompat(KEY_SEARCH_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addRegisterInfo(registerInfo: RegisterInfo?): Intent {
|
||||||
|
registerInfo?.let {
|
||||||
|
putExtra(KEY_REGISTER_INFO, it)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.addRegisterInfo(registerInfo: RegisterInfo?): Bundle {
|
||||||
|
registerInfo?.let {
|
||||||
|
putParcelable(KEY_REGISTER_INFO, it)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrieveRegisterInfo(): RegisterInfo? {
|
||||||
|
return getParcelableExtraCompat(KEY_REGISTER_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.getRegisterInfo(): RegisterInfo? {
|
||||||
|
return getParcelableCompat(KEY_REGISTER_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.removeInfo() {
|
||||||
|
removeExtra(KEY_SEARCH_INFO)
|
||||||
|
removeExtra(KEY_REGISTER_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
|
||||||
|
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.addSpecialMode(specialMode: SpecialMode): Bundle {
|
||||||
|
this.putEnum(KEY_SPECIAL_MODE, specialMode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrieveSpecialMode(): SpecialMode {
|
||||||
|
return this.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.getSpecialMode(): SpecialMode {
|
||||||
|
return this.getEnum<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
|
||||||
|
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrieveTypeMode(): TypeMode {
|
||||||
|
return getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.removeModes() {
|
||||||
|
removeExtra(KEY_SPECIAL_MODE)
|
||||||
|
removeExtra(KEY_TYPE_MODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addNodesIds(nodesIds: List<UUID>): Intent {
|
||||||
|
this.putParcelableList(EXTRA_NODES_IDS, nodesIds.map { ParcelUuid(it) })
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrieveNodesIds(): List<UUID>? {
|
||||||
|
return getParcelableList<ParcelUuid>(EXTRA_NODES_IDS)?.map { it.uuid }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.removeNodesIds() {
|
||||||
|
removeExtra(EXTRA_NODES_IDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the node id to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addNodeId(nodeId: UUID?) {
|
||||||
|
nodeId?.let {
|
||||||
|
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the node id from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveNodeId(): UUID? {
|
||||||
|
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.removeNodeId() {
|
||||||
|
removeExtra(EXTRA_NODE_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve nodes ids from intent and get the corresponding entry info list in [database]
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveAndRemoveEntries(database: ContextualDatabase): List<EntryInfo> {
|
||||||
|
val nodesIds = retrieveNodesIds()
|
||||||
|
?: throw IOException("NodesIds is null")
|
||||||
|
removeNodesIds()
|
||||||
|
return nodesIds.mapNotNull { nodeId ->
|
||||||
|
database
|
||||||
|
.getEntryById(NodeIdUUID(nodeId))
|
||||||
|
?.getEntryInfo(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intent sender uses special retains data in callback
|
||||||
|
*/
|
||||||
|
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
|
||||||
|
return (specialMode == SpecialMode.SELECTION
|
||||||
|
&& (typeMode == TypeMode.MAGIKEYBOARD || typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||||
|
|| (specialMode == SpecialMode.REGISTRATION
|
||||||
|
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doSpecialAction(
|
||||||
|
intent: Intent,
|
||||||
|
defaultAction: () -> Unit,
|
||||||
|
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
|
selectionAction: (
|
||||||
|
intentSenderMode: Boolean,
|
||||||
|
typeMode: TypeMode,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) -> Unit,
|
||||||
|
registrationAction: (
|
||||||
|
intentSenderMode: Boolean,
|
||||||
|
typeMode: TypeMode,
|
||||||
|
registerInfo: RegisterInfo?
|
||||||
|
) -> Unit
|
||||||
|
) {
|
||||||
|
when (val specialMode = intent.retrieveSpecialMode()) {
|
||||||
|
SpecialMode.DEFAULT -> {
|
||||||
|
intent.removeModes()
|
||||||
|
intent.removeInfo()
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
SpecialMode.SEARCH -> {
|
||||||
|
val searchInfo = intent.retrieveSearchInfo()
|
||||||
|
intent.removeModes()
|
||||||
|
intent.removeInfo()
|
||||||
|
if (searchInfo != null)
|
||||||
|
searchAction.invoke(searchInfo)
|
||||||
|
else {
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialMode.SELECTION -> {
|
||||||
|
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
|
||||||
|
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
||||||
|
when (val typeMode = intent.retrieveTypeMode()) {
|
||||||
|
TypeMode.DEFAULT -> {
|
||||||
|
intent.removeModes()
|
||||||
|
if (searchInfo != null)
|
||||||
|
searchAction.invoke(searchInfo)
|
||||||
|
else
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
TypeMode.MAGIKEYBOARD -> selectionAction.invoke(
|
||||||
|
isIntentSenderMode(specialMode, typeMode),
|
||||||
|
typeMode,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
TypeMode.PASSKEY ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
selectionAction.invoke(
|
||||||
|
isIntentSenderMode(specialMode, typeMode),
|
||||||
|
typeMode,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
} else
|
||||||
|
defaultAction.invoke()
|
||||||
|
TypeMode.AUTOFILL -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
selectionAction.invoke(
|
||||||
|
isIntentSenderMode(specialMode, typeMode),
|
||||||
|
typeMode,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
} else
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (searchInfo != null)
|
||||||
|
searchAction.invoke(searchInfo)
|
||||||
|
else
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialMode.REGISTRATION -> {
|
||||||
|
val registerInfo: RegisterInfo? = intent.retrieveRegisterInfo()
|
||||||
|
val typeMode = intent.retrieveTypeMode()
|
||||||
|
val intentSenderMode = isIntentSenderMode(specialMode, typeMode)
|
||||||
|
if (!intentSenderMode) {
|
||||||
|
intent.removeModes()
|
||||||
|
intent.removeInfo()
|
||||||
|
}
|
||||||
|
if (registerInfo != null)
|
||||||
|
registrationAction.invoke(
|
||||||
|
intentSenderMode,
|
||||||
|
typeMode,
|
||||||
|
registerInfo
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performSelection(items: List<EntryInfo>,
|
||||||
|
actionPopulateCredentialProvider: (entryInfo: EntryInfo) -> Unit,
|
||||||
|
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||||
|
if (items.size == 1) {
|
||||||
|
val itemFound = items[0]
|
||||||
|
actionPopulateCredentialProvider.invoke(itemFound)
|
||||||
|
} else if (items.size > 1) {
|
||||||
|
// Select the one we want in the selection
|
||||||
|
actionEntrySelection.invoke(true)
|
||||||
|
} else {
|
||||||
|
// Select an arbitrary one
|
||||||
|
actionEntrySelection.invoke(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to assign a drawable to a new icon from a database icon
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun EntryInfo.buildIcon(
|
||||||
|
context: Context,
|
||||||
|
database: ContextualDatabase
|
||||||
|
): Icon? {
|
||||||
|
try {
|
||||||
|
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
|
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
|
return IconCompat.createWithBitmap(bitmap).toIcon(context)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
enum class SpecialMode {
|
enum class SpecialMode {
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
SEARCH,
|
SEARCH,
|
||||||
SAVE,
|
|
||||||
SELECTION,
|
SELECTION,
|
||||||
REGISTRATION;
|
REGISTRATION;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
|
enum class TypeMode {
|
||||||
|
DEFAULT, MAGIKEYBOARD, PASSKEY, AUTOFILL
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addRegisterInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getRegisterInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.getSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.retrieveAutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.AutofillLauncherViewModel
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
|
private val autofillLauncherViewModel: AutofillLauncherViewModel by viewModels()
|
||||||
|
|
||||||
|
private var mAutofillSelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
autofillLauncherViewModel.manageSelectionResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mAutofillRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
autofillLauncherViewModel.manageRegistrationResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyCustomStyle(): Boolean = false
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean = true
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// To apply the bypass https://github.com/Kunzisoft/KeePassDX/issues/2238
|
||||||
|
// before managing intent in super class
|
||||||
|
intent.retrieveSelectionBundle()?.apply {
|
||||||
|
intent.addSpecialMode(getSpecialMode())
|
||||||
|
intent.addSearchInfo(getSearchInfo())
|
||||||
|
intent.addRegisterInfo(getRegisterInfo())
|
||||||
|
intent.addAutofillComponent(retrieveAutofillComponent())
|
||||||
|
}
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
autofillLauncherViewModel.initialize()
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// Initialize the parameters
|
||||||
|
autofillLauncherViewModel.uiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
AutofillLauncherViewModel.UIState.Loading -> {}
|
||||||
|
is AutofillLauncherViewModel.UIState.ShowBlockRestartMessage -> {
|
||||||
|
showBlockRestartMessage()
|
||||||
|
autofillLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
is AutofillLauncherViewModel.UIState.ShowAutofillSuggestionMessage -> {
|
||||||
|
showAutofillSuggestionMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// Retrieve the UI
|
||||||
|
autofillLauncherViewModel.credentialUiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is CredentialLauncherViewModel.CredentialState.Loading -> {}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
|
||||||
|
GroupActivity.launchForSelection(
|
||||||
|
context = this@AutofillLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mAutofillSelectionActivityResultLauncher,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this@AutofillLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mAutofillRegistrationActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||||
|
FileDatabaseSelectActivity.launchForSelection(
|
||||||
|
context = this@AutofillLauncherActivity,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mAutofillSelectionActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
|
context = this@AutofillLauncherActivity,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mAutofillRegistrationActivityResultLauncher,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = uiState.lockDatabase,
|
||||||
|
resultCode = uiState.resultCode,
|
||||||
|
data = uiState.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.ShowError -> {
|
||||||
|
toastError(uiState.error)
|
||||||
|
autofillLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onUnknownDatabaseRetrieved(database)
|
||||||
|
autofillLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showBlockRestartMessage() {
|
||||||
|
// If item not allowed, show a toast
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.autofill_block_restart,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAutofillSuggestionMessage() {
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.autofill_inline_suggestions_keyboard,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_PENDING_INTENT_BUNDLE = "com.kunzisoft.keepass.extra.BUNDLE"
|
||||||
|
private val TAG = AutofillLauncherActivity::class.java.name
|
||||||
|
|
||||||
|
fun Intent.retrieveSelectionBundle(): Bundle? {
|
||||||
|
return this.getBundleExtra(KEY_PENDING_INTENT_BUNDLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPendingIntentForSelection(
|
||||||
|
context: Context,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
|
autofillComponent: AutofillComponent
|
||||||
|
): PendingIntent? {
|
||||||
|
try {
|
||||||
|
// Doesn't work with direct extra Parcelable in Android 11 (don't know why?)
|
||||||
|
// https://github.com/Kunzisoft/KeePassDX/issues/2238
|
||||||
|
// Wrap into a bundle to bypass the problem
|
||||||
|
val tempBundle = Bundle().apply {
|
||||||
|
addSpecialMode(SpecialMode.SELECTION)
|
||||||
|
addSearchInfo(searchInfo)
|
||||||
|
addAutofillComponent(autofillComponent)
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
randomRequestCode(),
|
||||||
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
|
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
|
||||||
|
},
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
Log.e(TAG, "Unable to create pending intent for selection", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPendingIntentForRegistration(
|
||||||
|
context: Context,
|
||||||
|
registerInfo: RegisterInfo
|
||||||
|
): PendingIntent? {
|
||||||
|
try {
|
||||||
|
// Bypass intent issue
|
||||||
|
val tempBundle = Bundle().apply {
|
||||||
|
addSpecialMode(SpecialMode.REGISTRATION)
|
||||||
|
addRegisterInfo(registerInfo)
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
randomRequestCode(),
|
||||||
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
|
putExtra(KEY_PENDING_INTENT_BUNDLE, tempBundle)
|
||||||
|
},
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
Log.e(TAG, "Unable to create pending intent for registration", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.EntrySelectionViewModel
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to search or select entry in database,
|
||||||
|
* Commonly used with Magikeyboard
|
||||||
|
*/
|
||||||
|
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
|
private val entrySelectionViewModel: EntrySelectionViewModel by viewModels()
|
||||||
|
|
||||||
|
private var mEntrySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
entrySelectionViewModel.manageSelectionResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyCustomStyle() = false
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested() = false
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
entrySelectionViewModel.initialize()
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// Initialize the parameters
|
||||||
|
entrySelectionViewModel.uiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is EntrySelectionViewModel.UIState.Loading -> {}
|
||||||
|
is EntrySelectionViewModel.UIState.PopulateKeyboard -> {
|
||||||
|
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(
|
||||||
|
context = this@EntrySelectionLauncherActivity,
|
||||||
|
entry = uiState.entryInfo,
|
||||||
|
toast = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is EntrySelectionViewModel.UIState.LaunchFileDatabaseSelectForSearch -> {
|
||||||
|
FileDatabaseSelectActivity.launchForSearch(
|
||||||
|
context = this@EntrySelectionLauncherActivity,
|
||||||
|
searchInfo = uiState.searchInfo
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
is EntrySelectionViewModel.UIState.LaunchGroupActivityForSearch -> {
|
||||||
|
GroupActivity.launchForSearch(
|
||||||
|
context = this@EntrySelectionLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
searchInfo = uiState.searchInfo
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// Retrieve the UI
|
||||||
|
entrySelectionViewModel.credentialUiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is CredentialLauncherViewModel.CredentialState.Loading -> {}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
|
||||||
|
GroupActivity.launchForSelection(
|
||||||
|
context = this@EntrySelectionLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mEntrySelectionActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this@EntrySelectionLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = null // Null to not get any callback
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||||
|
FileDatabaseSelectActivity.launchForSelection(
|
||||||
|
context = this@EntrySelectionLauncherActivity,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = mEntrySelectionActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
|
context = this@EntrySelectionLauncherActivity,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
activityResultLauncher = null // Null to not get any callback
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = uiState.lockDatabase,
|
||||||
|
resultCode = uiState.resultCode,
|
||||||
|
data = uiState.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.ShowError -> {
|
||||||
|
toastError(uiState.error)
|
||||||
|
entrySelectionViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onUnknownDatabaseRetrieved(database)
|
||||||
|
entrySelectionViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun launch(
|
||||||
|
context: Context,
|
||||||
|
searchInfo: SearchInfo? = null
|
||||||
|
) {
|
||||||
|
context.startActivity(Intent(
|
||||||
|
context,
|
||||||
|
EntrySelectionLauncherActivity::class.java
|
||||||
|
).apply {
|
||||||
|
addSearchInfo(searchInfo)
|
||||||
|
// New task needed because don't launch from an Activity context
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addHardwareKey
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addSeed
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.buildHardwareKeyChallenge
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.isYubikeyDriverAvailable
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.UIState
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
|
||||||
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special activity to deal with hardware key drivers,
|
||||||
|
* return the response to the database service once finished
|
||||||
|
*/
|
||||||
|
class HardwareKeyActivity: DatabaseModeActivity(){
|
||||||
|
|
||||||
|
private val mHardwareKeyLauncherViewModel: HardwareKeyLauncherViewModel by viewModels()
|
||||||
|
|
||||||
|
private var activityResultLauncher: ActivityResultLauncher<Intent> =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
mHardwareKeyLauncherViewModel.manageSelectionResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyCustomStyle(): Boolean = false
|
||||||
|
|
||||||
|
override fun showDatabaseDialog(): Boolean = false
|
||||||
|
|
||||||
|
override fun manageDatabaseInfo(): Boolean = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
mHardwareKeyLauncherViewModel.uiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is UIState.Loading -> {}
|
||||||
|
is UIState.ShowHardwareKeyDriverNeeded -> {
|
||||||
|
showHardwareKeyDriverNeeded(
|
||||||
|
this@HardwareKeyActivity,
|
||||||
|
uiState.hardwareKey
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.onChallengeResponded(null)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is UIState.LaunchChallengeActivityForResponse -> {
|
||||||
|
// Send to the driver
|
||||||
|
activityResultLauncher.launch(
|
||||||
|
buildHardwareKeyChallenge(uiState.challenge)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is UIState.OnChallengeResponded -> {
|
||||||
|
mDatabaseViewModel.onChallengeResponded(uiState.response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
mHardwareKeyLauncherViewModel.credentialUiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = uiState.lockDatabase,
|
||||||
|
resultCode = uiState.resultCode,
|
||||||
|
data = uiState.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.ShowError -> {
|
||||||
|
toastError(uiState.error)
|
||||||
|
mHardwareKeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
mHardwareKeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showHardwareKeyDriverNeeded(
|
||||||
|
context: Context,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
onDialogDismissed: DialogInterface.OnDismissListener
|
||||||
|
) {
|
||||||
|
val builder = AlertDialog.Builder(context)
|
||||||
|
builder
|
||||||
|
.setMessage(
|
||||||
|
context.getString(R.string.error_driver_required, hardwareKey.toString())
|
||||||
|
)
|
||||||
|
.setPositiveButton(R.string.download) { _, _ ->
|
||||||
|
context.openExternalApp(
|
||||||
|
context.getString(R.string.key_driver_app_id),
|
||||||
|
context.getString(R.string.key_driver_url)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
.setOnDismissListener(onDialogDismissed)
|
||||||
|
builder.create().show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = HardwareKeyActivity::class.java.simpleName
|
||||||
|
|
||||||
|
fun launchHardwareKeyActivity(
|
||||||
|
context: Context,
|
||||||
|
hardwareKey: HardwareKey,
|
||||||
|
seed: ByteArray?
|
||||||
|
) {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(
|
||||||
|
context,
|
||||||
|
HardwareKeyActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
|
||||||
|
addHardwareKey(hardwareKey)
|
||||||
|
addSeed(seed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isHardwareKeyAvailable(
|
||||||
|
context: Context,
|
||||||
|
hardwareKey: HardwareKey?
|
||||||
|
): Boolean {
|
||||||
|
if (hardwareKey == null)
|
||||||
|
return false
|
||||||
|
return when (hardwareKey) {
|
||||||
|
/*
|
||||||
|
HardwareKey.FIDO2_SECRET -> {
|
||||||
|
// TODO FIDO2 under development
|
||||||
|
false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
|
||||||
|
// Check available intent
|
||||||
|
isYubikeyDriverAvailable(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.activity
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels()
|
||||||
|
|
||||||
|
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
passkeyLauncherViewModel.manageSelectionResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mPasskeyRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
passkeyLauncherViewModel.manageRegistrationResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyCustomStyle(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// Initialize the parameters
|
||||||
|
passkeyLauncherViewModel.initialize()
|
||||||
|
// Retrieve the UI
|
||||||
|
passkeyLauncherViewModel.uiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is PasskeyLauncherViewModel.UIState.Loading -> {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.ShowAppPrivilegedDialog -> {
|
||||||
|
showAppPrivilegedDialog(
|
||||||
|
temptingApp = uiState.temptingApp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.ShowAppSignatureDialog -> {
|
||||||
|
showAppSignatureDialog(
|
||||||
|
temptingApp = uiState.temptingApp,
|
||||||
|
nodeId = uiState.nodeId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
|
||||||
|
updateEntry(uiState.oldEntry, uiState.newEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
passkeyLauncherViewModel.credentialUiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is CredentialLauncherViewModel.CredentialState.Loading -> {}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.SetActivityResult -> {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = uiState.lockDatabase,
|
||||||
|
resultCode = uiState.resultCode,
|
||||||
|
data = uiState.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.ShowError -> {
|
||||||
|
toastError(uiState.error)
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForSelection -> {
|
||||||
|
GroupActivity.launchForSelection(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
activityResultLauncher = mPasskeySelectionActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchGroupActivityForRegistration -> {
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||||
|
FileDatabaseSelectActivity.launchForSelection(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
activityResultLauncher = mPasskeySelectionActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CredentialLauncherViewModel.CredentialState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
typeMode = uiState.typeMode,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onUnknownDatabaseRetrieved(database)
|
||||||
|
passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
|
// TODO When auto save is enabled, WARNING filter by the calling activity
|
||||||
|
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a dialog that asks the user to add an app to the list of privileged apps.
|
||||||
|
*/
|
||||||
|
private fun showAppPrivilegedDialog(
|
||||||
|
temptingApp: AndroidPrivilegedApp
|
||||||
|
) {
|
||||||
|
Log.w(javaClass.simpleName, "No privileged apps file found")
|
||||||
|
AlertDialog.Builder(this@PasskeyLauncherActivity).apply {
|
||||||
|
setTitle(getString(R.string.passkeys_privileged_apps_ask_title))
|
||||||
|
setMessage(StringBuilder()
|
||||||
|
.append(
|
||||||
|
getString(
|
||||||
|
R.string.passkeys_privileged_apps_ask_message,
|
||||||
|
temptingApp.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.append("\n\n")
|
||||||
|
.append(getString(R.string.passkeys_privileged_apps_explanation))
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
passkeyLauncherViewModel.saveCustomPrivilegedApp(
|
||||||
|
intent = intent,
|
||||||
|
specialMode = mSpecialMode,
|
||||||
|
database = mDatabase,
|
||||||
|
temptingApp = temptingApp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
setOnCancelListener {
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
}.create().show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a dialog that asks the user to add an app signature in an existing passkey
|
||||||
|
*/
|
||||||
|
private fun showAppSignatureDialog(
|
||||||
|
temptingApp: AppOrigin,
|
||||||
|
nodeId: UUID
|
||||||
|
) {
|
||||||
|
AlertDialog.Builder(this@PasskeyLauncherActivity).apply {
|
||||||
|
setTitle(getString(R.string.passkeys_missing_signature_app_ask_title))
|
||||||
|
setMessage(StringBuilder()
|
||||||
|
.append(
|
||||||
|
getString(
|
||||||
|
R.string.passkeys_missing_signature_app_ask_message,
|
||||||
|
temptingApp.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.append("\n\n")
|
||||||
|
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
|
||||||
|
.append("\n\n")
|
||||||
|
.append(getString(R.string.passkeys_missing_signature_app_ask_question))
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
passkeyLauncherViewModel.saveAppSignature(
|
||||||
|
database = mDatabase,
|
||||||
|
temptingApp = temptingApp,
|
||||||
|
nodeId = nodeId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
setOnCancelListener {
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
}.create().show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PasskeyLauncherActivity::class.java.name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a pending intent to launch the passkey launcher activity
|
||||||
|
* [nodeId] can be :
|
||||||
|
* - null if manual selection is requested
|
||||||
|
* - null if manual registration is requested
|
||||||
|
* - an entry node id if direct selection is requested
|
||||||
|
* - a group node id if direct registration is requested in a default group
|
||||||
|
* - an entry node id if overwriting is requested in an existing entry
|
||||||
|
*/
|
||||||
|
fun getPendingIntent(
|
||||||
|
context: Context,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
|
appOrigin: AppOrigin? = null,
|
||||||
|
nodeId: UUID? = null
|
||||||
|
): PendingIntent? {
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
randomRequestCode(),
|
||||||
|
Intent(context, PasskeyLauncherActivity::class.java).apply {
|
||||||
|
addSpecialMode(specialMode)
|
||||||
|
addTypeMode(TypeMode.PASSKEY)
|
||||||
|
addSearchInfo(searchInfo)
|
||||||
|
addAppOrigin(appOrigin)
|
||||||
|
addNodeId(nodeId)
|
||||||
|
addAuthCode(nodeId)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
|
import android.app.assist.AssistStructure
|
||||||
|
|
||||||
|
data class AutofillComponent(
|
||||||
|
val assistStructure: AssistStructure,
|
||||||
|
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?
|
||||||
|
)
|
||||||
@@ -17,10 +17,9 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -28,6 +27,7 @@ import android.content.Intent
|
|||||||
import android.graphics.BlendMode
|
import android.graphics.BlendMode
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import android.service.autofill.Dataset
|
import android.service.autofill.Dataset
|
||||||
import android.service.autofill.Field
|
import android.service.autofill.Field
|
||||||
import android.service.autofill.FillResponse
|
import android.service.autofill.FillResponse
|
||||||
@@ -38,19 +38,14 @@ import android.view.autofill.AutofillId
|
|||||||
import android.view.autofill.AutofillManager
|
import android.view.autofill.AutofillManager
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import android.widget.Toast
|
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
@@ -58,21 +53,63 @@ import com.kunzisoft.keepass.model.EntryInfo
|
|||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
object AutofillHelper {
|
object AutofillHelper {
|
||||||
|
|
||||||
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
private const val EXTRA_BASE_STRUCTURE = "com.kunzisoft.keepass.autofill.BASE_STRUCTURE"
|
||||||
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||||
|
|
||||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
fun Intent.addAutofillComponent(autofillComponent: AutofillComponent?): Intent {
|
||||||
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
autofillComponent?.let {
|
||||||
|
this.putExtra(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||||
|
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrieveAutofillComponent(): AutofillComponent? {
|
||||||
|
this.getParcelableExtraCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
AutofillComponent(assistStructure,
|
AutofillComponent(
|
||||||
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
assistStructure,
|
||||||
|
this.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||||
|
} else {
|
||||||
|
AutofillComponent(assistStructure, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.addAutofillComponent(autofillComponent: AutofillComponent?): Bundle {
|
||||||
|
autofillComponent?.let {
|
||||||
|
this.putParcelable(EXTRA_BASE_STRUCTURE, autofillComponent.assistStructure)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||||
|
this.putParcelable(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.retrieveAutofillComponent(): AutofillComponent? {
|
||||||
|
this.getParcelableCompat<AssistStructure>(EXTRA_BASE_STRUCTURE)?.let { assistStructure ->
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
AutofillComponent(
|
||||||
|
assistStructure,
|
||||||
|
this.getParcelableCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
AutofillComponent(assistStructure, null)
|
AutofillComponent(assistStructure, null)
|
||||||
}
|
}
|
||||||
@@ -131,11 +168,13 @@ object AutofillHelper {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDatasetForEntry(context: Context,
|
private fun buildDatasetForEntry(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entryInfo: EntryInfo,
|
entryInfo: EntryInfo,
|
||||||
struct: StructureParser.Result,
|
struct: StructureParser.Result,
|
||||||
inlinePresentation: InlinePresentation?): Dataset {
|
inlinePresentation: InlinePresentation?
|
||||||
|
): Dataset {
|
||||||
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
|
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
|
||||||
|
|
||||||
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
@@ -175,8 +214,8 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (entryInfo.expires) {
|
if (entryInfo.expires) {
|
||||||
val year = entryInfo.expiryTime.getYearInt()
|
val year = entryInfo.expiryTime.getYear()
|
||||||
val month = entryInfo.expiryTime.getMonthInt()
|
val month = entryInfo.expiryTime.getMonth()
|
||||||
val monthString = month.toString().padStart(2, '0')
|
val monthString = month.toString().padStart(2, '0')
|
||||||
val day = entryInfo.expiryTime.getDay()
|
val day = entryInfo.expiryTime.getDay()
|
||||||
val dayString = day.toString().padStart(2, '0')
|
val dayString = day.toString().padStart(2, '0')
|
||||||
@@ -191,7 +230,7 @@ object AutofillHelper {
|
|||||||
} else {
|
} else {
|
||||||
datasetBuilder.addValueToDatasetBuilder(
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
it,
|
it,
|
||||||
AutofillValue.forDate(entryInfo.expiryTime.date.time)
|
AutofillValue.forDate(entryInfo.expiryTime.toMilliseconds())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +301,7 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (field in entryInfo.customFields) {
|
for (field in entryInfo.getCustomFieldsForFilling()) {
|
||||||
if (field.name == TemplateField.LABEL_HOLDER) {
|
if (field.name == TemplateField.LABEL_HOLDER) {
|
||||||
struct.creditCardHolderId?.let { ccNameId ->
|
struct.creditCardHolderId?.let { ccNameId ->
|
||||||
datasetBuilder.addValueToDatasetBuilder(
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
@@ -293,38 +332,24 @@ object AutofillHelper {
|
|||||||
return dataset
|
return dataset
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to assign a drawable to a new icon from a database icon
|
|
||||||
*/
|
|
||||||
private fun buildIconFromEntry(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryInfo: EntryInfo): Icon? {
|
|
||||||
try {
|
|
||||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
|
||||||
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
|
||||||
return Icon.createWithBitmap(bitmap)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||||
positionItem: Int,
|
positionItem: Int,
|
||||||
entryInfo: EntryInfo): InlinePresentation? {
|
entryInfo: EntryInfo
|
||||||
|
): InlinePresentation? {
|
||||||
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||||
|
|
||||||
if (positionItem <= maxSuggestion - 1
|
if (positionItem <= maxSuggestion - 1) {
|
||||||
&& inlinePresentationSpecs.size > positionItem
|
|
||||||
) {
|
// If positionItem is larger than the number of specs in the list, then
|
||||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
// the last spec is used for the remainder of the suggestions
|
||||||
|
val inlinePresentationSpec = inlinePresentationSpecs[min(positionItem, inlinePresentationSpecs.size - 1)]
|
||||||
|
|
||||||
// Make sure that the IME spec claims support for v1 UI template.
|
// Make sure that the IME spec claims support for v1 UI template.
|
||||||
val imeStyle = inlinePresentationSpec.style
|
val imeStyle = inlinePresentationSpec.style
|
||||||
@@ -334,13 +359,9 @@ object AutofillHelper {
|
|||||||
// Build the content for IME UI
|
// Build the content for IME UI
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
0,
|
randomRequestCode(),
|
||||||
Intent(context, AutofillSettingsActivity::class.java),
|
Intent(context, AutofillSettingsActivity::class.java),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
return InlinePresentation(
|
return InlinePresentation(
|
||||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||||
@@ -351,7 +372,7 @@ object AutofillHelper {
|
|||||||
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
entryInfo.buildIcon(context, database)?.let { icon ->
|
||||||
setEndIcon(icon.apply {
|
setEndIcon(icon.apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
@@ -365,9 +386,11 @@ object AutofillHelper {
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun buildInlinePresentationForManualSelection(context: Context,
|
private fun buildInlinePresentationForManualSelection(
|
||||||
|
context: Context,
|
||||||
inlinePresentationSpec: InlinePresentationSpec,
|
inlinePresentationSpec: InlinePresentationSpec,
|
||||||
pendingIntent: PendingIntent): InlinePresentation? {
|
pendingIntent: PendingIntent
|
||||||
|
): InlinePresentation? {
|
||||||
// Make sure that the IME spec claims support for v1 UI template.
|
// Make sure that the IME spec claims support for v1 UI template.
|
||||||
val imeStyle = inlinePresentationSpec.style
|
val imeStyle = inlinePresentationSpec.style
|
||||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||||
@@ -384,11 +407,13 @@ object AutofillHelper {
|
|||||||
}.build().slice, inlinePresentationSpec, false)
|
}.build().slice, inlinePresentationSpec, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildResponse(context: Context,
|
fun buildResponse(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entriesInfo: List<EntryInfo>,
|
entriesInfo: List<EntryInfo>,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
autofillComponent: AutofillComponent
|
||||||
|
): FillResponse? {
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
// Add Header
|
// Add Header
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
@@ -409,7 +434,8 @@ object AutofillHelper {
|
|||||||
// Add inline suggestion for new IME and dataset
|
// Add inline suggestion for new IME and dataset
|
||||||
var numberInlineSuggestions = 0
|
var numberInlineSuggestions = 0
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
autofillComponent.compatInlineSuggestionsRequest
|
||||||
|
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||||
@@ -425,21 +451,27 @@ object AutofillHelper {
|
|||||||
var inlinePresentation: InlinePresentation? = null
|
var inlinePresentation: InlinePresentation? = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& numberInlineSuggestions > 0
|
&& numberInlineSuggestions > 0
|
||||||
&& compatInlineSuggestionsRequest != null) {
|
&& autofillComponent.compatInlineSuggestionsRequest != null) {
|
||||||
inlinePresentation = buildInlinePresentationForEntry(
|
inlinePresentation = buildInlinePresentationForEntry(
|
||||||
context,
|
context,
|
||||||
database,
|
database,
|
||||||
compatInlineSuggestionsRequest,
|
autofillComponent.compatInlineSuggestionsRequest,
|
||||||
numberInlineSuggestions--,
|
numberInlineSuggestions--,
|
||||||
entry
|
entry
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Create dataset for each entry
|
// Create dataset for each entry
|
||||||
responseBuilder.addDataset(
|
responseBuilder.addDataset(
|
||||||
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
|
buildDatasetForEntry(
|
||||||
|
context = context,
|
||||||
|
database = database,
|
||||||
|
entryInfo = entry,
|
||||||
|
struct = parseResult,
|
||||||
|
inlinePresentation = inlinePresentation
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to add dataset")
|
Log.e(TAG, "Unable to add dataset", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,15 +483,27 @@ object AutofillHelper {
|
|||||||
webScheme = parseResult.webScheme
|
webScheme = parseResult.webScheme
|
||||||
manualSelection = true
|
manualSelection = true
|
||||||
}
|
}
|
||||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
val manualSelectionView = RemoteViews(
|
||||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
context.packageName,
|
||||||
searchInfo, compatInlineSuggestionsRequest)
|
R.layout.item_autofill_select_entry
|
||||||
|
)
|
||||||
|
AutofillLauncherActivity.getPendingIntentForSelection(
|
||||||
|
context,
|
||||||
|
searchInfo,
|
||||||
|
autofillComponent
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
|
||||||
var inlinePresentation: InlinePresentation? = null
|
var inlinePresentation: InlinePresentation? = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
autofillComponent.compatInlineSuggestionsRequest
|
||||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
val inlinePresentationSpec =
|
||||||
|
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||||
|
inlinePresentation = buildInlinePresentationForManualSelection(
|
||||||
|
context,
|
||||||
|
inlinePresentationSpec,
|
||||||
|
pendingIntent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +538,7 @@ object AutofillHelper {
|
|||||||
responseBuilder.addDataset(dataset)
|
responseBuilder.addDataset(dataset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
responseBuilder.build()
|
responseBuilder.build()
|
||||||
@@ -504,92 +549,33 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the Autofill response for one entry
|
* Build the Autofill response
|
||||||
*/
|
*/
|
||||||
fun buildResponseAndSetResult(activity: Activity,
|
fun buildResponse(
|
||||||
database: ContextualDatabase,
|
context: Context,
|
||||||
entryInfo: EntryInfo) {
|
|
||||||
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the Autofill response for many entry
|
|
||||||
*/
|
|
||||||
fun buildResponseAndSetResult(activity: Activity,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entriesInfo: List<EntryInfo>) {
|
|
||||||
if (entriesInfo.isEmpty()) {
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
|
||||||
} else {
|
|
||||||
var setResultOk = false
|
|
||||||
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
|
||||||
StructureParser(structure).parse()?.let { result ->
|
|
||||||
// New Response
|
|
||||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
|
||||||
if (compatInlineSuggestionsRequest != null) {
|
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
|
|
||||||
} else {
|
|
||||||
buildResponse(activity, database, entriesInfo, result, null)
|
|
||||||
}
|
|
||||||
val mReplyIntent = Intent()
|
|
||||||
Log.d(activity.javaClass.name, "Success Autofill auth.")
|
|
||||||
mReplyIntent.putExtra(
|
|
||||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
|
||||||
response)
|
|
||||||
setResultOk = true
|
|
||||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!setResultOk) {
|
|
||||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildActivityResultLauncher(activity: AppCompatActivity,
|
|
||||||
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
|
|
||||||
return activity.registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) {
|
|
||||||
// Utility method to loop and close each activity with return data
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
activity.setResult(it.resultCode, it.data)
|
|
||||||
}
|
|
||||||
if (it.resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
activity.finish()
|
|
||||||
|
|
||||||
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
|
|
||||||
// Close the database
|
|
||||||
activity.sendBroadcast(Intent(LOCK_ACTION))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to start an activity with an Autofill for result
|
|
||||||
*/
|
|
||||||
fun startActivityForAutofillResult(activity: AppCompatActivity,
|
|
||||||
intent: Intent,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
database: ContextualDatabase,
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
entriesInfo: List<EntryInfo>,
|
||||||
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
onIntentCreated: (Intent) -> Unit
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
) {
|
||||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
if (entriesInfo.isEmpty()) {
|
||||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
throw IOException("No entries found")
|
||||||
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
} else {
|
||||||
|
StructureParser(autofillComponent.assistStructure).parse()?.let { result ->
|
||||||
|
// New Response
|
||||||
|
onIntentCreated(Intent().putExtra(
|
||||||
|
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||||
|
buildResponse(
|
||||||
|
context = context,
|
||||||
|
database = database,
|
||||||
|
entriesInfo = entriesInfo,
|
||||||
|
parseResult = result,
|
||||||
|
autofillComponent = autofillComponent
|
||||||
|
)
|
||||||
|
))
|
||||||
|
} ?: throw IOException("Unable to parse the structure")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
activityResultLauncher?.launch(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val TAG = AutofillHelper::class.java.name
|
private val TAG = AutofillHelper::class.java.name
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.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.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
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.autofill.inline.UiVersions
|
||||||
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.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
|
||||||
|
import com.kunzisoft.keepass.model.CreditCard
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
class KeeAutofillService : AutofillService() {
|
||||||
|
|
||||||
|
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
|
private var mDatabase: ContextualDatabase? = null
|
||||||
|
private var applicationIdBlocklist: Set<String>? = null
|
||||||
|
private var webDomainBlocklist: Set<String>? = null
|
||||||
|
private var askToSaveData: Boolean = false
|
||||||
|
private var autofillInlineSuggestionsEnabled: Boolean = false
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||||
|
mDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||||
|
this.mDatabase = database
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPreferences() {
|
||||||
|
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
||||||
|
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
||||||
|
askToSaveData = PreferencesUtil.askToSaveAutofillData(this)
|
||||||
|
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFillRequest(
|
||||||
|
request: FillRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: FillCallback
|
||||||
|
) {
|
||||||
|
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||||
|
|
||||||
|
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
|
||||||
|
Log.d(TAG, "Autofill requested in compatibility mode")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Autofill requested in native mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user's settings for authenticating Responses and Datasets.
|
||||||
|
val latestStructure = request.fillContexts.last().structure
|
||||||
|
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||||
|
|
||||||
|
// Build search info only if applicationId or webDomain are not blocked
|
||||||
|
if (autofillAllowedFor(
|
||||||
|
applicationId = parseResult.applicationId,
|
||||||
|
applicationIdBlocklist = applicationIdBlocklist,
|
||||||
|
webDomain = parseResult.webDomain,
|
||||||
|
webDomainBlocklist = webDomainBlocklist)
|
||||||
|
) {
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = parseResult.applicationId
|
||||||
|
webDomain = parseResult.webDomain
|
||||||
|
webScheme = parseResult.webScheme
|
||||||
|
}
|
||||||
|
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
|
CompatInlineSuggestionsRequest(request)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val autofillComponent = AutofillComponent(
|
||||||
|
latestStructure,
|
||||||
|
inlineSuggestionsRequest
|
||||||
|
)
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = mDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, items ->
|
||||||
|
callback.onSuccess(
|
||||||
|
AutofillHelper.buildResponse(
|
||||||
|
context = this,
|
||||||
|
database = openedDatabase,
|
||||||
|
entriesInfo = items,
|
||||||
|
parseResult = parseResult,
|
||||||
|
autofillComponent = autofillComponent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onItemNotFound = { openedDatabase ->
|
||||||
|
// Show UI if no search result
|
||||||
|
showUIForEntrySelection(parseResult, openedDatabase,
|
||||||
|
searchInfo, autofillComponent, callback)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Show UI if database not open
|
||||||
|
showUIForEntrySelection(parseResult, null,
|
||||||
|
searchInfo, autofillComponent, callback)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
private fun showUIForEntrySelection(
|
||||||
|
parseResult: StructureParser.Result,
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
autofillComponent: AutofillComponent,
|
||||||
|
callback: FillCallback
|
||||||
|
) {
|
||||||
|
var success = false
|
||||||
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
|
if (autofillIds.isNotEmpty()) {
|
||||||
|
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||||
|
// to generate Response.
|
||||||
|
AutofillLauncherActivity.getPendingIntentForSelection(
|
||||||
|
this,
|
||||||
|
searchInfo,
|
||||||
|
autofillComponent
|
||||||
|
)?.intentSender?.let { intentSender ->
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
||||||
|
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||||
|
RemoteViews(
|
||||||
|
packageName,
|
||||||
|
R.layout.item_autofill_unlock_web_domain
|
||||||
|
).apply {
|
||||||
|
setTextViewText(
|
||||||
|
R.id.autofill_web_domain_text,
|
||||||
|
parseResult.webDomain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||||
|
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||||
|
setTextViewText(
|
||||||
|
R.id.autofill_app_id_text,
|
||||||
|
parseResult.applicationId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||||
|
RemoteViews(
|
||||||
|
packageName,
|
||||||
|
R.layout.item_autofill_select_entry_web_domain
|
||||||
|
).apply {
|
||||||
|
setTextViewText(
|
||||||
|
R.id.autofill_web_domain_text,
|
||||||
|
parseResult.webDomain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||||
|
RemoteViews(
|
||||||
|
packageName,
|
||||||
|
R.layout.item_autofill_select_entry_app_id
|
||||||
|
).apply {
|
||||||
|
setTextViewText(
|
||||||
|
R.id.autofill_app_id_text,
|
||||||
|
parseResult.applicationId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RemoteViews(packageName, R.layout.item_autofill_select_entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the autofill framework the interest to save credentials
|
||||||
|
if (askToSaveData) {
|
||||||
|
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
||||||
|
val requiredIds = ArrayList<AutofillId>()
|
||||||
|
val optionalIds = ArrayList<AutofillId>()
|
||||||
|
|
||||||
|
// Only if at least a password
|
||||||
|
parseResult.passwordId?.let { passwordInfo ->
|
||||||
|
parseResult.usernameId?.let { usernameInfo ->
|
||||||
|
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
||||||
|
requiredIds.add(usernameInfo)
|
||||||
|
}
|
||||||
|
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
||||||
|
requiredIds.add(passwordInfo)
|
||||||
|
}
|
||||||
|
// or a credit card form
|
||||||
|
if (requiredIds.isEmpty()) {
|
||||||
|
parseResult.creditCardNumberId?.let { numberId ->
|
||||||
|
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
|
||||||
|
requiredIds.add(numberId)
|
||||||
|
Log.d(TAG, "Asking to save credit card number")
|
||||||
|
}
|
||||||
|
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
|
||||||
|
}
|
||||||
|
if (requiredIds.isNotEmpty()) {
|
||||||
|
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
|
||||||
|
if (optionalIds.isNotEmpty()) {
|
||||||
|
builder.setOptionalIds(optionalIds.toTypedArray())
|
||||||
|
}
|
||||||
|
responseBuilder.setSaveInfo(builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build inline presentation
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
&& autofillInlineSuggestionsEnabled
|
||||||
|
) {
|
||||||
|
var inlinePresentation: InlinePresentation? = null
|
||||||
|
autofillComponent.compatInlineSuggestionsRequest
|
||||||
|
?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
|
val inlinePresentationSpecs =
|
||||||
|
inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
|
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||||
|
&& inlinePresentationSpecs.isNotEmpty()
|
||||||
|
) {
|
||||||
|
val inlinePresentationSpec = inlinePresentationSpecs[0]
|
||||||
|
|
||||||
|
// Make sure that the IME spec claims support for v1 UI template.
|
||||||
|
val imeStyle = inlinePresentationSpec.style
|
||||||
|
if (UiVersions.getVersions(imeStyle)
|
||||||
|
.contains(UiVersions.INLINE_UI_VERSION_1)
|
||||||
|
) {
|
||||||
|
// Build the content for IME UI
|
||||||
|
inlinePresentation = InlinePresentation(
|
||||||
|
InlineSuggestionUi.newContentBuilder(
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
randomRequestCode(),
|
||||||
|
Intent(this, AutofillSettingsActivity::class.java),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
).apply {
|
||||||
|
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
||||||
|
setTitle(getString(R.string.autofill_sign_in_prompt))
|
||||||
|
setStartIcon(
|
||||||
|
Icon.createWithResource(
|
||||||
|
this@KeeAutofillService,
|
||||||
|
R.mipmap.ic_launcher_round
|
||||||
|
).apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
})
|
||||||
|
}.build().slice, inlinePresentationSpec, false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
try {
|
||||||
|
// Buggy method on some API 33 devices
|
||||||
|
responseBuilder.setAuthentication(
|
||||||
|
autofillIds,
|
||||||
|
intentSender,
|
||||||
|
Presentations.Builder().apply {
|
||||||
|
inlinePresentation?.let {
|
||||||
|
setInlinePresentation(it)
|
||||||
|
}
|
||||||
|
setDialogPresentation(remoteViewsUnlock)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to use the new setAuthentication method.", e)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
responseBuilder.setAuthentication(
|
||||||
|
autofillIds,
|
||||||
|
intentSender,
|
||||||
|
remoteViewsUnlock,
|
||||||
|
inlinePresentation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
responseBuilder.setAuthentication(
|
||||||
|
autofillIds,
|
||||||
|
intentSender,
|
||||||
|
remoteViewsUnlock,
|
||||||
|
inlinePresentation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
responseBuilder.setAuthentication(
|
||||||
|
autofillIds,
|
||||||
|
intentSender,
|
||||||
|
remoteViewsUnlock
|
||||||
|
)
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
callback.onSuccess(responseBuilder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!success)
|
||||||
|
callback.onFailure("Unable to get Autofill ids for UI selection")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||||
|
var success = false
|
||||||
|
if (askToSaveData && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val latestStructure = request.fillContexts.last().structure
|
||||||
|
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||||
|
|
||||||
|
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
|
||||||
|
var expiration: DateTime? = parseResult.creditCardExpirationValue
|
||||||
|
if (parseResult.creditCardExpirationValue == null
|
||||||
|
&& parseResult.creditCardExpirationYearValue != 0
|
||||||
|
&& parseResult.creditCardExpirationMonthValue != 0) {
|
||||||
|
expiration = DateTime()
|
||||||
|
.withYear(parseResult.creditCardExpirationYearValue)
|
||||||
|
.withMonthOfYear(parseResult.creditCardExpirationMonthValue)
|
||||||
|
if (parseResult.creditCardExpirationDayValue != 0) {
|
||||||
|
expiration = expiration.withDayOfMonth(parseResult.creditCardExpirationDayValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show UI to save data
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = parseResult.applicationId
|
||||||
|
webDomain = parseResult.webDomain
|
||||||
|
webScheme = parseResult.webScheme
|
||||||
|
}
|
||||||
|
val registerInfo = RegisterInfo(
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
username = parseResult.usernameValue?.textValue?.toString(),
|
||||||
|
password = parseResult.passwordValue?.textValue?.toString(),
|
||||||
|
creditCard = parseResult.creditCardNumber?.let { cardNumber ->
|
||||||
|
CreditCard(
|
||||||
|
parseResult.creditCardHolder,
|
||||||
|
cardNumber,
|
||||||
|
expiration,
|
||||||
|
parseResult.cardVerificationValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AutofillLauncherActivity.getPendingIntentForRegistration(
|
||||||
|
this,
|
||||||
|
registerInfo
|
||||||
|
)?.intentSender?.let { intentSender ->
|
||||||
|
success = true
|
||||||
|
callback.onSuccess(intentSender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
callback.onFailure("Saving form values is not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnected() {
|
||||||
|
Log.d(TAG, "onConnected")
|
||||||
|
getPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnected() {
|
||||||
|
Log.d(TAG, "onDisconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
elementNotNull.contains(appIdBlocked)
|
||||||
|
} == true
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Autofill not allowed for $elementNotNull")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -27,8 +27,7 @@ import android.view.autofill.AutofillId
|
|||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +36,6 @@ import kotlin.collections.ArrayList
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class StructureParser(private val structure: AssistStructure) {
|
class StructureParser(private val structure: AssistStructure) {
|
||||||
private var result: Result? = null
|
private var result: Result? = null
|
||||||
private var usernameNeeded = true
|
|
||||||
private var usernameIdCandidate: AutofillId? = null
|
private var usernameIdCandidate: AutofillId? = null
|
||||||
private var usernameValueCandidate: AutofillValue? = null
|
private var usernameValueCandidate: AutofillValue? = null
|
||||||
|
|
||||||
@@ -53,7 +51,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
applicationId = windowNode.title.toString().split("/")[0]
|
applicationId = windowNode.title.toString().split("/")[0]
|
||||||
Log.d(TAG, "Autofill applicationId: $applicationId")
|
Log.d(TAG, "Autofill applicationId: $applicationId")
|
||||||
|
|
||||||
if (applicationId?.contains("PopupWindow:") == false) {
|
if (applicationId?.contains(APPLICATION_ID_POPUP_WINDOW) == false) {
|
||||||
if (parseViewNode(windowNode.rootViewNode))
|
if (parseViewNode(windowNode.rootViewNode))
|
||||||
break@mainLoop
|
break@mainLoop
|
||||||
}
|
}
|
||||||
@@ -105,7 +103,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
if (node.autofillId != null) {
|
if (node.autofillId != null) {
|
||||||
// Parse methods
|
// Parse methods
|
||||||
val hints = node.autofillHints
|
val hints = node.autofillHints
|
||||||
if (hints != null && hints.isNotEmpty()) {
|
if (!hints.isNullOrEmpty()) {
|
||||||
if (parseNodeByAutofillHint(node))
|
if (parseNodeByAutofillHint(node))
|
||||||
returnValue = true
|
returnValue = true
|
||||||
} else if (parseNodeByHtmlAttributes(node))
|
} else if (parseNodeByHtmlAttributes(node))
|
||||||
@@ -134,16 +132,37 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
||||||
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||||
|| it.contains("email", true)
|
|| it.contains("email", true)
|
||||||
|| it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
|
|| it.contains("login", true) -> {
|
||||||
|
// Replace username until we have a password
|
||||||
|
if (result?.passwordId == null) {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
result?.usernameValue = node.autofillValue
|
result?.usernameValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username hint")
|
Log.d(TAG, "Autofill username hint if no password")
|
||||||
|
} else {
|
||||||
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username hint if password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
|
||||||
|
if (usernameIdCandidate == null) {
|
||||||
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill phone")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
||||||
|
// Password Id changed if it's the second times we are here,
|
||||||
|
// So the last username candidate is most appropriate
|
||||||
|
if (result?.passwordId != null && usernameIdCandidate != null) {
|
||||||
|
result?.usernameId = usernameIdCandidate
|
||||||
|
result?.usernameValue = usernameValueCandidate
|
||||||
|
}
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password hint")
|
Log.d(TAG, "Autofill password hint")
|
||||||
return true
|
// Comment "return" to check all the tree
|
||||||
|
// return true
|
||||||
}
|
}
|
||||||
it.equals("cc-name", true) -> {
|
it.equals("cc-name", true) -> {
|
||||||
Log.d(TAG, "Autofill credit card name hint")
|
Log.d(TAG, "Autofill credit card name hint")
|
||||||
@@ -279,15 +298,20 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
"type" -> {
|
"type" -> {
|
||||||
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
|
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
|
||||||
"tel", "email" -> {
|
"tel", "email" -> {
|
||||||
|
if (result?.passwordId == null) {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
result?.usernameValue = node.autofillValue
|
result?.usernameValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
"text" -> {
|
"text" -> {
|
||||||
|
// Assume username is before password
|
||||||
|
if (result?.passwordId == null) {
|
||||||
usernameIdCandidate = autofillId
|
usernameIdCandidate = autofillId
|
||||||
usernameValueCandidate = node.autofillValue
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
"password" -> {
|
"password" -> {
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
@@ -315,31 +339,37 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
return "0x${"%08x".format(inputType)}"
|
return "0x${"%08x".format(inputType)}"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
private fun manageTypeText(
|
||||||
val autofillId = node.autofillId
|
node: AssistStructure.ViewNode,
|
||||||
val inputType = node.inputType
|
autofillId: AutofillId?,
|
||||||
when (inputType and InputType.TYPE_MASK_CLASS) {
|
inputType: Int
|
||||||
InputType.TYPE_CLASS_TEXT -> {
|
): Boolean {
|
||||||
when {
|
when {
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
||||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
|
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
|
||||||
|
if (result?.passwordId == null) {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
result?.usernameValue = node.autofillValue
|
result?.usernameValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
||||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
||||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
||||||
|
// Assume the username field is before the password field
|
||||||
|
if (result?.passwordId == null) {
|
||||||
usernameIdCandidate = autofillId
|
usernameIdCandidate = autofillId
|
||||||
usernameValueCandidate = node.autofillValue
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||||
// Some forms used visible password as username
|
// Some forms used visible password as username
|
||||||
if (usernameIdCandidate == null && usernameValueCandidate == null) {
|
if (result?.passwordId == null &&
|
||||||
|
usernameIdCandidate == null && usernameValueCandidate == null) {
|
||||||
usernameIdCandidate = autofillId
|
usernameIdCandidate = autofillId
|
||||||
usernameValueCandidate = node.autofillValue
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
|
||||||
@@ -347,7 +377,6 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
|
||||||
usernameNeeded = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
@@ -367,13 +396,20 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
|
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
|
||||||
InputType.TYPE_TEXT_VARIATION_URI) -> {
|
InputType.TYPE_TEXT_VARIATION_URI) -> {
|
||||||
// Type not used
|
// Type not used
|
||||||
|
Log.d(TAG, "Autofill not used android text type: ${showHexInputType(inputType)}")
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
InputType.TYPE_CLASS_NUMBER -> {
|
|
||||||
|
private fun manageTypeNumber(
|
||||||
|
node: AssistStructure.ViewNode,
|
||||||
|
autofillId: AutofillId?,
|
||||||
|
inputType: Int
|
||||||
|
): Boolean {
|
||||||
when {
|
when {
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
||||||
@@ -394,6 +430,37 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun manageTypeNull(
|
||||||
|
node: AssistStructure.ViewNode,
|
||||||
|
autofillId: AutofillId?,
|
||||||
|
inputType: Int
|
||||||
|
): Boolean {
|
||||||
|
if (node.className == "android.widget.EditText") {
|
||||||
|
Log.d(TAG, "Autofill null android input type class: ${showHexInputType(inputType)}" +
|
||||||
|
", get the EditText node class name!")
|
||||||
|
if (result?.passwordId == null) {
|
||||||
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
||||||
|
val autofillId = node.autofillId
|
||||||
|
val inputType = node.inputType
|
||||||
|
when (inputType and InputType.TYPE_MASK_CLASS) {
|
||||||
|
InputType.TYPE_CLASS_TEXT -> {
|
||||||
|
return manageTypeText(node, autofillId, inputType)
|
||||||
|
}
|
||||||
|
InputType.TYPE_CLASS_NUMBER -> {
|
||||||
|
return manageTypeNumber(node, autofillId, inputType)
|
||||||
|
}
|
||||||
|
InputType.TYPE_NULL -> {
|
||||||
|
return manageTypeNull(node, autofillId, inputType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -422,58 +489,14 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
var creditCardExpirationDayOptions: Array<CharSequence>? = null
|
var creditCardExpirationDayOptions: Array<CharSequence>? = null
|
||||||
|
|
||||||
var usernameId: AutofillId? = null
|
var usernameId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var passwordId: AutofillId? = null
|
var passwordId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardHolderId: AutofillId? = null
|
var creditCardHolderId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardNumberId: AutofillId? = null
|
var creditCardNumberId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardExpirationDateId: AutofillId? = null
|
var creditCardExpirationDateId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardExpirationYearId: AutofillId? = null
|
var creditCardExpirationYearId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardExpirationMonthId: AutofillId? = null
|
var creditCardExpirationMonthId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardExpirationDayId: AutofillId? = null
|
var creditCardExpirationDayId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var cardVerificationValueId: AutofillId? = null
|
var cardVerificationValueId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun allAutofillIds(): Array<AutofillId> {
|
fun allAutofillIds(): Array<AutofillId> {
|
||||||
val all = ArrayList<AutofillId>()
|
val all = ArrayList<AutofillId>()
|
||||||
@@ -500,13 +523,13 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
|
|
||||||
var usernameValue: AutofillValue? = null
|
var usernameValue: AutofillValue? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
if (allowSaveValues && field == null)
|
if (allowSaveValues)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
var passwordValue: AutofillValue? = null
|
var passwordValue: AutofillValue? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
if (allowSaveValues && field == null)
|
if (allowSaveValues)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,5 +582,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = StructureParser::class.java.name
|
private val TAG = StructureParser::class.java.name
|
||||||
|
|
||||||
|
const val APPLICATION_ID_POPUP_WINDOW = "PopupWindow:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard;
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard;
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||||
|
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
@@ -52,7 +52,7 @@ import android.widget.TextView;
|
|||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
import com.kunzisoft.keepass.R;
|
||||||
import com.kunzisoft.keepass.magikeyboard.Keyboard.Key;
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.Keyboard.Key;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -41,9 +41,10 @@ import androidx.core.graphics.BlendModeCompat
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
@@ -324,9 +325,9 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
actionGoAutomatically()
|
actionGoAutomatically()
|
||||||
}
|
}
|
||||||
KEY_FIELDS -> {
|
KEY_FIELDS -> {
|
||||||
getEntryInfo()?.customFields?.let { customFields ->
|
getEntryInfo()?.getCustomFieldsForFilling()?.let { customFields ->
|
||||||
fieldsAdapter?.apply {
|
fieldsAdapter?.apply {
|
||||||
setFields(customFields.filter { it.name != OTP_TOKEN_FIELD})
|
setFields(customFields)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,10 +342,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
mDatabase,
|
context = this,
|
||||||
searchInfo,
|
database = mDatabase,
|
||||||
{ _, items ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { _, items ->
|
||||||
performSelection(
|
performSelection(
|
||||||
items,
|
items,
|
||||||
{
|
{
|
||||||
@@ -361,11 +363,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
onItemNotFound = {
|
||||||
// Select if not found
|
// Select if not found
|
||||||
launchEntrySelection(searchInfo)
|
launchEntrySelection(searchInfo)
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// Select if database not opened
|
// Select if database not opened
|
||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
launchEntrySelection(searchInfo)
|
launchEntrySelection(searchInfo)
|
||||||
@@ -460,34 +462,23 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performSelection(items: List<EntryInfo>,
|
fun performSelection(
|
||||||
|
items: List<EntryInfo>,
|
||||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||||
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
actionEntrySelection: (autoSearch: Boolean) -> Unit
|
||||||
if (items.size == 1) {
|
) {
|
||||||
val itemFound = items[0]
|
EntrySelectionHelper.performSelection(
|
||||||
|
items = items,
|
||||||
|
actionPopulateCredentialProvider = { itemFound ->
|
||||||
if (entryUUID != itemFound.id) {
|
if (entryUUID != itemFound.id) {
|
||||||
actionPopulateKeyboard.invoke(itemFound)
|
actionPopulateKeyboard.invoke(itemFound)
|
||||||
} else {
|
} else {
|
||||||
// Force selection if magikeyboard already populated
|
// Force selection if magikeyboard already populated
|
||||||
actionEntrySelection.invoke(false)
|
actionEntrySelection.invoke(false)
|
||||||
}
|
}
|
||||||
} else if (items.size > 1) {
|
},
|
||||||
// Select the one we want in the selection
|
actionEntrySelection = actionEntrySelection
|
||||||
actionEntrySelection.invoke(true)
|
)
|
||||||
} else {
|
|
||||||
// Select an arbitrary one
|
|
||||||
actionEntrySelection.invoke(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
|
||||||
entry: EntryInfo,
|
|
||||||
toast: Boolean = true) {
|
|
||||||
// Populate Magikeyboard with entry
|
|
||||||
addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
|
||||||
// Consume the selection mode
|
|
||||||
EntrySelectionHelper.removeModesFromIntent(activity.intent)
|
|
||||||
activity.moveTaskToBack(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey
|
||||||
|
|
||||||
|
import android.graphics.BlendMode
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.os.OutcomeReceiver
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.exceptions.ClearCredentialException
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialException
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.BeginCreateCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||||
|
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||||
|
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||||
|
import androidx.credentials.provider.CreateEntry
|
||||||
|
import androidx.credentials.provider.CredentialEntry
|
||||||
|
import androidx.credentials.provider.CredentialProviderService
|
||||||
|
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||||
|
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil.isPasskeyAutoSelectEnable
|
||||||
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
import java.io.IOException
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeyProviderService : CredentialProviderService() {
|
||||||
|
|
||||||
|
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
|
private var mDatabase: ContextualDatabase? = null
|
||||||
|
private lateinit var defaultIcon: Icon
|
||||||
|
private var isAutoSelectAllowed: Boolean = false
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||||
|
mDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||||
|
this.mDatabase = database
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultIcon = Icon.createWithResource(
|
||||||
|
this@PasskeyProviderService,
|
||||||
|
R.mipmap.ic_launcher_round
|
||||||
|
).apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
}
|
||||||
|
|
||||||
|
isAutoSelectAllowed = isPasskeyAutoSelectEnable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
|
||||||
|
return SearchInfo().apply {
|
||||||
|
this.relyingParty = relyingParty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeginGetCredentialRequest(
|
||||||
|
request: BeginGetCredentialRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
|
||||||
|
) {
|
||||||
|
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
|
||||||
|
try {
|
||||||
|
processGetCredentialsRequest(request) { response ->
|
||||||
|
callback.onResult(response)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
|
||||||
|
callback.onError(GetCredentialUnknownException())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processGetCredentialsRequest(
|
||||||
|
request: BeginGetCredentialRequest,
|
||||||
|
callback: (BeginGetCredentialResponse?) -> Unit
|
||||||
|
) {
|
||||||
|
var knownOption = false
|
||||||
|
for (option in request.beginGetCredentialOptions) {
|
||||||
|
when (option) {
|
||||||
|
is BeginGetPublicKeyCredentialOption -> {
|
||||||
|
knownOption = true
|
||||||
|
populatePasskeyData(option) { listCredentials ->
|
||||||
|
callback(BeginGetCredentialResponse(listCredentials))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (knownOption.not()) {
|
||||||
|
throw IOException("unknown type of beginGetCredentialOption")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populatePasskeyData(
|
||||||
|
option: BeginGetPublicKeyCredentialOption,
|
||||||
|
callback: (List<CredentialEntry>) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
|
val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId
|
||||||
|
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
||||||
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = mDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { database, items ->
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection with found items")
|
||||||
|
for (passkeyEntry in items) {
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
nodeId = passkeyEntry.id,
|
||||||
|
appOrigin = passkeyEntry.appOrigin
|
||||||
|
)?.let { usagePendingIntent ->
|
||||||
|
val passkey = passkeyEntry.passkey
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = passkey?.username ?: "Unknown",
|
||||||
|
icon = passkeyEntry.buildIcon(this@PasskeyProviderService, database)?.apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
} ?: defaultIcon,
|
||||||
|
pendingIntent = usagePendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
displayName = passkeyEntry.getVisualTitle(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
|
},
|
||||||
|
onItemNotFound = { _ ->
|
||||||
|
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_database_username),
|
||||||
|
displayName = getString(R.string.passkey_selection_description),
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
||||||
|
// Database is locked, a public key credential entry is shown to unlock it
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_database_username),
|
||||||
|
displayName = getString(R.string.passkey_locked_database_description),
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeginCreateCredentialRequest(
|
||||||
|
request: BeginCreateCredentialRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
|
||||||
|
) {
|
||||||
|
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
|
||||||
|
try {
|
||||||
|
processCreateCredentialRequest(request) {
|
||||||
|
callback.onResult(BeginCreateCredentialResponse(it))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
|
||||||
|
toastError(e)
|
||||||
|
callback.onError(CreateCredentialUnknownException(e.localizedMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processCreateCredentialRequest(
|
||||||
|
request: BeginCreateCredentialRequest,
|
||||||
|
callback: (List<CreateEntry>) -> Unit
|
||||||
|
) {
|
||||||
|
when (request) {
|
||||||
|
is BeginCreatePublicKeyCredentialRequest -> {
|
||||||
|
// Request is passkey type
|
||||||
|
handleCreatePasskeyQuery(request, callback)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// request type not supported
|
||||||
|
throw IOException("unknown type of BeginCreateCredentialRequest")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
||||||
|
accountName: String,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Add pending intent for registration in opened database to create new item")
|
||||||
|
// TODO add a setting to directly store in a specific group
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
this.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
description = getString(R.string.passkey_creation_description)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCreatePasskeyQuery(
|
||||||
|
request: BeginCreatePublicKeyCredentialRequest,
|
||||||
|
callback: (List<CreateEntry>) -> Unit
|
||||||
|
) {
|
||||||
|
val databaseName = mDatabase?.name
|
||||||
|
val accountName =
|
||||||
|
if (databaseName?.isBlank() != false)
|
||||||
|
getString(R.string.passkey_database_username)
|
||||||
|
else databaseName
|
||||||
|
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
||||||
|
val relyingPartyId = PublicKeyCredentialCreationOptions(
|
||||||
|
requestJson = request.requestJson,
|
||||||
|
clientDataHash = request.clientDataHash
|
||||||
|
).relyingPartyEntity.id
|
||||||
|
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
||||||
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = mDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { database, items ->
|
||||||
|
if (database.isReadOnly) {
|
||||||
|
throw RegisterInReadOnlyDatabaseException()
|
||||||
|
} else {
|
||||||
|
// To create a new entry
|
||||||
|
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||||
|
/* TODO Overwrite
|
||||||
|
// To select an existing entry and permit an overwrite
|
||||||
|
Log.w(TAG, "Passkey already registered")
|
||||||
|
for (entryInfo in items) {
|
||||||
|
PasskeyHelper.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
passkeyEntryNodeId = entryInfo.id
|
||||||
|
)?.let { createPendingIntent ->
|
||||||
|
createEntries.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
pendingIntent = createPendingIntent,
|
||||||
|
description = getString(
|
||||||
|
R.string.passkey_update_description,
|
||||||
|
entryInfo.passkey?.displayName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
callback(createEntries)
|
||||||
|
},
|
||||||
|
onItemNotFound = { database ->
|
||||||
|
// To create a new entry
|
||||||
|
if (database.isReadOnly) {
|
||||||
|
throw RegisterInReadOnlyDatabaseException()
|
||||||
|
} else {
|
||||||
|
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||||
|
}
|
||||||
|
callback(createEntries)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Launch the passkey launcher activity to open the database
|
||||||
|
Log.d(TAG, "Add pending intent for passkey registration in closed database")
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
createEntries.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
description = getString(R.string.passkey_locked_database_description)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
callback(createEntries)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClearCredentialStateRequest(
|
||||||
|
request: ProviderClearCredentialStateRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<Void?, ClearCredentialException>
|
||||||
|
) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PasskeyProviderService::class.java.simpleName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 AOSP modified by 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an Android privileged app, based on AOSP code
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class AndroidPrivilegedApp(
|
||||||
|
val packageName: String,
|
||||||
|
val fingerprints: Set<String>
|
||||||
|
): Parcelable {
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$packageName ($fingerprints)"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PACKAGE_NAME_KEY = "package_name"
|
||||||
|
private const val SIGNATURES_KEY = "signatures"
|
||||||
|
private const val FINGERPRINT_KEY = "cert_fingerprint_sha256"
|
||||||
|
private const val BUILD_KEY = "build"
|
||||||
|
private const val USER_DEBUG_KEY = "userdebug"
|
||||||
|
private const val TYPE_KEY = "type"
|
||||||
|
private const val APP_INFO_KEY = "info"
|
||||||
|
private const val ANDROID_TYPE_KEY = "android"
|
||||||
|
private const val USER_BUILD_TYPE = "userdebug"
|
||||||
|
private const val APPS_KEY = "apps"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a list of AndroidPrivilegedApp objects from a JSONObject.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun extractPrivilegedApps(jsonObject: JSONObject): List<AndroidPrivilegedApp> {
|
||||||
|
val apps = mutableListOf<AndroidPrivilegedApp>()
|
||||||
|
if (!jsonObject.has(APPS_KEY)) {
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
val appsJsonArray = jsonObject.getJSONArray(APPS_KEY)
|
||||||
|
for (i in 0 until appsJsonArray.length()) {
|
||||||
|
try {
|
||||||
|
val appJsonObject = appsJsonArray.getJSONObject(i)
|
||||||
|
if (appJsonObject.getString(TYPE_KEY) != ANDROID_TYPE_KEY) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!appJsonObject.has(APP_INFO_KEY)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apps.add(
|
||||||
|
createFromJSONObject(
|
||||||
|
appJsonObject.getJSONObject(APP_INFO_KEY)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Log.e(AndroidPrivilegedApp::class.simpleName, "Error parsing privileged app", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an AndroidPrivilegedApp object from a JSONObject.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
private fun createFromJSONObject(
|
||||||
|
appInfoJsonObject: JSONObject,
|
||||||
|
filterUserDebug: Boolean = true
|
||||||
|
): AndroidPrivilegedApp {
|
||||||
|
val signaturesJson = appInfoJsonObject.getJSONArray(SIGNATURES_KEY)
|
||||||
|
val fingerprints = mutableSetOf<String>()
|
||||||
|
for (j in 0 until signaturesJson.length()) {
|
||||||
|
if (filterUserDebug) {
|
||||||
|
if (USER_DEBUG_KEY == signaturesJson.getJSONObject(j)
|
||||||
|
.optString(BUILD_KEY) && USER_BUILD_TYPE != Build.TYPE
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fingerprints.add(signaturesJson.getJSONObject(j).getString(FINGERPRINT_KEY))
|
||||||
|
}
|
||||||
|
return AndroidPrivilegedApp(
|
||||||
|
packageName = appInfoJsonObject.getString(PACKAGE_NAME_KEY),
|
||||||
|
fingerprints = fingerprints
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a JSONObject from a list of AndroidPrivilegedApp objects.
|
||||||
|
* The structure will be similar to what `extractPrivilegedApps` expects.
|
||||||
|
*
|
||||||
|
* @param privilegedApps The list of AndroidPrivilegedApp objects.
|
||||||
|
* @return A JSONObject representing the list.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun toJsonObject(privilegedApps: List<AndroidPrivilegedApp>): JSONObject {
|
||||||
|
val rootJsonObject = JSONObject()
|
||||||
|
val appsJsonArray = JSONArray()
|
||||||
|
|
||||||
|
for (app in privilegedApps) {
|
||||||
|
val appInfoObject = JSONObject()
|
||||||
|
appInfoObject.put(PACKAGE_NAME_KEY, app.packageName)
|
||||||
|
|
||||||
|
val signaturesArray = JSONArray()
|
||||||
|
for (fingerprint in app.fingerprints) {
|
||||||
|
val signatureObject = JSONObject()
|
||||||
|
signatureObject.put(FINGERPRINT_KEY, fingerprint)
|
||||||
|
// If needed: signatureObject.put(BUILD_KEY, "user")
|
||||||
|
signaturesArray.put(signatureObject)
|
||||||
|
}
|
||||||
|
appInfoObject.put(SIGNATURES_KEY, signaturesArray)
|
||||||
|
|
||||||
|
val appContainerObject = JSONObject()
|
||||||
|
appContainerObject.put(TYPE_KEY, ANDROID_TYPE_KEY)
|
||||||
|
appContainerObject.put(APP_INFO_KEY, appInfoObject)
|
||||||
|
|
||||||
|
appsJsonArray.put(appContainerObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootJsonObject.put(APPS_KEY, appsJsonArray)
|
||||||
|
return rootJsonObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import com.kunzisoft.encrypt.Signature
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class AuthenticatorAssertionResponse(
|
||||||
|
private val requestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
private val userPresent: Boolean,
|
||||||
|
private val userVerified: Boolean,
|
||||||
|
private val backupEligibility: Boolean,
|
||||||
|
private val backupState: Boolean,
|
||||||
|
private var userHandle: String,
|
||||||
|
privateKey: String,
|
||||||
|
private val clientDataResponse: ClientDataResponse,
|
||||||
|
) : AuthenticatorResponse {
|
||||||
|
|
||||||
|
override var clientJson = JSONObject()
|
||||||
|
private var authenticatorData: ByteArray = AuthenticatorData.buildAuthenticatorData(
|
||||||
|
relyingPartyId = requestOptions.rpId.toByteArray(),
|
||||||
|
userPresent = userPresent,
|
||||||
|
userVerified = userVerified,
|
||||||
|
backupEligibility = backupEligibility,
|
||||||
|
backupState = backupState
|
||||||
|
)
|
||||||
|
private var signature: ByteArray = byteArrayOf()
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
signature = Signature.sign(privateKey, dataToSign())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(this::class.java.simpleName, "Unable to sign: ${e.message}")
|
||||||
|
throw GetCredentialUnknownException("Signing failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dataToSign(): ByteArray {
|
||||||
|
return authenticatorData + clientDataResponse.hashData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun json(): JSONObject {
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#authdata-flags
|
||||||
|
return clientJson.apply {
|
||||||
|
put("clientDataJSON", clientDataResponse.buildResponse())
|
||||||
|
put("authenticatorData", b64Encode(authenticatorData))
|
||||||
|
put("signature", b64Encode(signature))
|
||||||
|
put("userHandle", userHandle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import com.kunzisoft.keepass.utils.UUIDUtils.asBytes
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class AuthenticatorAttestationResponse(
|
||||||
|
private val requestOptions: PublicKeyCredentialCreationOptions,
|
||||||
|
private val credentialId: ByteArray,
|
||||||
|
private val credentialPublicKey: ByteArray,
|
||||||
|
private val userPresent: Boolean,
|
||||||
|
private val userVerified: Boolean,
|
||||||
|
private val backupEligibility: Boolean,
|
||||||
|
private val backupState: Boolean,
|
||||||
|
private val publicKeyTypeId: Long,
|
||||||
|
private val publicKeyCbor: ByteArray,
|
||||||
|
private val clientDataResponse: ClientDataResponse,
|
||||||
|
) : AuthenticatorResponse {
|
||||||
|
|
||||||
|
override var clientJson = JSONObject()
|
||||||
|
var attestationObject: ByteArray
|
||||||
|
|
||||||
|
init {
|
||||||
|
attestationObject = defaultAttestationObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAuthData(): ByteArray {
|
||||||
|
return AuthenticatorData.buildAuthenticatorData(
|
||||||
|
relyingPartyId = requestOptions.relyingPartyEntity.id.toByteArray(),
|
||||||
|
userPresent = userPresent,
|
||||||
|
userVerified = userVerified,
|
||||||
|
backupEligibility = backupEligibility,
|
||||||
|
backupState = backupState,
|
||||||
|
attestedCredentialData = true
|
||||||
|
) + AAGUID +
|
||||||
|
//credIdLen
|
||||||
|
byteArrayOf((credentialId.size shr 8).toByte(), credentialId.size.toByte()) +
|
||||||
|
credentialId +
|
||||||
|
credentialPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun defaultAttestationObject(): ByteArray {
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#attestation-object
|
||||||
|
val ao = mutableMapOf<String, Any>()
|
||||||
|
ao.put("fmt", "none")
|
||||||
|
ao.put("attStmt", emptyMap<Any, Any>())
|
||||||
|
ao.put("authData", buildAuthData())
|
||||||
|
return Cbor().encode(ao)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun json(): JSONObject {
|
||||||
|
// See AuthenticatorAttestationResponseJSON at
|
||||||
|
// https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
|
||||||
|
return clientJson.apply {
|
||||||
|
put("clientDataJSON", clientDataResponse.buildResponse())
|
||||||
|
put("authenticatorData", b64Encode(buildAuthData()))
|
||||||
|
put("transports", JSONArray(listOf("internal", "hybrid")))
|
||||||
|
put("publicKey", b64Encode(publicKeyCbor))
|
||||||
|
put("publicKeyAlgorithm", publicKeyTypeId)
|
||||||
|
put("attestationObject", b64Encode(attestationObject))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Authenticator Attestation Global Unique Identifier
|
||||||
|
private val AAGUID: ByteArray = UUID.fromString("eaecdef2-1c31-5634-8639-f1cbd9c00a08").asBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.HashManager
|
||||||
|
|
||||||
|
class AuthenticatorData {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun buildAuthenticatorData(
|
||||||
|
relyingPartyId: ByteArray,
|
||||||
|
userPresent: Boolean,
|
||||||
|
userVerified: Boolean,
|
||||||
|
backupEligibility: Boolean,
|
||||||
|
backupState: Boolean,
|
||||||
|
attestedCredentialData: Boolean = false
|
||||||
|
): ByteArray {
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#table-authData
|
||||||
|
var flags = 0
|
||||||
|
if (userPresent)
|
||||||
|
flags = flags or 0x01
|
||||||
|
// bit at index 1 is reserved
|
||||||
|
if (userVerified)
|
||||||
|
flags = flags or 0x04
|
||||||
|
if (backupEligibility)
|
||||||
|
flags = flags or 0x08
|
||||||
|
if (backupState)
|
||||||
|
flags = flags or 0x10
|
||||||
|
// bit at index 5 is reserved
|
||||||
|
if (attestedCredentialData) {
|
||||||
|
flags = flags or 0x40
|
||||||
|
}
|
||||||
|
// bit at index 7: Extension data included == false
|
||||||
|
|
||||||
|
return HashManager.hashSha256(relyingPartyId) +
|
||||||
|
byteArrayOf(flags.toByte()) +
|
||||||
|
byteArrayOf(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
interface AuthenticatorResponse {
|
||||||
|
var clientJson: JSONObject
|
||||||
|
|
||||||
|
fun json(): JSONObject
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import androidx.annotation.RestrictTo
|
||||||
|
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||||
|
class Cbor {
|
||||||
|
data class Item(val item: Any, val len: Int)
|
||||||
|
|
||||||
|
data class Arg(val arg: Long, val len: Int)
|
||||||
|
|
||||||
|
val TYPE_UNSIGNED_INT = 0x00
|
||||||
|
val TYPE_NEGATIVE_INT = 0x01
|
||||||
|
val TYPE_BYTE_STRING = 0x02
|
||||||
|
val TYPE_TEXT_STRING = 0x03
|
||||||
|
val TYPE_ARRAY = 0x04
|
||||||
|
val TYPE_MAP = 0x05
|
||||||
|
val TYPE_TAG = 0x06
|
||||||
|
val TYPE_FLOAT = 0x07
|
||||||
|
|
||||||
|
fun decode(data: ByteArray): Any {
|
||||||
|
val ret = parseItem(data, 0)
|
||||||
|
return ret.item
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encode(data: Any): ByteArray {
|
||||||
|
if (data is Number) {
|
||||||
|
if (data is Double) {
|
||||||
|
throw IllegalArgumentException("Don't support doubles yet")
|
||||||
|
} else {
|
||||||
|
val value = data.toLong()
|
||||||
|
if (value >= 0) {
|
||||||
|
return createArg(TYPE_UNSIGNED_INT, value)
|
||||||
|
} else {
|
||||||
|
return createArg(TYPE_NEGATIVE_INT, -1 - value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data is ByteArray) {
|
||||||
|
return createArg(TYPE_BYTE_STRING, data.size.toLong()) + data
|
||||||
|
}
|
||||||
|
if (data is String) {
|
||||||
|
return createArg(TYPE_TEXT_STRING, data.length.toLong()) + data.encodeToByteArray()
|
||||||
|
}
|
||||||
|
if (data is List<*>) {
|
||||||
|
var ret = createArg(TYPE_ARRAY, data.size.toLong())
|
||||||
|
for (i in data) {
|
||||||
|
ret += encode(i!!)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
if (data is Map<*, *>) {
|
||||||
|
// See:
|
||||||
|
// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#ctap2-canonical-cbor-encoding-form
|
||||||
|
var ret = createArg(TYPE_MAP, data.size.toLong())
|
||||||
|
var byteMap: MutableMap<ByteArray, ByteArray> = mutableMapOf()
|
||||||
|
for (i in data) {
|
||||||
|
// Convert to byte arrays so we can sort them.
|
||||||
|
byteMap.put(encode(i.key!!), encode(i.value!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
var keysList = ArrayList<ByteArray>(byteMap.keys)
|
||||||
|
keysList.sortedWith(
|
||||||
|
Comparator<ByteArray> { a, b ->
|
||||||
|
// If two keys have different lengths, the shorter one sorts earlier;
|
||||||
|
// If two keys have the same length, the one with the lower value in (byte-wise)
|
||||||
|
// lexical order sorts earlier.
|
||||||
|
var aBytes = byteMap.get(a)!!
|
||||||
|
var bBytes = byteMap.get(b)!!
|
||||||
|
when {
|
||||||
|
a.size > b.size -> 1
|
||||||
|
a.size < b.size -> -1
|
||||||
|
aBytes.size > bBytes.size -> 1
|
||||||
|
aBytes.size < bBytes.size -> -1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for (key in keysList) {
|
||||||
|
ret += key
|
||||||
|
ret += byteMap.get(key)!!
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Bad type")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getType(data: ByteArray, offset: Int): Int {
|
||||||
|
val d = data[offset].toInt()
|
||||||
|
return (d and 0xFF) shr 5
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getArg(data: ByteArray, offset: Int): Arg {
|
||||||
|
val arg = data[offset].toLong() and 0x1F
|
||||||
|
if (arg < 24) {
|
||||||
|
return Arg(arg, 1)
|
||||||
|
}
|
||||||
|
if (arg == 24L) {
|
||||||
|
return Arg(data[offset + 1].toLong() and 0xFF, 2)
|
||||||
|
}
|
||||||
|
if (arg == 25L) {
|
||||||
|
var ret = (data[offset + 1].toLong() and 0xFF) shl 8
|
||||||
|
ret = ret or (data[offset + 2].toLong() and 0xFF)
|
||||||
|
return Arg(ret, 3)
|
||||||
|
}
|
||||||
|
if (arg == 26L) {
|
||||||
|
var ret = (data[offset + 1].toLong() and 0xFF) shl 24
|
||||||
|
ret = ret or ((data[offset + 2].toLong() and 0xFF) shl 16)
|
||||||
|
ret = ret or ((data[offset + 3].toLong() and 0xFF) shl 8)
|
||||||
|
ret = ret or (data[offset + 4].toLong() and 0xFF)
|
||||||
|
return Arg(ret, 5)
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Bad arg")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseItem(data: ByteArray, offset: Int): Item {
|
||||||
|
val itemType = getType(data, offset)
|
||||||
|
val arg = getArg(data, offset)
|
||||||
|
println("Type $itemType ${arg.arg} ${arg.len}")
|
||||||
|
|
||||||
|
when (itemType) {
|
||||||
|
TYPE_UNSIGNED_INT -> {
|
||||||
|
return Item(arg.arg, arg.len)
|
||||||
|
}
|
||||||
|
TYPE_NEGATIVE_INT -> {
|
||||||
|
return Item(-1 - arg.arg, arg.len)
|
||||||
|
}
|
||||||
|
TYPE_BYTE_STRING -> {
|
||||||
|
val ret =
|
||||||
|
data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
|
||||||
|
return Item(ret, arg.len + arg.arg.toInt())
|
||||||
|
}
|
||||||
|
TYPE_TEXT_STRING -> {
|
||||||
|
val ret =
|
||||||
|
data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
|
||||||
|
return Item(ret.toString(Charsets.UTF_8), arg.len + arg.arg.toInt())
|
||||||
|
}
|
||||||
|
TYPE_ARRAY -> {
|
||||||
|
val ret = mutableListOf<Any>()
|
||||||
|
var consumed = arg.len
|
||||||
|
for (i in 0 until arg.arg.toInt()) {
|
||||||
|
val item = parseItem(data, offset + consumed)
|
||||||
|
ret.add(item.item)
|
||||||
|
consumed += item.len
|
||||||
|
}
|
||||||
|
return Item(ret.toList(), consumed)
|
||||||
|
}
|
||||||
|
TYPE_MAP -> {
|
||||||
|
val ret = mutableMapOf<Any, Any>()
|
||||||
|
var consumed = arg.len
|
||||||
|
for (i in 0 until arg.arg.toInt()) {
|
||||||
|
val key = parseItem(data, offset + consumed)
|
||||||
|
consumed += key.len
|
||||||
|
val value = parseItem(data, offset + consumed)
|
||||||
|
consumed += value.len
|
||||||
|
ret[key.item] = value.item
|
||||||
|
}
|
||||||
|
return Item(ret.toMap(), consumed)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw IllegalArgumentException("Bad type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createArg(type: Int, arg: Long): ByteArray {
|
||||||
|
val t = type shl 5
|
||||||
|
val a = arg.toInt()
|
||||||
|
if (arg < 24) {
|
||||||
|
return byteArrayOf(((t or a) and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
if (arg <= 0xFF) {
|
||||||
|
return byteArrayOf(((t or 24) and 0xFF).toByte(), (a and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
if (arg <= 0xFFFF) {
|
||||||
|
return byteArrayOf(
|
||||||
|
((t or 25) and 0xFF).toByte(),
|
||||||
|
((a shr 8) and 0xFF).toByte(),
|
||||||
|
(a and 0xFF).toByte()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (arg <= 0xFFFFFFFF) {
|
||||||
|
return byteArrayOf(
|
||||||
|
((t or 26) and 0xFF).toByte(),
|
||||||
|
((a shr 24) and 0xFF).toByte(),
|
||||||
|
((a shr 16) and 0xFF).toByte(),
|
||||||
|
((a shr 8) and 0xFF).toByte(),
|
||||||
|
(a and 0xFF).toByte()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("bad Arg")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.HashManager
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
open class ClientDataBuildResponse(
|
||||||
|
type: Type,
|
||||||
|
challenge: ByteArray,
|
||||||
|
origin: String,
|
||||||
|
crossOrigin: Boolean? = false,
|
||||||
|
topOrigin: String? = null,
|
||||||
|
): AuthenticatorResponse, ClientDataResponse {
|
||||||
|
override var clientJson = JSONObject()
|
||||||
|
|
||||||
|
init {
|
||||||
|
// https://w3c.github.io/webauthn/#client-data
|
||||||
|
clientJson.put("type", type.value)
|
||||||
|
clientJson.put("challenge", b64Encode(challenge))
|
||||||
|
clientJson.put("origin", origin)
|
||||||
|
crossOrigin?.let {
|
||||||
|
clientJson.put("crossOrigin", it)
|
||||||
|
}
|
||||||
|
topOrigin?.let {
|
||||||
|
clientJson.put("topOrigin", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun json(): JSONObject {
|
||||||
|
return clientJson
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Type(val value: String) {
|
||||||
|
GET("webauthn.get"), CREATE("webauthn.create")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildResponse(): String {
|
||||||
|
return b64Encode(json().toString().toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashData(): ByteArray {
|
||||||
|
return HashManager.hashSha256(json().toString().toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
open class ClientDataDefinedResponse(
|
||||||
|
private val clientDataHash: ByteArray
|
||||||
|
): ClientDataResponse {
|
||||||
|
|
||||||
|
override fun hashData(): ByteArray {
|
||||||
|
return clientDataHash
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildResponse(): String {
|
||||||
|
return CLIENT_DATA_JSON_PRIVILEGED
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CLIENT_DATA_JSON_PRIVILEGED = "<placeholder>"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
interface ClientDataResponse {
|
||||||
|
fun hashData(): ByteArray
|
||||||
|
fun buildResponse(): String
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
data class PublicKeyCredentialRpEntity(val name: String, val id: String)
|
||||||
|
|
||||||
|
data class PublicKeyCredentialUserEntity(
|
||||||
|
val name: String,
|
||||||
|
val id: ByteArray,
|
||||||
|
val displayName: String
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PublicKeyCredentialUserEntity
|
||||||
|
|
||||||
|
if (name != other.name) return false
|
||||||
|
if (!id.contentEquals(other.id)) return false
|
||||||
|
if (displayName != other.displayName) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = name.hashCode()
|
||||||
|
result = 31 * result + id.contentHashCode()
|
||||||
|
result = 31 * result + displayName.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PublicKeyCredentialParameters(val type: String, val alg: Long)
|
||||||
|
|
||||||
|
data class PublicKeyCredentialDescriptor(
|
||||||
|
val type: String,
|
||||||
|
val id: ByteArray,
|
||||||
|
val transports: List<String>
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PublicKeyCredentialDescriptor
|
||||||
|
|
||||||
|
if (type != other.type) return false
|
||||||
|
if (!id.contentEquals(other.id)) return false
|
||||||
|
if (transports != other.transports) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = type.hashCode()
|
||||||
|
result = 31 * result + id.contentHashCode()
|
||||||
|
result = 31 * result + transports.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AuthenticatorSelectionCriteria(
|
||||||
|
val authenticatorAttachment: String,
|
||||||
|
val residentKey: String,
|
||||||
|
val requireResidentKey: Boolean = false,
|
||||||
|
val userVerification: String = "preferred"
|
||||||
|
)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class FidoPublicKeyCredential(
|
||||||
|
val id: String,
|
||||||
|
val response: AuthenticatorResponse,
|
||||||
|
val authenticatorAttachment: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun json(): String {
|
||||||
|
// see at https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
|
||||||
|
val discoverableCredential = true
|
||||||
|
val rk = JSONObject()
|
||||||
|
rk.put("rk", discoverableCredential)
|
||||||
|
val credProps = JSONObject()
|
||||||
|
credProps.put("credProps", rk)
|
||||||
|
|
||||||
|
// See RegistrationResponseJSON at
|
||||||
|
// https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
|
||||||
|
val ret = JSONObject()
|
||||||
|
ret.put("id", id)
|
||||||
|
ret.put("rawId", id)
|
||||||
|
ret.put("type", "public-key")
|
||||||
|
ret.put("authenticatorAttachment", authenticatorAttachment)
|
||||||
|
ret.put("response", response.json())
|
||||||
|
ret.put("clientExtensionResults", JSONObject()) // TODO credProps
|
||||||
|
|
||||||
|
return ret.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class PublicKeyCredentialCreationOptions(
|
||||||
|
requestJson: String,
|
||||||
|
var clientDataHash: ByteArray?
|
||||||
|
) {
|
||||||
|
val json: JSONObject = JSONObject(requestJson)
|
||||||
|
|
||||||
|
val relyingPartyEntity: PublicKeyCredentialRpEntity
|
||||||
|
val userEntity: PublicKeyCredentialUserEntity
|
||||||
|
val challenge: ByteArray
|
||||||
|
val pubKeyCredParams: List<PublicKeyCredentialParameters>
|
||||||
|
|
||||||
|
var timeout: Long
|
||||||
|
var excludeCredentials: List<PublicKeyCredentialDescriptor>
|
||||||
|
var authenticatorSelection: AuthenticatorSelectionCriteria
|
||||||
|
var attestation: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
val rpJson = json.getJSONObject("rp")
|
||||||
|
relyingPartyEntity = PublicKeyCredentialRpEntity(rpJson.getString("name"), rpJson.getString("id"))
|
||||||
|
val rpUser = json.getJSONObject("user")
|
||||||
|
val userId = Base64Helper.b64Decode(rpUser.getString("id"))
|
||||||
|
userEntity =
|
||||||
|
PublicKeyCredentialUserEntity(
|
||||||
|
rpUser.getString("name"),
|
||||||
|
userId,
|
||||||
|
rpUser.getString("displayName")
|
||||||
|
)
|
||||||
|
challenge = Base64Helper.b64Decode(json.getString("challenge"))
|
||||||
|
val pubKeyCredParamsJson = json.getJSONArray("pubKeyCredParams")
|
||||||
|
val pubKeyCredParamsTmp: MutableList<PublicKeyCredentialParameters> = mutableListOf()
|
||||||
|
for (i in 0 until pubKeyCredParamsJson.length()) {
|
||||||
|
val e = pubKeyCredParamsJson.getJSONObject(i)
|
||||||
|
pubKeyCredParamsTmp.add(
|
||||||
|
PublicKeyCredentialParameters(e.getString("type"), e.getLong("alg"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pubKeyCredParams = pubKeyCredParamsTmp.toList()
|
||||||
|
|
||||||
|
timeout = json.optLong("timeout", 0)
|
||||||
|
// TODO: Fix excludeCredentials and authenticatorSelection
|
||||||
|
excludeCredentials = emptyList()
|
||||||
|
authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required")
|
||||||
|
attestation = json.optString("attestation", "none")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PublicKeyCredentialCreationOptions::class.simpleName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import java.security.KeyPair
|
||||||
|
|
||||||
|
data class PublicKeyCredentialCreationParameters(
|
||||||
|
val publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions,
|
||||||
|
val credentialId: ByteArray,
|
||||||
|
val signatureKey: Pair<KeyPair, Long>,
|
||||||
|
val clientDataResponse: ClientDataResponse
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PublicKeyCredentialCreationParameters
|
||||||
|
|
||||||
|
if (publicKeyCredentialCreationOptions != other.publicKeyCredentialCreationOptions) return false
|
||||||
|
if (!credentialId.contentEquals(other.credentialId)) return false
|
||||||
|
if (signatureKey != other.signatureKey) return false
|
||||||
|
if (clientDataResponse != other.clientDataResponse) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = publicKeyCredentialCreationOptions.hashCode()
|
||||||
|
result = 31 * result + credentialId.contentHashCode()
|
||||||
|
result = 31 * result + signatureKey.hashCode()
|
||||||
|
result = 31 * result + clientDataResponse.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class PublicKeyCredentialRequestOptions(requestJson: String) {
|
||||||
|
val json: JSONObject = JSONObject(requestJson)
|
||||||
|
val challenge: ByteArray = Base64Helper.b64Decode(json.getString("challenge"))
|
||||||
|
val timeout: Long = json.optLong("timeout", 0)
|
||||||
|
val rpId: String = json.optString("rpId", "")
|
||||||
|
val userVerification: String = json.optString("userVerification", "preferred")
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
|
||||||
|
data class PublicKeyCredentialUsageParameters(
|
||||||
|
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
val clientDataResponse: ClientDataResponse,
|
||||||
|
var appOrigin: AppOrigin
|
||||||
|
)
|
||||||
@@ -0,0 +1,604 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||||
|
import androidx.credentials.CreatePublicKeyCredentialResponse
|
||||||
|
import androidx.credentials.GetPublicKeyCredentialOption
|
||||||
|
import androidx.credentials.PublicKeyCredential
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.CallingAppInfo
|
||||||
|
import androidx.credentials.provider.PendingIntentHandler
|
||||||
|
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||||
|
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import com.kunzisoft.encrypt.Signature
|
||||||
|
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataBuildResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataDefinedResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.FidoPublicKeyCredential
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.getOriginFromPrivilegedAllowLists
|
||||||
|
import com.kunzisoft.keepass.model.AndroidOrigin
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil
|
||||||
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.IOException
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to manage the passkey elements,
|
||||||
|
* allows to add and retrieve intent values with preconfigured keys,
|
||||||
|
* and makes it easy to create creation and usage requests
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
object PasskeyHelper {
|
||||||
|
|
||||||
|
private const val EXTRA_PASSKEY = "com.kunzisoft.keepass.passkey.extra.passkey"
|
||||||
|
|
||||||
|
private const val HMAC_TYPE = "HmacSHA256"
|
||||||
|
|
||||||
|
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
||||||
|
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
|
||||||
|
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
|
||||||
|
|
||||||
|
private const val SEPARATOR = "_"
|
||||||
|
|
||||||
|
private const val NAME_OF_HMAC_KEY = "KeePassDXCredentialProviderHMACKey"
|
||||||
|
|
||||||
|
private const val KEYSTORE_TYPE = "AndroidKeyStore"
|
||||||
|
|
||||||
|
private val PLACEHOLDER_FOR_NEW_NODE_ID = "0".repeat(32)
|
||||||
|
|
||||||
|
private val REGEX_TIMESTAMP = "[0-9]{10}".toRegex()
|
||||||
|
private val REGEX_AUTHENTICATION_CODE = "[A-F0-9]{64}".toRegex() // 256 bits = 64 hex chars
|
||||||
|
|
||||||
|
private const val MAX_DIFF_IN_SECONDS = 60
|
||||||
|
|
||||||
|
private val internalSecureRandom: SecureRandom = SecureRandom()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an authentication code generated by an entry to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addAuthCode(passkeyEntryNodeId: UUID? = null) {
|
||||||
|
putExtras(Bundle().apply {
|
||||||
|
val timestamp = Instant.now().epochSecond
|
||||||
|
putString(EXTRA_TIMESTAMP, timestamp.toString())
|
||||||
|
putString(
|
||||||
|
EXTRA_AUTHENTICATION_CODE,
|
||||||
|
generatedAuthenticationCode(
|
||||||
|
passkeyEntryNodeId, timestamp
|
||||||
|
).toHexString()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the passkey to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addPasskey(passkey: Passkey?) {
|
||||||
|
passkey?.let {
|
||||||
|
putExtra(EXTRA_PASSKEY, passkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the passkey from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrievePasskey(): Passkey? {
|
||||||
|
return this.getParcelableExtraCompat(EXTRA_PASSKEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the passkey from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.removePasskey() {
|
||||||
|
return this.removeExtra(EXTRA_PASSKEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the app origin to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addAppOrigin(appOrigin: AppOrigin?) {
|
||||||
|
appOrigin?.let {
|
||||||
|
putExtra(EXTRA_APP_ORIGIN, appOrigin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the app origin from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveAppOrigin(): AppOrigin? {
|
||||||
|
return this.getParcelableExtraCompat(EXTRA_APP_ORIGIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the app origin from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.removeAppOrigin() {
|
||||||
|
return this.removeExtra(EXTRA_APP_ORIGIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Passkey response for one entry
|
||||||
|
*/
|
||||||
|
fun Activity.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
entryInfo.passkey?.let { passkey ->
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(javaClass.name, "Success Passkey manual selection")
|
||||||
|
mReplyIntent.addPasskey(passkey)
|
||||||
|
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
|
||||||
|
mReplyIntent.addNodeId(entryInfo.id)
|
||||||
|
extras?.let {
|
||||||
|
mReplyIntent.putExtras(it)
|
||||||
|
}
|
||||||
|
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
} ?: run {
|
||||||
|
throw IOException("No passkey found")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.name, "Unable to add the passkey as result", e)
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
getString(R.string.error_passkey_result),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the timestamp and authentication code transmitted via PendingIntent
|
||||||
|
*/
|
||||||
|
fun checkSecurity(intent: Intent, nodeId: UUID?) {
|
||||||
|
val timestampString = intent.getStringExtra(EXTRA_TIMESTAMP)
|
||||||
|
if (timestampString.isNullOrEmpty())
|
||||||
|
throw CreateCredentialUnknownException("Timestamp null")
|
||||||
|
if (timestampString.matches(REGEX_TIMESTAMP).not()) {
|
||||||
|
throw CreateCredentialUnknownException("Timestamp not valid")
|
||||||
|
}
|
||||||
|
val timestamp = timestampString.toLong()
|
||||||
|
val diff = Instant.now().epochSecond - timestamp
|
||||||
|
if (diff < 0 || diff > MAX_DIFF_IN_SECONDS) {
|
||||||
|
throw CreateCredentialUnknownException("Out of time")
|
||||||
|
}
|
||||||
|
verifyAuthenticationCode(
|
||||||
|
intent.getStringExtra(EXTRA_AUTHENTICATION_CODE),
|
||||||
|
generatedAuthenticationCode(nodeId, timestamp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the authentication code from the encrypted message received from the intent
|
||||||
|
*/
|
||||||
|
private fun verifyAuthenticationCode(
|
||||||
|
valueToCheck: String?,
|
||||||
|
authenticationCode: ByteArray
|
||||||
|
) {
|
||||||
|
if (valueToCheck.isNullOrEmpty())
|
||||||
|
throw CreateCredentialUnknownException("Authentication code empty")
|
||||||
|
if (valueToCheck.matches(REGEX_AUTHENTICATION_CODE).not())
|
||||||
|
throw CreateCredentialUnknownException("Authentication not valid")
|
||||||
|
if (MessageDigest.isEqual(authenticationCode, generateAuthenticationCode(valueToCheck)))
|
||||||
|
throw CreateCredentialUnknownException("Authentication code incorrect")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the authentication code base on the entry [nodeId] and [timestamp]
|
||||||
|
*/
|
||||||
|
private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray {
|
||||||
|
return generateAuthenticationCode(
|
||||||
|
(nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the authentication code base on the entry [message]
|
||||||
|
*/
|
||||||
|
private fun generateAuthenticationCode(message: String): ByteArray {
|
||||||
|
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||||
|
keyStore.load(null)
|
||||||
|
val hmacKey = try {
|
||||||
|
keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// key not found
|
||||||
|
generateKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
val mac = Mac.getInstance(HMAC_TYPE)
|
||||||
|
mac.init(hmacKey)
|
||||||
|
val authenticationCode = mac.doFinal(message.toByteArray())
|
||||||
|
return authenticationCode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the HMAC key if cannot be found in the KeyStore
|
||||||
|
*/
|
||||||
|
private fun generateKey(): SecretKey? {
|
||||||
|
val keyGenerator = KeyGenerator.getInstance(
|
||||||
|
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE
|
||||||
|
)
|
||||||
|
val keySizeInBits = 128
|
||||||
|
keyGenerator.init(
|
||||||
|
KeyGenParameterSpec.Builder(NAME_OF_HMAC_KEY, KeyProperties.PURPOSE_SIGN)
|
||||||
|
.setKeySize(keySizeInBits)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
val key = keyGenerator.generateKey()
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the [PublicKeyCredentialCreationOptions] from the intent
|
||||||
|
*/
|
||||||
|
fun ProviderCreateCredentialRequest.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
|
||||||
|
val request = this
|
||||||
|
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
|
||||||
|
throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
|
||||||
|
}
|
||||||
|
val createPublicKeyCredentialRequest = request.callingRequest as CreatePublicKeyCredentialRequest
|
||||||
|
return PublicKeyCredentialCreationOptions(
|
||||||
|
requestJson = createPublicKeyCredentialRequest.requestJson,
|
||||||
|
clientDataHash = createPublicKeyCredentialRequest.clientDataHash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the [GetPublicKeyCredentialOption] from the intent
|
||||||
|
*/
|
||||||
|
fun ProviderGetCredentialRequest.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
|
||||||
|
val request = this
|
||||||
|
if (request.credentialOptions.size != 1) {
|
||||||
|
throw GetCredentialUnknownException("not exact one credentialOption")
|
||||||
|
}
|
||||||
|
if (request.credentialOptions[0] !is GetPublicKeyCredentialOption) {
|
||||||
|
throw CreateCredentialUnknownException("credentialOptions is of wrong type: ${request.credentialOptions[0]}")
|
||||||
|
}
|
||||||
|
return request.credentialOptions[0] as GetPublicKeyCredentialOption
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to retrieve the origin asynchronously,
|
||||||
|
* checks for the presence of the application in the privilege lists
|
||||||
|
*
|
||||||
|
* @param providedClientDataHash Client data hash precalculated by the system
|
||||||
|
* @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
|
||||||
|
* @param context Context for file operations.
|
||||||
|
* call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list, return the clientDataHash
|
||||||
|
* call [onOriginNotRetrieved] if the origin is not retrieved from the system, return a new Android Origin
|
||||||
|
*/
|
||||||
|
suspend fun getOrigin(
|
||||||
|
providedClientDataHash: ByteArray?,
|
||||||
|
callingAppInfo: CallingAppInfo?,
|
||||||
|
context: Context,
|
||||||
|
onOriginRetrieved: suspend (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
|
||||||
|
onOriginNotRetrieved: suspend (appOrigin: AppOrigin, androidOriginString: String) -> Unit
|
||||||
|
) {
|
||||||
|
if (callingAppInfo == null) {
|
||||||
|
throw SecurityException("Calling app info cannot be retrieved")
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
|
// For trusted browsers like Chrome and Firefox
|
||||||
|
val callOrigin = try {
|
||||||
|
getOriginFromPrivilegedAllowLists(callingAppInfo, context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Throw the Privileged Exception only if it's a browser
|
||||||
|
if (e is PrivilegedAllowLists.PrivilegedException
|
||||||
|
&& AppUtil.getInstalledBrowsersWithSignatures(context).any {
|
||||||
|
it.packageName == e.temptingApp.packageName
|
||||||
|
}
|
||||||
|
) throw e
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the default Android origin
|
||||||
|
val androidOrigin = AndroidOrigin(
|
||||||
|
packageName = callingAppInfo.packageName,
|
||||||
|
fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if the webDomain is validated by the system
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (callOrigin != null && providedClientDataHash != null) {
|
||||||
|
// Origin already defined by the system
|
||||||
|
Log.d(javaClass.simpleName, "Origin $callOrigin retrieved from callingAppInfo")
|
||||||
|
onOriginRetrieved(
|
||||||
|
AppOrigin.fromOrigin(callOrigin, androidOrigin, verified = true),
|
||||||
|
providedClientDataHash
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Add Android origin by default
|
||||||
|
onOriginNotRetrieved(
|
||||||
|
AppOrigin(verified = false).apply {
|
||||||
|
addAndroidOrigin(androidOrigin)
|
||||||
|
},
|
||||||
|
androidOrigin.toOriginValue()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a credential id randomly
|
||||||
|
*/
|
||||||
|
private fun generateCredentialId(): ByteArray {
|
||||||
|
// see https://w3c.github.io/webauthn/#credential-id
|
||||||
|
val size = 16
|
||||||
|
val credentialId = ByteArray(size)
|
||||||
|
internalSecureRandom.nextBytes(credentialId)
|
||||||
|
return credentialId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to create a passkey and the associated creation request parameters
|
||||||
|
* [intent] allows to retrieve the request
|
||||||
|
* [context] context to manage package verification files
|
||||||
|
* [defaultBackupEligibility] the default backup eligibility to add the the passkey entry
|
||||||
|
* [defaultBackupState] the default backup state to add the the passkey entry
|
||||||
|
* [passkeyCreated] is called asynchronously when the passkey has been created
|
||||||
|
*/
|
||||||
|
suspend fun retrievePasskeyCreationRequestParameters(
|
||||||
|
intent: Intent,
|
||||||
|
context: Context,
|
||||||
|
defaultBackupEligibility: Boolean?,
|
||||||
|
defaultBackupState: Boolean?,
|
||||||
|
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
|
||||||
|
) {
|
||||||
|
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||||
|
if (createCredentialRequest == null)
|
||||||
|
throw CreateCredentialUnknownException("could not retrieve request from intent")
|
||||||
|
val callingAppInfo = createCredentialRequest.callingAppInfo
|
||||||
|
val creationOptions = createCredentialRequest.retrievePasskeyCreationComponent()
|
||||||
|
|
||||||
|
val relyingParty = creationOptions.relyingPartyEntity.id
|
||||||
|
val username = creationOptions.userEntity.name
|
||||||
|
val userHandle = creationOptions.userEntity.id
|
||||||
|
val pubKeyCredParams = creationOptions.pubKeyCredParams
|
||||||
|
val clientDataHash = creationOptions.clientDataHash
|
||||||
|
|
||||||
|
val credentialId = generateCredentialId()
|
||||||
|
|
||||||
|
val (keyPair, keyTypeId) = Signature.generateKeyPair(
|
||||||
|
pubKeyCredParams.map { params -> params.alg }
|
||||||
|
) ?: throw CreateCredentialUnknownException("no known public key type found")
|
||||||
|
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
|
||||||
|
|
||||||
|
// Create the passkey element
|
||||||
|
val passkey = Passkey(
|
||||||
|
username = username,
|
||||||
|
privateKeyPem = privateKeyPem,
|
||||||
|
credentialId = b64Encode(credentialId),
|
||||||
|
userHandle = b64Encode(userHandle),
|
||||||
|
relyingParty = relyingParty,
|
||||||
|
backupEligibility = defaultBackupEligibility,
|
||||||
|
backupState = defaultBackupState
|
||||||
|
)
|
||||||
|
|
||||||
|
// create new entry in database
|
||||||
|
getOrigin(
|
||||||
|
providedClientDataHash = clientDataHash,
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
context = context,
|
||||||
|
onOriginRetrieved = { appInfoToStore, clientDataHash ->
|
||||||
|
passkeyCreated.invoke(
|
||||||
|
passkey,
|
||||||
|
appInfoToStore,
|
||||||
|
PublicKeyCredentialCreationParameters(
|
||||||
|
publicKeyCredentialCreationOptions = creationOptions,
|
||||||
|
credentialId = credentialId,
|
||||||
|
signatureKey = Pair(keyPair, keyTypeId),
|
||||||
|
clientDataResponse = ClientDataDefinedResponse(clientDataHash)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onOriginNotRetrieved = { appInfoToStore, origin ->
|
||||||
|
passkeyCreated.invoke(
|
||||||
|
passkey,
|
||||||
|
appInfoToStore,
|
||||||
|
PublicKeyCredentialCreationParameters(
|
||||||
|
publicKeyCredentialCreationOptions = creationOptions,
|
||||||
|
credentialId = credentialId,
|
||||||
|
signatureKey = Pair(keyPair, keyTypeId),
|
||||||
|
clientDataResponse = ClientDataBuildResponse(
|
||||||
|
type = ClientDataBuildResponse.Type.CREATE,
|
||||||
|
challenge = creationOptions.challenge,
|
||||||
|
origin = origin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the passkey public key credential response,
|
||||||
|
* by calling this method the user is always recognized as present and verified
|
||||||
|
*/
|
||||||
|
fun buildCreatePublicKeyCredentialResponse(
|
||||||
|
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters,
|
||||||
|
backupEligibility: Boolean,
|
||||||
|
backupState: Boolean
|
||||||
|
): CreatePublicKeyCredentialResponse {
|
||||||
|
|
||||||
|
val keyPair = publicKeyCredentialCreationParameters.signatureKey.first
|
||||||
|
val keyTypeId = publicKeyCredentialCreationParameters.signatureKey.second
|
||||||
|
val responseJson = FidoPublicKeyCredential(
|
||||||
|
id = b64Encode(publicKeyCredentialCreationParameters.credentialId),
|
||||||
|
response = AuthenticatorAttestationResponse(
|
||||||
|
requestOptions = publicKeyCredentialCreationParameters.publicKeyCredentialCreationOptions,
|
||||||
|
credentialId = publicKeyCredentialCreationParameters.credentialId,
|
||||||
|
credentialPublicKey = Cbor().encode(
|
||||||
|
Signature.convertPublicKeyToMap(
|
||||||
|
publicKeyIn = keyPair.public,
|
||||||
|
keyTypeId = keyTypeId
|
||||||
|
) ?: mapOf<Int, Any>()),
|
||||||
|
userPresent = true,
|
||||||
|
userVerified = true,
|
||||||
|
backupEligibility = backupEligibility,
|
||||||
|
backupState = backupState,
|
||||||
|
publicKeyTypeId = keyTypeId,
|
||||||
|
publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!,
|
||||||
|
clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse
|
||||||
|
),
|
||||||
|
authenticatorAttachment = "platform"
|
||||||
|
).json()
|
||||||
|
// log only the length to prevent logging sensitive information
|
||||||
|
Log.d(javaClass.simpleName, "Json response for key creation")
|
||||||
|
return CreatePublicKeyCredentialResponse(responseJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to use a passkey and create the associated usage request parameters
|
||||||
|
* [intent] allows to retrieve the request
|
||||||
|
* [context] context to manage package verification files
|
||||||
|
* [result] is called asynchronously after the creation of PublicKeyCredentialUsageParameters, the origin associated with it may or may not be verified
|
||||||
|
*/
|
||||||
|
suspend fun retrievePasskeyUsageRequestParameters(
|
||||||
|
intent: Intent,
|
||||||
|
context: Context,
|
||||||
|
result: suspend (PublicKeyCredentialUsageParameters) -> Unit
|
||||||
|
) {
|
||||||
|
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||||
|
if (getCredentialRequest == null)
|
||||||
|
throw CreateCredentialUnknownException("could not retrieve request from intent")
|
||||||
|
val callingAppInfo = getCredentialRequest.callingAppInfo
|
||||||
|
val credentialOption = getCredentialRequest.retrievePasskeyUsageComponent()
|
||||||
|
val clientDataHash = credentialOption.clientDataHash
|
||||||
|
|
||||||
|
val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
|
||||||
|
|
||||||
|
getOrigin(
|
||||||
|
providedClientDataHash = clientDataHash,
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
context = context,
|
||||||
|
onOriginRetrieved = { appOrigin, clientDataHash ->
|
||||||
|
result.invoke(
|
||||||
|
PublicKeyCredentialUsageParameters(
|
||||||
|
publicKeyCredentialRequestOptions = requestOptions,
|
||||||
|
clientDataResponse = ClientDataDefinedResponse(clientDataHash),
|
||||||
|
appOrigin = appOrigin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onOriginNotRetrieved = { appOrigin, androidOriginString ->
|
||||||
|
// By default we crate an usage parameter with Android origin
|
||||||
|
result.invoke(
|
||||||
|
PublicKeyCredentialUsageParameters(
|
||||||
|
publicKeyCredentialRequestOptions = requestOptions,
|
||||||
|
clientDataResponse = ClientDataBuildResponse(
|
||||||
|
type = ClientDataBuildResponse.Type.GET,
|
||||||
|
challenge = requestOptions.challenge,
|
||||||
|
origin = androidOriginString
|
||||||
|
),
|
||||||
|
appOrigin = appOrigin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the passkey public key credential response,
|
||||||
|
* by calling this method the user is always recognized as present and verified
|
||||||
|
*/
|
||||||
|
fun buildPasskeyPublicKeyCredential(
|
||||||
|
requestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
clientDataResponse: ClientDataResponse,
|
||||||
|
passkey: Passkey,
|
||||||
|
defaultBackupEligibility: Boolean,
|
||||||
|
defaultBackupState: Boolean
|
||||||
|
): PublicKeyCredential {
|
||||||
|
val getCredentialResponse = FidoPublicKeyCredential(
|
||||||
|
id = passkey.credentialId,
|
||||||
|
response = AuthenticatorAssertionResponse(
|
||||||
|
requestOptions = requestOptions,
|
||||||
|
userPresent = true,
|
||||||
|
userVerified = true,
|
||||||
|
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
|
||||||
|
backupState = passkey.backupState ?: defaultBackupState,
|
||||||
|
userHandle = passkey.userHandle,
|
||||||
|
privateKey = passkey.privateKeyPem,
|
||||||
|
clientDataResponse = clientDataResponse
|
||||||
|
),
|
||||||
|
authenticatorAttachment = "platform"
|
||||||
|
).json()
|
||||||
|
Log.d(javaClass.simpleName, "Json response for key usage")
|
||||||
|
return PublicKeyCredential(getCredentialResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the application signature is contained in the [appOrigin]
|
||||||
|
*/
|
||||||
|
fun getVerifiedGETClientDataResponse(
|
||||||
|
usageParameters: PublicKeyCredentialUsageParameters,
|
||||||
|
appOrigin: AppOrigin
|
||||||
|
): ClientDataResponse {
|
||||||
|
val appToCheck = usageParameters.appOrigin
|
||||||
|
return if (appToCheck.verified) {
|
||||||
|
usageParameters.clientDataResponse
|
||||||
|
} else {
|
||||||
|
// Origin checked by Android app signature
|
||||||
|
ClientDataBuildResponse(
|
||||||
|
type = ClientDataBuildResponse.Type.GET,
|
||||||
|
challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
|
||||||
|
origin = appToCheck.checkAppOrigin(appOrigin)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user