mirror of
https://github.com/bitwarden/android.git
synced 2026-05-09 13:29:18 -05:00
Compare commits
596 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d192065054 | ||
|
|
5f40ac3004 | ||
|
|
7746b6c0c7 | ||
|
|
6160402186 | ||
|
|
a35ec8cf3c | ||
|
|
ae8db9256c | ||
|
|
688dd3a39b | ||
|
|
6223f362c3 | ||
|
|
1148e4821c | ||
|
|
f32eecc0d7 | ||
|
|
2ba516f50f | ||
|
|
c4c7af54ff | ||
|
|
ae0bf6318d | ||
|
|
3be2242431 | ||
|
|
d9c0911238 | ||
|
|
5aa8369ac5 | ||
|
|
35e8cecdcf | ||
|
|
a7939414ae | ||
|
|
6c355ae5b7 | ||
|
|
843247b02d | ||
|
|
efbb8446e3 | ||
|
|
a279a2b1a3 | ||
|
|
3329dfaf20 | ||
|
|
b615bfa664 | ||
|
|
f6bd467ec8 | ||
|
|
8548c74d58 | ||
|
|
e2b93ec08c | ||
|
|
e565a5a118 | ||
|
|
3a41138f39 | ||
|
|
889457ae96 | ||
|
|
be88cdf42e | ||
|
|
e37cefeb1d | ||
|
|
ae20e55b1a | ||
|
|
bd29e1738c | ||
|
|
f28f5ee688 | ||
|
|
c92c334e03 | ||
|
|
fb8f260a94 | ||
|
|
9f5e97b8c9 | ||
|
|
1aec94ee7d | ||
|
|
d4b153107a | ||
|
|
d405a0d04b | ||
|
|
86587258c9 | ||
|
|
7571d35fb3 | ||
|
|
c7bef56639 | ||
|
|
d5a75c2d53 | ||
|
|
e11b0c7839 | ||
|
|
a6929f2d8d | ||
|
|
9b064eea90 | ||
|
|
d9ef87e21f | ||
|
|
7b3ad98698 | ||
|
|
c00cdc7407 | ||
|
|
4bab5a59fc | ||
|
|
4932353003 | ||
|
|
5997579330 | ||
|
|
7abb52b42d | ||
|
|
ddfd9bd0d8 | ||
|
|
5abdf1e4b0 | ||
|
|
4234008337 | ||
|
|
7b7c2da67c | ||
|
|
12dd865cec | ||
|
|
0c53fa6c0b | ||
|
|
b2eae2c33f | ||
|
|
7da1e48aa5 | ||
|
|
10b6f533b2 | ||
|
|
6a77dbe8b5 | ||
|
|
6f7aedbcd2 | ||
|
|
97285f463e | ||
|
|
df846374f5 | ||
|
|
65ff843ada | ||
|
|
26a7876525 | ||
|
|
718cece22e | ||
|
|
ef8223bd8b | ||
|
|
382597f356 | ||
|
|
45200d0480 | ||
|
|
1534fb598b | ||
|
|
819cc625a1 | ||
|
|
7e82b6e400 | ||
|
|
02c44f514a | ||
|
|
6ef5d4b6aa | ||
|
|
cb8c1341e2 | ||
|
|
b2391dd66a | ||
|
|
bca9f5e859 | ||
|
|
b654ef1b43 | ||
|
|
348abcefe9 | ||
|
|
a96fcd944e | ||
|
|
05aa52b032 | ||
|
|
cce9befe8c | ||
|
|
8e7ec7af4c | ||
|
|
2e1845887c | ||
|
|
c1be5be188 | ||
|
|
76b6853f90 | ||
|
|
89935ac42b | ||
|
|
050b3b3007 | ||
|
|
249dbdaaf8 | ||
|
|
dbb006d745 | ||
|
|
5d4197076c | ||
|
|
1e223b1a2a | ||
|
|
b19e7e1495 | ||
|
|
1ef7e2173b | ||
|
|
bef6ffc094 | ||
|
|
06b08d595b | ||
|
|
4fb031d76c | ||
|
|
c4f13fc8bd | ||
|
|
8a534d11d2 | ||
|
|
57ea58fc3c | ||
|
|
245fcd7502 | ||
|
|
dbeb00ba1c | ||
|
|
cbfd7ad1b1 | ||
|
|
d4033a7705 | ||
|
|
96bd25eae5 | ||
|
|
ec8e934bf4 | ||
|
|
3092ba1fc6 | ||
|
|
5ea17700b3 | ||
|
|
d418444dc0 | ||
|
|
531b003347 | ||
|
|
dca88a58e7 | ||
|
|
da878a9fab | ||
|
|
95552a7a55 | ||
|
|
90b638eff0 | ||
|
|
ccd4fd9aba | ||
|
|
2d15c4864f | ||
|
|
b183f7af42 | ||
|
|
2ece0856d4 | ||
|
|
429c76ce03 | ||
|
|
506d0f13c7 | ||
|
|
6a0a7d70bc | ||
|
|
3742940290 | ||
|
|
1cb647b7ae | ||
|
|
ffeae93728 | ||
|
|
e90bd136f6 | ||
|
|
30eb11b85e | ||
|
|
a04598c77a | ||
|
|
cad2df79b6 | ||
|
|
089136552b | ||
|
|
1b0bc13903 | ||
|
|
d125fab0b7 | ||
|
|
13210343db | ||
|
|
3c4ac8b01a | ||
|
|
0a888a72c8 | ||
|
|
40f33dff89 | ||
|
|
5938e38070 | ||
|
|
31bc171d6b | ||
|
|
0967234ad8 | ||
|
|
911c9e4704 | ||
|
|
072c3a992c | ||
|
|
1e0e4831b8 | ||
|
|
e804dbd48e | ||
|
|
9a5aa217e6 | ||
|
|
c6beaec102 | ||
|
|
5a9944f79d | ||
|
|
89c267aa5d | ||
|
|
f33296b44f | ||
|
|
a8416b073e | ||
|
|
fd4a7c5716 | ||
|
|
771e719963 | ||
|
|
c5293715e1 | ||
|
|
2c40a7f105 | ||
|
|
a3ed2bc068 | ||
|
|
16cc70f344 | ||
|
|
6dd783051f | ||
|
|
1bb85d0fa0 | ||
|
|
dfeb87be10 | ||
|
|
dae50a7b88 | ||
|
|
63324fcec1 | ||
|
|
49642f5a1d | ||
|
|
016d0f889c | ||
|
|
fe84feb184 | ||
|
|
54d3b34876 | ||
|
|
b6dfc3d17b | ||
|
|
96c6b9c214 | ||
|
|
27666c193e | ||
|
|
b76f7202a4 | ||
|
|
7ccba88780 | ||
|
|
87d324b063 | ||
|
|
e397c036e4 | ||
|
|
29384596d4 | ||
|
|
88a741c93a | ||
|
|
db3490f61a | ||
|
|
4930c1032e | ||
|
|
202b4de5ca | ||
|
|
8f9585e4bc | ||
|
|
e5e0464929 | ||
|
|
c2537f329d | ||
|
|
b7ffa3966d | ||
|
|
9240fb82e4 | ||
|
|
2eb41e932b | ||
|
|
51e299998f | ||
|
|
2f6578fd5a | ||
|
|
0844939eca | ||
|
|
8f2d55c146 | ||
|
|
3e5e6ce3ab | ||
|
|
4831750ffd | ||
|
|
7d7380d622 | ||
|
|
a0b9e92ae9 | ||
|
|
ce180f1bbb | ||
|
|
c99e5ce2de | ||
|
|
eaa7923d1f | ||
|
|
56367cc14e | ||
|
|
6e0ce3b742 | ||
|
|
fab018782c | ||
|
|
0211729525 | ||
|
|
540ece5a40 | ||
|
|
78e7adfbc1 | ||
|
|
6f26ae50ea | ||
|
|
a5e57f1836 | ||
|
|
9e5fefa3ee | ||
|
|
8b16135955 | ||
|
|
a1108889cb | ||
|
|
150c8e0312 | ||
|
|
f3916b4ef6 | ||
|
|
8df4292e08 | ||
|
|
05c768610e | ||
|
|
21a5242abe | ||
|
|
deb9eb8d9b | ||
|
|
4a91d87d9d | ||
|
|
064db9fb6a | ||
|
|
c47f8606cd | ||
|
|
3e2f10a5b9 | ||
|
|
b060b70a6b | ||
|
|
7ea7d78e66 | ||
|
|
b64175ff6e | ||
|
|
164cc09f19 | ||
|
|
0960f61c37 | ||
|
|
f8bf864fc9 | ||
|
|
93aece75cf | ||
|
|
5159258de5 | ||
|
|
68a834ac14 | ||
|
|
33a430419c | ||
|
|
eb4ffebba0 | ||
|
|
53d4c4c03e | ||
|
|
e80585f77e | ||
|
|
0ff2fe6d6a | ||
|
|
b0885ff60a | ||
|
|
a55fbca16a | ||
|
|
fcd69e3e6f | ||
|
|
28e87fe216 | ||
|
|
1c10a94109 | ||
|
|
6f535c0abe | ||
|
|
bdb6136d36 | ||
|
|
2d9451cc34 | ||
|
|
6217532237 | ||
|
|
ef1e8403e1 | ||
|
|
24c8406ed8 | ||
|
|
51c87625cb | ||
|
|
fa248243b6 | ||
|
|
f1d7d1a530 | ||
|
|
79ebd2ba33 | ||
|
|
a23fc319de | ||
|
|
c5a266dfc0 | ||
|
|
fca00d38f5 | ||
|
|
65380095f0 | ||
|
|
002fd06b72 | ||
|
|
5d85060260 | ||
|
|
4dfee643a0 | ||
|
|
7aab846244 | ||
|
|
c704cd2eca | ||
|
|
09c11f4890 | ||
|
|
df6e842201 | ||
|
|
b82614e5fa | ||
|
|
27beb25bf7 | ||
|
|
d1f13e49a4 | ||
|
|
36a718753d | ||
|
|
be0ebb9b3f | ||
|
|
4fc01c77d1 | ||
|
|
258f25cd37 | ||
|
|
98c3ced191 | ||
|
|
c26a7cdf28 | ||
|
|
083578ec2b | ||
|
|
f73ce842fc | ||
|
|
56ad1ef05b | ||
|
|
5faa30e2f2 | ||
|
|
a9b6f296d8 | ||
|
|
655beb9dd6 | ||
|
|
0d6a8513b2 | ||
|
|
ab9d57b4f2 | ||
|
|
c382227b6a | ||
|
|
cf3624264e | ||
|
|
62cfd5e746 | ||
|
|
1446e43c46 | ||
|
|
43dc2f8116 | ||
|
|
8eb408b140 | ||
|
|
970a1e14cd | ||
|
|
736912bd6c | ||
|
|
ec47cb9ee2 | ||
|
|
690de93e63 | ||
|
|
9adb106a12 | ||
|
|
499ab2d2d0 | ||
|
|
12afbea83e | ||
|
|
efbf84238d | ||
|
|
2b87cdac9e | ||
|
|
8eab74d458 | ||
|
|
b465cc5078 | ||
|
|
9b5c88e990 | ||
|
|
4756040c4a | ||
|
|
bde47d7919 | ||
|
|
86db9bd3fa | ||
|
|
cd9f4e8723 | ||
|
|
879c2b9107 | ||
|
|
cdb03f5649 | ||
|
|
ba8e3a6c51 | ||
|
|
028242c4be | ||
|
|
3296477932 | ||
|
|
c3af26d83f | ||
|
|
3e9e45ba2f | ||
|
|
22c0745993 | ||
|
|
537281f6c3 | ||
|
|
79d2a00bf8 | ||
|
|
57d79cd51c | ||
|
|
57082ff7c1 | ||
|
|
8a30f14dea | ||
|
|
2af96988ab | ||
|
|
ccb52ae6c5 | ||
|
|
cda4e47414 | ||
|
|
5e7dc26837 | ||
|
|
b5658fda42 | ||
|
|
1539c2032e | ||
|
|
94791b4256 | ||
|
|
49d9a46917 | ||
|
|
e7450171cd | ||
|
|
641a48fe44 | ||
|
|
bc057932a0 | ||
|
|
0cb8e369ae | ||
|
|
3d4c901039 | ||
|
|
e62dc5dd21 | ||
|
|
f8592f4e17 | ||
|
|
c4467f0cba | ||
|
|
8d578a9b57 | ||
|
|
73a802a483 | ||
|
|
60fce08c7e | ||
|
|
8ae6433906 | ||
|
|
83652c9699 | ||
|
|
a5cf4f49d7 | ||
|
|
78d14547e4 | ||
|
|
29f00421bb | ||
|
|
8501db0eb2 | ||
|
|
c1e9759dae | ||
|
|
8f24597bad | ||
|
|
954a9acf92 | ||
|
|
3dfe6adc05 | ||
|
|
1d84479cf3 | ||
|
|
c8dcafe737 | ||
|
|
567c2ffb94 | ||
|
|
488ec095bc | ||
|
|
32f2bfb29f | ||
|
|
20383f06a8 | ||
|
|
fd6b276cc8 | ||
|
|
e6eb626d85 | ||
|
|
569ffc3583 | ||
|
|
e2e5042be5 | ||
|
|
0c83a1099f | ||
|
|
36a5fee048 | ||
|
|
01ab047d9c | ||
|
|
4fd81ed3b8 | ||
|
|
8e092ef860 | ||
|
|
9e4119fe32 | ||
|
|
757baf0290 | ||
|
|
53b1bec42b | ||
|
|
e63c4806f1 | ||
|
|
2224708fb1 | ||
|
|
b3e885bcb1 | ||
|
|
10bbab971f | ||
|
|
8ec743736a | ||
|
|
2f05355487 | ||
|
|
b7c48c2e26 | ||
|
|
1e9583b3be | ||
|
|
75819cce3c | ||
|
|
d60c534e06 | ||
|
|
ad338a8fd6 | ||
|
|
290377af74 | ||
|
|
24195ddb90 | ||
|
|
73c5571d6b | ||
|
|
5beec9d687 | ||
|
|
9d19a73fd6 | ||
|
|
72cb9918ac | ||
|
|
aa6762dc22 | ||
|
|
b696964cb7 | ||
|
|
853f25bf57 | ||
|
|
70e75b910c | ||
|
|
aaa541dd19 | ||
|
|
80c1c26f5c | ||
|
|
f349a72f72 | ||
|
|
21e1d8b5bc | ||
|
|
9213b4843f | ||
|
|
dad501d37e | ||
|
|
61b09fe8f1 | ||
|
|
415aafb5e9 | ||
|
|
251d13a832 | ||
|
|
40dd0e9776 | ||
|
|
ad154b6c91 | ||
|
|
caa0c613a5 | ||
|
|
6908111377 | ||
|
|
0f009943b5 | ||
|
|
4f34f6da21 | ||
|
|
3d0dd2996e | ||
|
|
774b828cb0 | ||
|
|
071a5330e6 | ||
|
|
86bbf13175 | ||
|
|
87e223bc59 | ||
|
|
190f92ec67 | ||
|
|
ee7d00247e | ||
|
|
f26374aae7 | ||
|
|
f68b4df9f9 | ||
|
|
05d6c2f61e | ||
|
|
8fe637d207 | ||
|
|
ad8702a34f | ||
|
|
bcdbea13bf | ||
|
|
37b5dc7de3 | ||
|
|
39cc24c13a | ||
|
|
d0c2bb5b7e | ||
|
|
5667a1cfd0 | ||
|
|
3238279290 | ||
|
|
73e158afd0 | ||
|
|
21b41fd4db | ||
|
|
7fd16c9e10 | ||
|
|
cb1e4f6b39 | ||
|
|
71d3ae9ee8 | ||
|
|
c8b1b71960 | ||
|
|
384be79838 | ||
|
|
cdf8b49d61 | ||
|
|
84f92f1b13 | ||
|
|
2068948035 | ||
|
|
f89b053d2e | ||
|
|
4b53358c67 | ||
|
|
2721f7293c | ||
|
|
1c2ccd5305 | ||
|
|
4f55d622cb | ||
|
|
74ae39a665 | ||
|
|
37f1da1ec2 | ||
|
|
503c966177 | ||
|
|
76cc9c8579 | ||
|
|
ed0494e159 | ||
|
|
fd8eda75b5 | ||
|
|
765a9ebd74 | ||
|
|
39df103f12 | ||
|
|
135a48b3c0 | ||
|
|
368ca49fd6 | ||
|
|
759e926588 | ||
|
|
3ecf1382b2 | ||
|
|
190ba792a1 | ||
|
|
d1f21d3585 | ||
|
|
4c1d55e9fe | ||
|
|
f544ccc3ef | ||
|
|
537c501b9b | ||
|
|
2d3a47f3d1 | ||
|
|
6521848a8d | ||
|
|
fe1f897d64 | ||
|
|
a5726fb72b | ||
|
|
8f30742908 | ||
|
|
98bcff5e06 | ||
|
|
19596ea4c3 | ||
|
|
8dce8cd576 | ||
|
|
b94a1adda9 | ||
|
|
8489693d84 | ||
|
|
647b3e921b | ||
|
|
4e69ed57e8 | ||
|
|
95240a7ce3 | ||
|
|
19facaf8fd | ||
|
|
bef05d5ed9 | ||
|
|
fa93985a2e | ||
|
|
b7544ef7f4 | ||
|
|
fa2d7e0218 | ||
|
|
c817253760 | ||
|
|
eb4e2ab31f | ||
|
|
705153ea21 | ||
|
|
47fe78536f | ||
|
|
3f78ad6d6d | ||
|
|
e468ec695b | ||
|
|
ae349183e8 | ||
|
|
e039d5c3fb | ||
|
|
b1ecf125d1 | ||
|
|
69ca7649e2 | ||
|
|
22958aa5ed | ||
|
|
7ebbe700f0 | ||
|
|
de48bb37d7 | ||
|
|
1dd19a8b2d | ||
|
|
733053b548 | ||
|
|
f10a2b15ba | ||
|
|
c02afb6714 | ||
|
|
b672418c45 | ||
|
|
c017c1b10c | ||
|
|
e3a4a7b153 | ||
|
|
36f13e44a3 | ||
|
|
2597c44117 | ||
|
|
ae685c73d2 | ||
|
|
2b5337c4ff | ||
|
|
7e5203efa5 | ||
|
|
17c579bfc2 | ||
|
|
3c39d8beac | ||
|
|
2a057bb1fb | ||
|
|
f778d7ecd1 | ||
|
|
4c983525d3 | ||
|
|
e32a9f303d | ||
|
|
522e3bb939 | ||
|
|
0676cf8826 | ||
|
|
5173dfd424 | ||
|
|
5bc31448b4 | ||
|
|
e9a7136a9a | ||
|
|
c36d0851ca | ||
|
|
ace5f19375 | ||
|
|
88b40cfd10 | ||
|
|
38e693f92c | ||
|
|
9dbb40f33b | ||
|
|
76a3265bbb | ||
|
|
666c165b6f | ||
|
|
9db09c18cc | ||
|
|
b7330392cc | ||
|
|
162da64567 | ||
|
|
09f497ca9b | ||
|
|
87d7143cc8 | ||
|
|
23bcfad717 | ||
|
|
f1f16cfee5 | ||
|
|
82d3b44712 | ||
|
|
b56a21b6e5 | ||
|
|
eb2ba8e598 | ||
|
|
91f039ecb6 | ||
|
|
0d6aeee870 | ||
|
|
a0a5070ac7 | ||
|
|
e7bd966e94 | ||
|
|
075956ce17 | ||
|
|
13b256d4e9 | ||
|
|
5761e9510a | ||
|
|
3b3b9ef33b | ||
|
|
17fd3ec0f0 | ||
|
|
43a6495b98 | ||
|
|
86dabea39f | ||
|
|
8d08b5f7c5 | ||
|
|
13c29c8296 | ||
|
|
eac5516a94 | ||
|
|
88b674f54c | ||
|
|
bcc24a2e25 | ||
|
|
e14f399e2d | ||
|
|
ad2c575b39 | ||
|
|
57c2e7ee4e | ||
|
|
55b57a605e | ||
|
|
397c78b4af | ||
|
|
9e372c29d1 | ||
|
|
82fd7f01f8 | ||
|
|
a15b84a5bf | ||
|
|
5f46423638 | ||
|
|
8aebd36465 | ||
|
|
b4f864d89c | ||
|
|
8c8db78da6 | ||
|
|
b18d9f53c6 | ||
|
|
7134d89352 | ||
|
|
5a7dc198dd | ||
|
|
7dbfcfdea2 | ||
|
|
b56ccd1bab | ||
|
|
f05828c87d | ||
|
|
48817f0fe4 | ||
|
|
3bed2581af | ||
|
|
acb125b2b9 | ||
|
|
72e5aedccd | ||
|
|
9148a750a5 | ||
|
|
d4600c5c83 | ||
|
|
8094b3fd22 | ||
|
|
bd55b9ce72 | ||
|
|
4726cb743a | ||
|
|
244d259804 | ||
|
|
eab94dde79 | ||
|
|
2bb921b592 | ||
|
|
18b58e75f8 | ||
|
|
e2cd3867dd | ||
|
|
524b9e9a08 | ||
|
|
4b35484abb | ||
|
|
d305dc3081 | ||
|
|
dde90a251a | ||
|
|
516cd72f66 | ||
|
|
63884e8518 | ||
|
|
8a4d436f1f | ||
|
|
ab279e2264 | ||
|
|
2876d75a21 | ||
|
|
aaa0ce4ecd | ||
|
|
499bc20850 | ||
|
|
2bed4986a1 | ||
|
|
151b081161 | ||
|
|
e3371b7620 | ||
|
|
551f948644 | ||
|
|
4bd81782c8 | ||
|
|
4dbcec85bb | ||
|
|
5a0b1caecd | ||
|
|
2b13151bd1 | ||
|
|
5e643e11fd | ||
|
|
2789b1cc37 | ||
|
|
b7a47eb91e | ||
|
|
06f6f19255 | ||
|
|
e717183239 | ||
|
|
edb87202d2 | ||
|
|
9b808058f5 | ||
|
|
89589aa907 | ||
|
|
805fea630c | ||
|
|
145f8adf0c | ||
|
|
6bb5ef7417 | ||
|
|
722726882b | ||
|
|
9ed30d7913 | ||
|
|
6c5c0c7c03 | ||
|
|
a57a7e099c |
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -5,7 +5,10 @@
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Default file owners.
|
||||
# * @bitwarden/tech-leads
|
||||
* @bitwarden/team-android @brian-livefront @david-livefront @dseverns-livefront @ahaisting-livefront @phil-livefront
|
||||
|
||||
# Actions and workflow changes.
|
||||
.github/ @bitwarden/dept-development-mobile
|
||||
|
||||
# Auth
|
||||
# app/src/main/java/com/x8bit/bitwarden/data/auth @bitwarden/team-auth-dev
|
||||
|
||||
30
.github/ISSUE_TEMPLATE/bug.yml
vendored
30
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Android Beta Bug Report
|
||||
name: Android Bug Report
|
||||
description: File a bug report
|
||||
labels: [ bug ]
|
||||
body:
|
||||
@@ -7,19 +7,7 @@ body:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
> [!WARNING]
|
||||
> This is the new native Bitwarden Beta app repository. For the publicly available apps in App Store / Play Store, submit your report in [bitwarden/mobile](https://github.com/bitwarden/mobile)
|
||||
|
||||
|
||||
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
|
||||
- type: checkboxes
|
||||
id: beta
|
||||
attributes:
|
||||
label: Bitwarden Beta
|
||||
options:
|
||||
- label: "I'm using the new native Bitwarden Beta app and I'm aware that legacy .NET app bugs should be reported in [bitwarden/mobile](https://github.com/bitwarden/mobile)"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
@@ -63,6 +51,22 @@ body:
|
||||
description: What version of our software are you running?
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: server-region
|
||||
attributes:
|
||||
label: What server are you connecting to?
|
||||
options:
|
||||
- US
|
||||
- EU
|
||||
- Self-host
|
||||
- N/A
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: server-version
|
||||
attributes:
|
||||
label: Self-host Server Version
|
||||
description: If self-hosting, what version of Bitwarden Server are you running?
|
||||
- type: textarea
|
||||
id: environment-details
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -15,3 +15,5 @@ contact_links:
|
||||
- name: Security Issues
|
||||
url: https://hackerone.com/bitwarden
|
||||
about: We use HackerOne to manage security disclosures.
|
||||
- name: Report mobile autofill failure
|
||||
url: https://docs.google.com/forms/d/e/1FAIpQLScMopHyN7KGJs8hW562VTzbIGL4KcFnx0wJcsW0GYE1BnPiGA/viewform
|
||||
|
||||
203
.github/workflows/build.yml
vendored
203
.github/workflows/build.yml
vendored
@@ -33,17 +33,17 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/actions/wrapper-validation@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -62,13 +62,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
uses: ruby/setup-ruby@2a18b06812b0e15bb916e1df298d3e740422c47e # v1.203.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -84,22 +84,29 @@ jobs:
|
||||
- name: Build
|
||||
run: bundle exec fastlane assembleDebugApks
|
||||
|
||||
- name: Upload test reports on failure
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
if: failure()
|
||||
with:
|
||||
name: test-reports
|
||||
path: app/build/reports/tests/
|
||||
|
||||
publish_playstore:
|
||||
name: Publish Play Store artifacts
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: ["prod", "qa"]
|
||||
variant: ["prod", "dev"]
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
uses: ruby/setup-ruby@2a18b06812b0e15bb916e1df298d3e740422c47e # v1.203.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -150,10 +157,10 @@ jobs:
|
||||
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/actions/wrapper-validation@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -163,7 +170,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -172,11 +179,20 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Update app CI Build info
|
||||
run: |
|
||||
./scripts/update_app_ci_build_info.sh \
|
||||
$GITHUB_REPOSITORY \
|
||||
$GITHUB_REF_NAME \
|
||||
$GITHUB_SHA \
|
||||
$GITHUB_RUN_ID \
|
||||
$GITHUB_RUN_ATTEMPT
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
|
||||
@@ -230,128 +246,128 @@ jobs:
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:${{ env.PLAY_BETA_KEY_PASSWORD }}
|
||||
|
||||
- name: Generate QA Play Store APKs
|
||||
- name: Generate debug Play Store APKs
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
bundle exec fastlane assembleDebugApks
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload beta Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload release .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk
|
||||
if-no-files-found: error
|
||||
|
||||
# When building variants other than 'prod'
|
||||
- name: Upload other .apk artifact
|
||||
- name: Upload debug .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden-${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create checksum for release .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk" \
|
||||
> ./bw-android-apk-sha256.txt
|
||||
sha256sum "app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk" \
|
||||
> ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
|
||||
- name: Create checksum for beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk" \
|
||||
> ./bw-android-beta-apk-sha256.txt
|
||||
sha256sum "app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk" \
|
||||
> ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
|
||||
- name: Create checksum for release .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab" \
|
||||
> ./bw-android-aab-sha256.txt
|
||||
sha256sum "app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab" \
|
||||
> ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
|
||||
- name: Create checksum for beta .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab" \
|
||||
> ./bw-android-beta-aab-sha256.txt
|
||||
sha256sum "app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab" \
|
||||
> ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
|
||||
- name: Create checksum for other .apk artifact
|
||||
- name: Create checksum for Debug .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk" \
|
||||
> ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
sha256sum "app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk" \
|
||||
> ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
|
||||
- name: Upload .apk SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: bw-android-apk-sha256.txt
|
||||
path: ./bw-android-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .apk SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: bw-android-beta-apk-sha256.txt
|
||||
path: ./bw-android-beta-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .aab SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: bw-android-aab-sha256.txt
|
||||
path: ./bw-android-aab-sha256.txt
|
||||
name: com.x8bit.bitwarden.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .aab SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: bw-android-beta-aab-sha256.txt
|
||||
path: ./bw-android-beta-aab-sha256.txt
|
||||
name: com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .apk SHA file for other
|
||||
- name: Upload .apk SHA file for debug
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install Firebase app distribution plugin
|
||||
if: ${{ matrix.variant == 'prod' && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
if: ${{ matrix.variant == 'prod' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
run: bundle exec fastlane add_plugin firebase_app_distribution
|
||||
|
||||
- name: Publish release artifacts to Firebase
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
env:
|
||||
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
|
||||
run: |
|
||||
@@ -360,7 +376,7 @@ jobs:
|
||||
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
|
||||
|
||||
- name: Publish beta artifacts to Firebase
|
||||
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
env:
|
||||
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
|
||||
run: |
|
||||
@@ -375,19 +391,21 @@ jobs:
|
||||
|
||||
- name: Publish Play Store bundle
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
|
||||
run: bundle exec fastlane publishBetaToPlayStore
|
||||
run: |
|
||||
bundle exec fastlane publishProdToPlayStore
|
||||
bundle exec fastlane publishBetaToPlayStore
|
||||
|
||||
publish_fdroid:
|
||||
name: Publish F-Droid artifacts
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
uses: ruby/setup-ruby@2a18b06812b0e15bb916e1df298d3e740422c47e # v1.203.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -424,10 +442,10 @@ jobs:
|
||||
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/actions/wrapper-validation@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -437,7 +455,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -446,19 +464,35 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Update app CI Build info
|
||||
run: |
|
||||
./scripts/update_app_ci_build_info.sh \
|
||||
$GITHUB_REPOSITORY \
|
||||
$GITHUB_REF_NAME \
|
||||
$GITHUB_SHA \
|
||||
$GITHUB_RUN_ID \
|
||||
$GITHUB_RUN_ATTEMPT
|
||||
|
||||
# Start from 11000 to prevent collisions with mobile build version codes
|
||||
- name: Increment version
|
||||
run: |
|
||||
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
|
||||
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
|
||||
bundle exec fastlane setBuildVersionInfo \
|
||||
versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \
|
||||
versionCode:$VERSION_CODE \
|
||||
versionName:${{ inputs.version-name || '' }}
|
||||
|
||||
regex='versionName = "([^"]+)"'
|
||||
if [[ "$(cat app/build.gradle.kts)" =~ $regex ]]; then
|
||||
VERSION_NAME="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
|
||||
- name: Generate F-Droid artifacts
|
||||
env:
|
||||
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
|
||||
@@ -469,7 +503,6 @@ jobs:
|
||||
keyAlias:bitwarden \
|
||||
keyPassword:"${{ env.FDROID_STORE_PASSWORD }}"
|
||||
|
||||
# Generate the F-Droid APK for publishing
|
||||
- name: Generate F-Droid Beta Artifacts
|
||||
env:
|
||||
FDROID_BETA_KEYSTORE_PASSWORD: ${{ secrets.FDROID_BETA_KEYSTORE_PASSWORD }}
|
||||
@@ -482,49 +515,49 @@ jobs:
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create checksum for F-Droid artifact
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk" \
|
||||
> ./bw-fdroid-apk-sha256.txt
|
||||
sha256sum "app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk" \
|
||||
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid SHA file
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: bw-fdroid-apk-sha256.txt
|
||||
path: ./bw-fdroid-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload F-Droid Beta .apk artifact
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid-beta.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create checksum for F-Droid Beta artifact
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk" \
|
||||
> ./bw-fdroid-beta-apk-sha256.txt
|
||||
sha256sum "app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk" \
|
||||
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid Beta SHA file
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: bw-fdroid-beta-apk-sha256.txt
|
||||
path: ./bw-fdroid-beta-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install Firebase app distribution plugin
|
||||
if: ${{ github.ref_name == 'main' && inputs.distribute_to_firebase }}
|
||||
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
|
||||
run: bundle exec fastlane add_plugin firebase_app_distribution
|
||||
|
||||
- name: Publish release F-Droid artifacts to Firebase
|
||||
if: ${{ github.ref_name == 'main' && inputs.distribute_to_firebase }}
|
||||
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
|
||||
env:
|
||||
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
|
||||
run: |
|
||||
|
||||
18
.github/workflows/crowdin-pull.yml
vendored
18
.github/workflows/crowdin-pull.yml
vendored
@@ -1,21 +1,20 @@
|
||||
---
|
||||
name: Crowdin Sync
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: { }
|
||||
inputs: {}
|
||||
schedule:
|
||||
- cron: '0 0 * * 5'
|
||||
|
||||
jobs:
|
||||
crowdin-sync:
|
||||
name: Autosync
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@@ -29,10 +28,17 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # v2.1.1
|
||||
uses: crowdin/github-action@a9ffb7d5ac46eca1bb1f06656bf888b39462f161 # v2.4.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
with:
|
||||
config: crowdin.yml
|
||||
|
||||
8
.github/workflows/crowdin-push.yml
vendored
8
.github/workflows/crowdin-push.yml
vendored
@@ -9,12 +9,12 @@ on:
|
||||
jobs:
|
||||
crowdin-push:
|
||||
name: Crowdin Push
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
|
||||
@@ -23,13 +23,13 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@2bd1450c2cdb2a8ac886232b8589696f22794229 # v0.2.0
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # v2.1.1
|
||||
uses: crowdin/github-action@a9ffb7d5ac46eca1bb1f06656bf888b39462f161 # v2.4.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
129
.github/workflows/github-release.yml
vendored
Normal file
129
.github/workflows/github-release.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
name: Create GitHub Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
description: 'Version Name - E.g. "2024.11.1"'
|
||||
required: true
|
||||
type: string
|
||||
version-number:
|
||||
description: 'Version Number - E.g. "123456"'
|
||||
required: true
|
||||
type: string
|
||||
artifact-run-id:
|
||||
description: 'GitHub Action Run ID containing artifacts'
|
||||
required: true
|
||||
type: string
|
||||
draft:
|
||||
description: 'Create as draft release'
|
||||
type: boolean
|
||||
default: true
|
||||
prerelease:
|
||||
description: 'Mark as pre-release'
|
||||
type: boolean
|
||||
default: true
|
||||
make-latest:
|
||||
description: 'Set as the latest release'
|
||||
type: boolean
|
||||
branch-protection-type:
|
||||
description: 'Branch protection type'
|
||||
type: choice
|
||||
options:
|
||||
- Branch Name
|
||||
- GitHub API
|
||||
default: Branch Name
|
||||
env:
|
||||
ARTIFACTS_PATH: artifacts
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get branch from workflow run
|
||||
id: get_release_branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
BRANCH_PROTECTION_TYPE: ${{ inputs.branch-protection-type }}
|
||||
run: |
|
||||
release_branch=$(gh run view $ARTIFACT_RUN_ID --json headBranch -q .headBranch)
|
||||
|
||||
case "$BRANCH_PROTECTION_TYPE" in
|
||||
"Branch Name")
|
||||
if [[ "$release_branch" != "main" && ! "$release_branch" =~ ^release/ ]]; then
|
||||
echo "::error::Branch '$release_branch' is not 'main' or a release branch starting with 'release/'. Releases must be created from protected branches."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"GitHub API")
|
||||
#NOTE requires token with "administration:read" scope
|
||||
if ! gh api "repos/${{ github.repository }}/branches/$release_branch/protection" | grep -q "required_status_checks"; then
|
||||
echo "::error::Branch '$release_branch' is not protected. Releases must be created from protected branches. If that's not correct, confirm if the github token user has the 'administration:read' scope."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unsupported branch protection type: $BRANCH_PROTECTION_TYPE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
run: |
|
||||
gh run download $ARTIFACT_RUN_ID -D $ARTIFACTS_PATH
|
||||
file_count=$(find $ARTIFACTS_PATH -type f | wc -l)
|
||||
echo "Downloaded $file_count file(s)."
|
||||
if [ "$file_count" -gt 0 ]; then
|
||||
echo "Downloaded files:"
|
||||
find $ARTIFACTS_PATH -type f
|
||||
fi
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||
with:
|
||||
tag_name: "v${{ inputs.version-name }}"
|
||||
name: "${{ inputs.version-name }} (${{ inputs.version-number }})"
|
||||
prerelease: ${{ inputs.prerelease }}
|
||||
draft: ${{ inputs.draft }}
|
||||
make_latest: ${{ inputs.make-latest }}
|
||||
target_commitish: ${{ steps.get_release_branch.outputs.release_branch }}
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
artifacts/**/*
|
||||
|
||||
- name: Update Release Description
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_ID: ${{ steps.create_release.outputs.id }}
|
||||
RELEASE_URL: ${{ steps.create_release.outputs.url }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
run: |
|
||||
# Get current release body
|
||||
current_body=$(gh api /repos/${{ github.repository }}/releases/$RELEASE_ID --jq .body)
|
||||
|
||||
# Append build source to the end
|
||||
updated_body="${current_body}
|
||||
**Builds Source:** https://github.com/${{ github.repository }}/actions/runs/$ARTIFACT_RUN_ID"
|
||||
|
||||
# Update release
|
||||
gh api --method PATCH /repos/${{ github.repository }}/releases/$RELEASE_ID \
|
||||
-f body="$updated_body"
|
||||
|
||||
echo "# :rocket: Release ready at:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$RELEASE_URL" >> $GITHUB_STEP_SUMMARY
|
||||
58
.github/workflows/release-branch.yml
vendored
Normal file
58
.github/workflows/release-branch.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Cut Release Branch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_type:
|
||||
description: 'Release Type'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- RC
|
||||
- Hotfix
|
||||
|
||||
jobs:
|
||||
create-release-branch:
|
||||
name: Create Release Branch
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create RC Branch
|
||||
if: inputs.release_type == 'RC'
|
||||
env:
|
||||
RC_PREFIX_DATE: "true" # replace with input if needed
|
||||
run: |
|
||||
if [ "$RC_PREFIX_DATE" = "true" ]; then
|
||||
current_date=$(date +'%Y.%m')
|
||||
branch_name="release/${current_date}-rc${{ github.run_number }}"
|
||||
else
|
||||
branch_name="release/rc${{ github.run_number }}"
|
||||
fi
|
||||
git switch main
|
||||
git switch -c $branch_name
|
||||
git push origin $branch_name
|
||||
echo "# :cherry_blossom: RC branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Create Hotfix Branch
|
||||
if: inputs.release_type == 'Hotfix'
|
||||
run: |
|
||||
latest_tag=$(git tag -l --sort=-creatordate | head -n 1)
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo "::error::No tags found in the repository"
|
||||
exit 1
|
||||
fi
|
||||
branch_name="release/hotfix-${latest_tag}"
|
||||
echo "🌿 branch name: $branch_name"
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then
|
||||
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
fi
|
||||
git switch -c $branch_name $latest_tag
|
||||
git push origin $branch_name
|
||||
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
60
.github/workflows/scan-ci.yml
vendored
Normal file
60
.github/workflows/scan-ci.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Scan Protected Branches On Push
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
sast:
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@b74e8d514feae4ad5ad2b43e72590935bd2daf5f # 2.0.39
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path .
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
24
.github/workflows/scan.yml
vendored
24
.github/workflows/scan.yml
vendored
@@ -1,14 +1,11 @@
|
||||
name: Scan
|
||||
name: Scan Pull Requests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
@@ -17,7 +14,7 @@ jobs:
|
||||
|
||||
sast:
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -26,12 +23,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@6c56658230f79c227a55120e9b24845d574d5225 # 2.0.31
|
||||
uses: checkmarx/ast-github-action@b74e8d514feae4ad5ad2b43e72590935bd2daf5f # 2.0.39
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
@@ -46,13 +43,13 @@ jobs:
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
||||
uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -60,16 +57,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0
|
||||
uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
|
||||
41
.github/workflows/test.yml
vendored
41
.github/workflows/test.yml
vendored
@@ -6,23 +6,19 @@ on:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
type: [checks_requested]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 17
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
name: Check PR run
|
||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-run
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
@@ -31,15 +27,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/actions/wrapper-validation@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -49,7 +43,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -58,12 +52,12 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
uses: ruby/setup-ruby@2a18b06812b0e15bb916e1df298d3e740422c47e # v1.203.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -78,9 +72,14 @@ jobs:
|
||||
run: |
|
||||
bundle exec fastlane check
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
|
||||
- name: Upload test reports on failure
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
if: failure()
|
||||
with:
|
||||
file: app/build/reports/kover/reportStandardDebug.xml
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
name: test-reports
|
||||
path: app/build/reports/tests/
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1
|
||||
with:
|
||||
files: app/build/reports/kover/reportStandardDebug.xml
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
shroud.reportKover 'App', 'app/build/reports/kover/reportStandardDebug.xml', 80, 80, false
|
||||
70
Gemfile.lock
70
Gemfile.lock
@@ -10,20 +10,20 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.961.0)
|
||||
aws-sdk-core (3.201.3)
|
||||
aws-partitions (1.1026.0)
|
||||
aws-sdk-core (3.214.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.88.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.157.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-s3 (1.176.1)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.9.1)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
@@ -32,15 +32,15 @@ GEM
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
date (3.3.4)
|
||||
date (3.4.1)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.111.0)
|
||||
faraday (1.10.3)
|
||||
excon (0.112.0)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@@ -59,17 +59,17 @@ GEM
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.222.0)
|
||||
fastlane (2.226.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -85,6 +85,7 @@ GEM
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
fastlane-sirp (>= 1.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
@@ -108,11 +109,13 @@ GEM
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty (~> 0.4.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-plugin-firebase_app_distribution (0.9.1)
|
||||
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
|
||||
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
|
||||
fastlane-sirp (1.0.0)
|
||||
sysrandom (~> 1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
@@ -134,7 +137,7 @@ GEM
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.7.0)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
@@ -155,21 +158,21 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.6)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.2)
|
||||
jwt (2.8.2)
|
||||
json (2.9.1)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
nanaimo (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.2.1)
|
||||
nkf (0.2.0)
|
||||
optparse (0.5.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.1)
|
||||
public_suffix (6.0.1)
|
||||
@@ -179,9 +182,8 @@ GEM
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.9)
|
||||
strscan
|
||||
rouge (2.0.7)
|
||||
rexml (3.4.0)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.5)
|
||||
@@ -193,11 +195,11 @@ GEM
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
strscan (3.1.0)
|
||||
sysrandom (1.0.5)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
time (0.3.0)
|
||||
time (0.4.1)
|
||||
date
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
@@ -205,17 +207,17 @@ GEM
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.24.0)
|
||||
xcodeproj (1.27.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
nanaimo (~> 0.4.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.4.0)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
|
||||
20
README.md
20
README.md
@@ -1,7 +1,4 @@
|
||||
# Bitwarden Android (BETA)
|
||||
|
||||
> [!TIP]
|
||||
> This repo has the new native Android app, currently in [Beta](https://community.bitwarden.com/t/about-the-beta-program/39185). Looking for the legacy .NET MAUI apps? Head on over to [bitwarden/mobile](https://github.com/bitwarden/mobile)
|
||||
# Bitwarden Android
|
||||
|
||||
## Contents
|
||||
|
||||
@@ -11,8 +8,8 @@
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Minimum SDK**: 28
|
||||
- **Target SDK**: 34
|
||||
- **Minimum SDK**: 29
|
||||
- **Target SDK**: 35
|
||||
- **Device Types Supported**: Phone and Tablet
|
||||
- **Orientations Supported**: Portrait and Landscape
|
||||
|
||||
@@ -28,6 +25,7 @@
|
||||
2. Create a `user.properties` file in the root directory of the project and add the following properties:
|
||||
|
||||
- `gitHubToken`: A "classic" Github Personal Access Token (PAT) with the `read:packages` scope (ex: `gitHubToken=gph_xx...xx`). These can be generated by going to the [Github tokens page](https://github.com/settings/tokens). See [the Github Packages user documentation concerning authentication](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-to-github-packages) for more details.
|
||||
- `localSdk`: A boolean value to determine if the SDK should be loaded from the local maven artifactory (ex: `localSdk=true`). This is particularly useful when developing new SDK capabilities. Review [Linking SDK to clients](https://contributing.bitwarden.com/getting-started/sdk/#linking-the-sdk-to-clients) for more details.
|
||||
|
||||
3. Setup the code style formatter:
|
||||
|
||||
@@ -134,6 +132,11 @@ The following is a list of all third-party dependencies included as part of the
|
||||
- https://github.com/firebase/firebase-android-sdk
|
||||
- Purpose: SDK for crash and non-fatal error reporting. (**NOTE:** This dependency is not included in builds distributed via F-Droid.)
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Google Play Reviews**
|
||||
- https://developer.android.com/reference/com/google/android/play/core/release-notes
|
||||
- Purpose: On standard builds provide an interface to add a review for the password manager application in Google Play.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Glide**
|
||||
- https://github.com/bumptech/glide
|
||||
@@ -170,6 +173,11 @@ The following is a list of all third-party dependencies included as part of the
|
||||
- Purpose: A networking layer interface.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Timber**
|
||||
- https://github.com/JakeWharton/timber
|
||||
- Purpose: Extensible logging library for Android.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **zxcvbn4j**
|
||||
- https://github.com/nulab/zxcvbn4j
|
||||
- Purpose: Password strength estimation.
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import com.android.utils.cxx.io.removeExtensionIfPresent
|
||||
import com.google.firebase.crashlytics.buildtools.gradle.tasks.InjectMappingFileIdTask
|
||||
import com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask
|
||||
import com.google.gms.googleservices.GoogleServicesTask
|
||||
import dagger.hilt.android.plugin.util.capitalize
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
@@ -20,6 +25,26 @@ plugins {
|
||||
alias(libs.plugins.sonarqube)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads local user-specific build properties that are not checked into source control.
|
||||
*/
|
||||
val userProperties = Properties().apply {
|
||||
val buildPropertiesFile = File(rootDir, "user.properties")
|
||||
if (buildPropertiesFile.exists()) {
|
||||
FileInputStream(buildPropertiesFile).use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads CI-specific build properties that are not checked into source control.
|
||||
*/
|
||||
val ciProperties = Properties().apply {
|
||||
val ciPropsFile = File(rootDir, "ci.properties")
|
||||
if (ciPropsFile.exists()) {
|
||||
FileInputStream(ciPropsFile).use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.x8bit.bitwarden"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
@@ -29,7 +54,7 @@ android {
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = 1
|
||||
versionName = "2024.06.00"
|
||||
versionName = "2024.9.0"
|
||||
|
||||
setProperty("archivesBaseName", "com.x8bit.bitwarden")
|
||||
|
||||
@@ -39,6 +64,12 @@ android {
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "CI_INFO",
|
||||
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}"
|
||||
)
|
||||
}
|
||||
|
||||
androidResources {
|
||||
@@ -61,6 +92,9 @@ android {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "true")
|
||||
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "true")
|
||||
}
|
||||
|
||||
// Beta and Release variants are identical except beta has a different package name
|
||||
@@ -72,6 +106,9 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "false")
|
||||
}
|
||||
release {
|
||||
isDebuggable = false
|
||||
@@ -80,6 +117,9 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +134,39 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
val bundlesDir = "${layout.buildDirectory.get()}/outputs/bundle"
|
||||
outputs
|
||||
.mapNotNull { it as? BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
val fileNameWithoutExtension = when (flavorName) {
|
||||
"fdroid" -> "$applicationId-$flavorName"
|
||||
"standard" -> "$applicationId"
|
||||
else -> output.outputFileName.removeExtensionIfPresent(".apk")
|
||||
}
|
||||
|
||||
// Set the APK output filename.
|
||||
output.outputFileName = "$fileNameWithoutExtension.apk"
|
||||
|
||||
val variantName = name
|
||||
val renameTaskName = "rename${variantName.capitalize()}AabFiles"
|
||||
tasks.register(renameTaskName) {
|
||||
group = "build"
|
||||
description = "Renames the bundle files for $variantName variant"
|
||||
doLast {
|
||||
renameFile(
|
||||
"$bundlesDir/$variantName/$namespace-$flavorName-${buildType.name}.aab",
|
||||
"$fileNameWithoutExtension.aab",
|
||||
)
|
||||
}
|
||||
}
|
||||
// Force renaming task to execute after the variant is built.
|
||||
tasks
|
||||
.getByName("bundle${variantName.capitalize()}")
|
||||
.finalizedBy(renameTaskName)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility(libs.versions.jvmTarget.get())
|
||||
targetCompatibility(libs.versions.jvmTarget.get())
|
||||
@@ -114,7 +187,10 @@ android {
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
lint {
|
||||
disable.add("MissingTranslation")
|
||||
disable += listOf(
|
||||
"MissingTranslation",
|
||||
"ExtraTranslation",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,11 +200,22 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy.dependencySubstitution {
|
||||
if ((userProperties["localSdk"] as String?).toBoolean()) {
|
||||
substitute(module("com.bitwarden:sdk-android"))
|
||||
.using(module("com.bitwarden:sdk-android:LOCAL"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
fun standardImplementation(dependencyNotation: Any) {
|
||||
add("standardImplementation", dependencyNotation)
|
||||
}
|
||||
|
||||
implementation(files("libs/authenticatorbridge-1.0.0-release.aar"))
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.autofill)
|
||||
@@ -170,6 +257,7 @@ dependencies {
|
||||
implementation(platform(libs.square.retrofit.bom))
|
||||
implementation(libs.square.retrofit)
|
||||
implementation(libs.square.retrofit.kotlinx.serialization)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.zxing.zxing.core)
|
||||
|
||||
// For now we are restricted to running Compose tests for debug builds only
|
||||
@@ -180,6 +268,7 @@ dependencies {
|
||||
standardImplementation(libs.google.firebase.cloud.messaging)
|
||||
standardImplementation(platform(libs.google.firebase.bom))
|
||||
standardImplementation(libs.google.firebase.crashlytics)
|
||||
standardImplementation(libs.google.play.review)
|
||||
|
||||
testImplementation(libs.androidx.compose.ui.test)
|
||||
testImplementation(libs.google.hilt.android.testing)
|
||||
@@ -213,6 +302,7 @@ kover {
|
||||
annotatedBy(
|
||||
// Compose previews
|
||||
"androidx.compose.ui.tooling.preview.Preview",
|
||||
"androidx.compose.ui.tooling.preview.PreviewScreenSizes",
|
||||
// Manually excluded classes/files/etc.
|
||||
"com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage",
|
||||
)
|
||||
@@ -261,6 +351,10 @@ tasks {
|
||||
dependsOn("detekt")
|
||||
}
|
||||
|
||||
getByName("sonar") {
|
||||
dependsOn("check")
|
||||
}
|
||||
|
||||
withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
||||
jvmTarget = libs.versions.jvmTarget.get()
|
||||
}
|
||||
@@ -273,15 +367,16 @@ tasks {
|
||||
maxHeapSize = "2g"
|
||||
maxParallelForks = Runtime.getRuntime().availableProcessors()
|
||||
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC"
|
||||
android.sourceSets["main"].res.srcDirs("src/test/res")
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
// Disable Fdroid-specific tasks that we want to exclude
|
||||
val tasks = tasks.withType<GoogleServicesTask>() +
|
||||
val fdroidTasksToDisable = tasks.withType<GoogleServicesTask>() +
|
||||
tasks.withType<InjectMappingFileIdTask>() +
|
||||
tasks.withType<UploadMappingFileTask>()
|
||||
tasks
|
||||
fdroidTasksToDisable
|
||||
.filter { it.name.contains("Fdroid") }
|
||||
.forEach { it.enabled = false }
|
||||
}
|
||||
@@ -298,8 +393,17 @@ sonar {
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
getByName("sonar") {
|
||||
dependsOn("check")
|
||||
private fun renameFile(path: String, newName: String) {
|
||||
val originalFile = File(path)
|
||||
if (!originalFile.exists()) {
|
||||
println("File $originalFile does not exist!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val newFile = File(originalFile.parentFile, newName)
|
||||
if (originalFile.renameTo(newFile)) {
|
||||
println("Renamed $originalFile to $newFile")
|
||||
} else {
|
||||
throw RuntimeException("Failed to rename $originalFile to $newFile")
|
||||
}
|
||||
}
|
||||
|
||||
BIN
app/libs/authenticatorbridge-1.0.0-release.aar
Normal file
BIN
app/libs/authenticatorbridge-1.0.0-release.aar
Normal file
Binary file not shown.
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
@@ -6,6 +6,10 @@
|
||||
# we keep it here.
|
||||
-keep class com.bitwarden.** { *; }
|
||||
|
||||
# The Android Verifier component must be kept because it looks like dead code. Proguard is unable to
|
||||
# see any JNI usage, so our rules must manually opt into keeping it.
|
||||
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }
|
||||
|
||||
################################################################################
|
||||
# Bitwarden Models
|
||||
################################################################################
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "f7906c69e0a2c065d4d3be140fc721b6",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ciphers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherType",
|
||||
"columnName": "cipher_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherJson",
|
||||
"columnName": "cipher_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ciphers_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "collections",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "organizationId",
|
||||
"columnName": "organization_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shouldHidePasswords",
|
||||
"columnName": "should_hide_passwords",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalId",
|
||||
"columnName": "external_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isReadOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canManage",
|
||||
"columnName": "manage",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collections_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "domains",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT NOT NULL, PRIMARY KEY(`user_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domainsJson",
|
||||
"columnName": "domains_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "folders",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "revisionDate",
|
||||
"columnName": "revision_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_folders_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sends",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendType",
|
||||
"columnName": "send_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendJson",
|
||||
"columnName": "send_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_sends_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"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, 'f7906c69e0a2c065d4d3be140fc721b6')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "ee697e71290c92fe5b607d0b7665481b",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ciphers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherType",
|
||||
"columnName": "cipher_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherJson",
|
||||
"columnName": "cipher_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ciphers_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "collections",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "organizationId",
|
||||
"columnName": "organization_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shouldHidePasswords",
|
||||
"columnName": "should_hide_passwords",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalId",
|
||||
"columnName": "external_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isReadOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canManage",
|
||||
"columnName": "manage",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collections_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "domains",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domainsJson",
|
||||
"columnName": "domains_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "folders",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "revisionDate",
|
||||
"columnName": "revision_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_folders_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sends",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendType",
|
||||
"columnName": "send_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendJson",
|
||||
"columnName": "send_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_sends_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"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, 'ee697e71290c92fe5b607d0b7665481b')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 6,
|
||||
"identityHash": "ee158c483edfe5102504670f3d9845d4",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ciphers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherType",
|
||||
"columnName": "cipher_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherJson",
|
||||
"columnName": "cipher_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ciphers_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "collections",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "organizationId",
|
||||
"columnName": "organization_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shouldHidePasswords",
|
||||
"columnName": "should_hide_passwords",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalId",
|
||||
"columnName": "external_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isReadOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canManage",
|
||||
"columnName": "manage",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collections_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "domains",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domainsJson",
|
||||
"columnName": "domains_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "folders",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "revisionDate",
|
||||
"columnName": "revision_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_folders_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sends",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendType",
|
||||
"columnName": "send_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendJson",
|
||||
"columnName": "send_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_sends_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"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, 'ee158c483edfe5102504670f3d9845d4')"
|
||||
]
|
||||
}
|
||||
}
|
||||
9
app/src/beta/res/values/manifest.xml
Normal file
9
app/src/beta/res/values/manifest.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- For beta variant, we don't have a matching variant of the Bitwarden Authenticator app.
|
||||
Therefore, we leave the known app cert null here so that no clients can connect to
|
||||
AuthenticatorBridgeService in the beta variant. If later another variant of the
|
||||
Bitwarden Authenticator app is added, a SHA-256 digest of that variant's APK can be added here.
|
||||
-->
|
||||
<string-array name="known_authenticator_app_certs" />
|
||||
</resources>
|
||||
27
app/src/beta/res/xml/shortcuts.xml
Normal file
27
app/src/beta/res/xml/shortcuts.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@mipmap/ic_generator_shortcut"
|
||||
android:shortcutId="bitwarden_password_generator"
|
||||
android:shortcutLongLabel="@string/password_generator"
|
||||
android:shortcutShortLabel="@string/password_generator">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="bitwarden://password_generator"
|
||||
android:targetClass="com.x8bit.bitwarden.MainActivity"
|
||||
android:targetPackage="com.x8bit.bitwarden.beta" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@mipmap/ic_vault_shortcut"
|
||||
android:shortcutId="bitwarden_my_vault"
|
||||
android:shortcutLongLabel="@string/my_vault"
|
||||
android:shortcutShortLabel="@string/my_vault">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="bitwarden://my_vault"
|
||||
android:targetClass="com.x8bit.bitwarden.MainActivity"
|
||||
android:targetPackage="com.x8bit.bitwarden.beta" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
7
app/src/debug/res/values/manifest.xml
Normal file
7
app/src/debug/res/values/manifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="known_authenticator_app_certs">
|
||||
<!-- This is the SHA-256 digest for the Authenticator App debug variant:-->
|
||||
<item>13144ab52af797a88c2fe292674461ef1715e0e1e4f5f538f63f1c174696f476</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
|
||||
/**
|
||||
* CrashLogsManager implementation for F-droid flavor builds.
|
||||
*/
|
||||
class CrashLogsManagerImpl(
|
||||
settingsRepository: SettingsRepository,
|
||||
legacyAppCenterMigrator: LegacyAppCenterMigrator,
|
||||
) : CrashLogsManager {
|
||||
override var isEnabled: Boolean = true
|
||||
|
||||
override fun trackNonFatalException(e: Exception) = Unit
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* [LogsManager] implementation for F-droid flavor builds.
|
||||
*/
|
||||
class LogsManagerImpl(
|
||||
settingsRepository: SettingsRepository,
|
||||
legacyAppCenterMigrator: LegacyAppCenterMigrator,
|
||||
) : LogsManager {
|
||||
init {
|
||||
if (BuildConfig.HAS_LOGS_ENABLED) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
}
|
||||
|
||||
override var isEnabled: Boolean = false
|
||||
|
||||
override fun setUserData(userId: String?, environmentType: Environment.Type) = Unit
|
||||
|
||||
override fun trackNonFatalException(throwable: Throwable) = Unit
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.x8bit.bitwarden.ui.platform.manager.review
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
/**
|
||||
* No-op implementation of [AppReviewManager] for F-Droid builds.
|
||||
*/
|
||||
class AppReviewManagerImpl(
|
||||
activity: Activity,
|
||||
) : AppReviewManager {
|
||||
override fun promptForReview() = Unit
|
||||
}
|
||||
@@ -16,6 +16,20 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Protect access to AuthenticatorBridgeService using this custom permission.
|
||||
|
||||
Note that each build type uses a different value for knownCerts.
|
||||
|
||||
This in effect means that the only application that can connect to the debug/release/etc
|
||||
variant AuthenticatorBridgeService is the debug/release/etc variant Bitwarden Authenticator
|
||||
app. -->
|
||||
<permission
|
||||
android:name="${applicationId}.permission.AUTHENTICATOR_BRIDGE_SERVICE"
|
||||
android:knownCerts="@array/known_authenticator_app_certs"
|
||||
android:label="Bitwarden Bridge"
|
||||
android:protectionLevel="signature|knownSigner"
|
||||
tools:targetApi="s" />
|
||||
|
||||
<application
|
||||
android:name=".BitwardenApplication"
|
||||
android:allowBackup="false"
|
||||
@@ -55,14 +69,49 @@
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data android:host="vault.bitwarden.com" />
|
||||
<data android:host="vault.bitwarden.eu" />
|
||||
<data android:host="*.bitwarden.pw" />
|
||||
<data android:pathPattern="/redirect-connector.*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="otpauth" />
|
||||
<data android:host="totp" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="bitwarden" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AccessibilityActivity"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTop"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<activity
|
||||
android:name=".AutofillTotpCopyActivity"
|
||||
android:exported="true"
|
||||
@@ -147,6 +196,26 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The AccessibilityService name below refers to the legacy Xamarin app's service name. This
|
||||
must always match in order for the app to properly query if it is providing accessibility
|
||||
services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.Accessibility.AccessibilityService"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/accessibility_service" />
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The CredentialProviderService name below refers to the legacy Xamarin app's service name.
|
||||
This must always match in order for the app to properly query if it is providing credential
|
||||
@@ -178,16 +247,79 @@
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The AutofillTileService name below refers to the legacy Xamarin app's service name.
|
||||
This must always match in order for the app to properly query if it is providing autofill
|
||||
tile services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.AutofillTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/autofill"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The GeneratorTileService name below refers to the legacy Xamarin app's service name.
|
||||
This must always match in order for the app to properly query if it is providing generator
|
||||
tile services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.GeneratorTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_generator"
|
||||
android:label="@string/password_generator"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The MyVaultTileService name below refers to the legacy Xamarin app's service name.
|
||||
This must always match in order for the app to properly query if it is providing vault
|
||||
tile services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.MyVaultTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/my_vault"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.data.platform.service.AuthenticatorBridgeService"
|
||||
android:exported="true"
|
||||
android:permission="${applicationId}.permission.AUTHENTICATOR_BRIDGE_SERVICE" />
|
||||
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
92
app/src/main/assets/fido2_privileged_community.json
Normal file
92
app/src/main/assets/fido2_privileged_community.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"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.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.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"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "us.spotco.fennec_dos",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "FF:81:F5:BE:56:39:65:94:EE:E7:0F:EF:28:32:25:6E:15:21:41:22:E2:BA:9C:ED:D2:60:05:FF:D4:BC:AA:A8"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "us.spotco.mulch",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -475,7 +475,102 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* An activity to be launched and then immediately closed so that the OS Shade can be collapsed
|
||||
* after the user clicks on the Autofill Quick Tile.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class AccessibilityActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -3,37 +3,62 @@ package com.x8bit.bitwarden
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.app.AppComponentFactory
|
||||
import com.x8bit.bitwarden.data.autofill.BitwardenAutofillService
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.BitwardenFido2ProviderService
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
private const val LEGACY_AUTOFILL_SERVICE_NAME = "com.x8bit.bitwarden.Autofill.AutofillService"
|
||||
private const val LEGACY_CREDENTIAL_SERVICE_NAME =
|
||||
"com.x8bit.bitwarden.Autofill.CredentialProviderService"
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenGeneratorTileService
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenVaultTileService
|
||||
|
||||
/**
|
||||
* A factory class that allows us to intercept when a manifest element is being instantiated
|
||||
* and modify various characteristics before initialization.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@Keep
|
||||
@OmitFromCoverage
|
||||
class BitwardenAppComponentFactory : AppComponentFactory() {
|
||||
/**
|
||||
* Used to intercept when the [BitwardenAutofillService] or [BitwardenFido2ProviderService] is
|
||||
* being instantiated and modify which service is created. This is required because the
|
||||
* [className] used in the manifest must match the legacy Xamarin app service name but the
|
||||
* service name in this app is different.
|
||||
* Used to intercept when certain legacy services are being instantiated and modify which
|
||||
* service is created. This is required because the [className] used in the manifest must match
|
||||
* the legacy Xamarin app service name but the service name in this app is different.
|
||||
*
|
||||
* Services currently being managed:
|
||||
* * [BitwardenAccessibilityService]
|
||||
* * [BitwardenAutofillService]
|
||||
* * [BitwardenAutofillTileService]
|
||||
* * [BitwardenFido2ProviderService]
|
||||
* * [BitwardenVaultTileService]
|
||||
* * [BitwardenGeneratorTileService]
|
||||
*/
|
||||
override fun instantiateServiceCompat(
|
||||
cl: ClassLoader,
|
||||
className: String,
|
||||
intent: Intent?,
|
||||
): Service = when (className) {
|
||||
LEGACY_ACCESSIBILITY_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(
|
||||
cl,
|
||||
BitwardenAccessibilityService::class.java.name,
|
||||
intent,
|
||||
)
|
||||
}
|
||||
|
||||
LEGACY_AUTOFILL_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(cl, BitwardenAutofillService::class.java.name, intent)
|
||||
}
|
||||
|
||||
LEGACY_AUTOFILL_TILE_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(
|
||||
cl,
|
||||
BitwardenAutofillTileService::class.java.name,
|
||||
intent,
|
||||
)
|
||||
}
|
||||
|
||||
LEGACY_CREDENTIAL_SERVICE_NAME -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
super.instantiateServiceCompat(
|
||||
@@ -48,6 +73,18 @@ class BitwardenAppComponentFactory : AppComponentFactory() {
|
||||
}
|
||||
}
|
||||
|
||||
LEGACY_VAULT_TILE_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(cl, BitwardenVaultTileService::class.java.name, intent)
|
||||
}
|
||||
|
||||
LEGACY_GENERATOR_TILE_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(
|
||||
cl,
|
||||
BitwardenGeneratorTileService::class.java.name,
|
||||
intent,
|
||||
)
|
||||
}
|
||||
|
||||
else -> super.instantiateServiceCompat(cl, className, intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package com.x8bit.bitwarden
|
||||
import android.app.Application
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -20,10 +19,10 @@ class BitwardenApplication : Application() {
|
||||
// Inject classes here that must be triggered on startup but are not otherwise consumed by
|
||||
// other callers.
|
||||
@Inject
|
||||
lateinit var networkConfigManager: NetworkConfigManager
|
||||
lateinit var logsManager: LogsManager
|
||||
|
||||
@Inject
|
||||
lateinit var crashLogsManager: CrashLogsManager
|
||||
lateinit var networkConfigManager: NetworkConfigManager
|
||||
|
||||
@Inject
|
||||
lateinit var authRequestNotificationManager: AuthRequestNotificationManager
|
||||
@@ -33,7 +32,4 @@ class BitwardenApplication : Application() {
|
||||
|
||||
@Inject
|
||||
lateinit var restrictionManager: RestrictionManager
|
||||
|
||||
@Inject
|
||||
lateinit var serverConfigRepository: ServerConfigRepository
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
/**
|
||||
* The legacy name for the accessibility service.
|
||||
*/
|
||||
const val LEGACY_ACCESSIBILITY_SERVICE_NAME: String =
|
||||
"com.x8bit.bitwarden.Accessibility.AccessibilityService"
|
||||
|
||||
/**
|
||||
* The legacy name for the autofill service.
|
||||
*/
|
||||
const val LEGACY_AUTOFILL_SERVICE_NAME: String = "com.x8bit.bitwarden.Autofill.AutofillService"
|
||||
|
||||
/**
|
||||
* The legacy name for the accessibility autofill tile service.
|
||||
*/
|
||||
const val LEGACY_AUTOFILL_TILE_SERVICE_NAME: String = "com.x8bit.bitwarden.AutofillTileService"
|
||||
|
||||
/**
|
||||
* The legacy name for the credential service.
|
||||
*/
|
||||
const val LEGACY_CREDENTIAL_SERVICE_NAME: String =
|
||||
"com.x8bit.bitwarden.Autofill.CredentialProviderService"
|
||||
|
||||
/**
|
||||
* The legacy name for the generator tile service.
|
||||
*/
|
||||
const val LEGACY_GENERATOR_TILE_SERVICE_NAME: String = "com.x8bit.bitwarden.GeneratorTileService"
|
||||
|
||||
/**
|
||||
* The legacy name for the vault tile service.
|
||||
*/
|
||||
const val LEGACY_VAULT_TILE_SERVICE_NAME: String = "com.x8bit.bitwarden.MyVaultTileService"
|
||||
@@ -2,7 +2,10 @@ package com.x8bit.bitwarden
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -11,17 +14,19 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -39,16 +44,20 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var autofillCompletionManager: AutofillCompletionManager
|
||||
|
||||
@Inject
|
||||
lateinit var accessibilityCompletionManager: AccessibilityCompletionManager
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
observeViewModelEvents()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
mainViewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
@@ -66,11 +75,33 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
setContent {
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
EventsEffect(viewModel = mainViewModel) { event ->
|
||||
when (event) {
|
||||
is MainEvent.CompleteAccessibilityAutofill -> {
|
||||
handleCompleteAccessibilityAutofill(event)
|
||||
}
|
||||
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
is MainEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(
|
||||
baseContext,
|
||||
event.message.invoke(resources),
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
LocalManagerProvider {
|
||||
BitwardenTheme(theme = state.theme) {
|
||||
RootNavScreen(
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -93,16 +124,27 @@ class MainActivity : AppCompatActivity() {
|
||||
currentFocus?.clearFocus()
|
||||
}
|
||||
|
||||
private fun observeViewModelEvents() {
|
||||
mainViewModel
|
||||
.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean = debugLaunchManager
|
||||
.actionOnInputEvent(event = event, action = ::sendOpenDebugMenuEvent)
|
||||
.takeIf { it }
|
||||
?: super.dispatchTouchEvent(event)
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean = debugLaunchManager
|
||||
.actionOnInputEvent(event = event, action = ::sendOpenDebugMenuEvent)
|
||||
.takeIf { it }
|
||||
?: super.dispatchKeyEvent(event)
|
||||
|
||||
private fun sendOpenDebugMenuEvent() {
|
||||
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
|
||||
}
|
||||
|
||||
private fun handleCompleteAccessibilityAutofill(
|
||||
event: MainEvent.CompleteAccessibilityAutofill,
|
||||
) {
|
||||
accessibilityCompletionManager.completeAccessibilityAutofill(
|
||||
activity = this,
|
||||
cipherView = event.cipherView,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleCompleteAutofill(event: MainEvent.CompleteAutofill) {
|
||||
|
||||
@@ -5,8 +5,12 @@ import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
|
||||
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull
|
||||
@@ -16,24 +20,35 @@ import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
@@ -44,7 +59,9 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
@@ -52,7 +69,9 @@ class MainViewModel @Inject constructor(
|
||||
settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val clock: Clock,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
initialState = MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
@@ -74,6 +93,12 @@ class MainViewModel @Inject constructor(
|
||||
.onEach { specialCircumstance = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
accessibilitySelectionManager
|
||||
.accessibilitySelectionFlow
|
||||
.map { MainAction.Internal.AccessibilitySelectionReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
autofillSelectionManager
|
||||
.autofillSelectionFlow
|
||||
.onEach { trySendAction(MainAction.Internal.AutofillSelectionReceive(it)) }
|
||||
@@ -123,10 +148,27 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
// On app launch, mark all active users as having previously logged in.
|
||||
// This covers any users who are active prior to this value being recorded.
|
||||
viewModelScope.launch {
|
||||
val userState = authRepository
|
||||
.userStateFlow
|
||||
.first()
|
||||
userState
|
||||
?.accounts
|
||||
?.forEach {
|
||||
settingsRepository.storeUserHasLoggedInValue(it.userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleAction(action: MainAction) {
|
||||
when (action) {
|
||||
is MainAction.Internal.AccessibilitySelectionReceive -> {
|
||||
handleAccessibilitySelectionReceive(action)
|
||||
}
|
||||
|
||||
is MainAction.Internal.AutofillSelectionReceive -> {
|
||||
handleAutofillSelectionReceive(action)
|
||||
}
|
||||
@@ -137,12 +179,25 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOpenDebugMenu() {
|
||||
sendEvent(MainEvent.NavigateToDebugMenu)
|
||||
}
|
||||
|
||||
private fun handleAccessibilitySelectionReceive(
|
||||
action: MainAction.Internal.AccessibilitySelectionReceive,
|
||||
) {
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
sendEvent(MainEvent.CompleteAccessibilityAutofill(cipherView = action.cipherView))
|
||||
}
|
||||
|
||||
private fun handleAutofillSelectionReceive(
|
||||
action: MainAction.Internal.AutofillSelectionReceive,
|
||||
) {
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView))
|
||||
}
|
||||
|
||||
@@ -176,7 +231,7 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun handleIntent(
|
||||
intent: Intent,
|
||||
isFirstIntent: Boolean,
|
||||
@@ -185,13 +240,39 @@ class MainViewModel @Inject constructor(
|
||||
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
|
||||
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
|
||||
val shareData = intentManager.getShareDataFromIntent(intent)
|
||||
val totpData: TotpData? =
|
||||
// First grab TOTP URI directly from the intent data:
|
||||
intent.getTotpDataOrNull()
|
||||
?: run {
|
||||
// Then check to see if the intent is coming from the Authenticator app:
|
||||
if (intent.isAddTotpLoginItemFromAuthenticator()) {
|
||||
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData.also {
|
||||
// Clear pending add TOTP data so it is only handled once:
|
||||
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
|
||||
val hasVaultShortcut = intent.isMyVaultShortcut
|
||||
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
|
||||
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
|
||||
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
|
||||
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
|
||||
when {
|
||||
passwordlessRequestData != null -> {
|
||||
authRepository.activeUserId?.let {
|
||||
if (it != passwordlessRequestData.userId &&
|
||||
!vaultRepository.isVaultUnlocked(it)
|
||||
) {
|
||||
// We only switch the account here if the current user's vault is not
|
||||
// unlocked, otherwise prompt the user to allow us to change the account
|
||||
// in the LoginApprovalScreen
|
||||
authRepository.switchAccount(passwordlessRequestData.userId)
|
||||
}
|
||||
}
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.PasswordlessRequest(
|
||||
passwordlessRequestData = passwordlessRequestData,
|
||||
@@ -201,6 +282,10 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
completeRegistrationData != null -> {
|
||||
handleCompleteRegistrationData(completeRegistrationData)
|
||||
}
|
||||
|
||||
autofillSaveItem != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AutofillSave(
|
||||
@@ -218,6 +303,11 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
totpData != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AddTotpLoginItem(data = totpData)
|
||||
}
|
||||
|
||||
shareData != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ShareNewSend(
|
||||
@@ -235,7 +325,7 @@ class MainViewModel @Inject constructor(
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = fido2CredentialRequestData,
|
||||
fido2CreateCredentialRequest = fido2CredentialRequestData,
|
||||
)
|
||||
|
||||
// Switch accounts if the selected user is not the active user.
|
||||
@@ -268,6 +358,11 @@ class MainViewModel @Inject constructor(
|
||||
hasVaultShortcut -> {
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.VaultShortcut
|
||||
}
|
||||
|
||||
hasAccountSecurityShortcut -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AccountSecurityShortcut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +370,49 @@ class MainViewModel @Inject constructor(
|
||||
sendEvent(MainEvent.Recreate)
|
||||
garbageCollectionManager.tryCollect()
|
||||
}
|
||||
|
||||
private fun handleCompleteRegistrationData(data: CompleteRegistrationData) {
|
||||
viewModelScope.launch {
|
||||
// Attempt to load the environment for the user if they have a pre-auth environment
|
||||
// saved.
|
||||
environmentRepository.loadEnvironmentForEmail(userEmail = data.email)
|
||||
// Determine if the token is still valid.
|
||||
val emailTokenResult = authRepository.validateEmailToken(
|
||||
email = data.email,
|
||||
token = data.verificationToken,
|
||||
)
|
||||
when (emailTokenResult) {
|
||||
is EmailTokenResult.Error -> {
|
||||
sendEvent(
|
||||
MainEvent.ShowToast(
|
||||
message = emailTokenResult
|
||||
.message
|
||||
?.asText()
|
||||
?: R.string.there_was_an_issue_validating_the_registration_token
|
||||
.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
EmailTokenResult.Expired -> {
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance
|
||||
.RegistrationEvent
|
||||
.ExpiredRegistrationLink
|
||||
}
|
||||
|
||||
EmailTokenResult.Success -> {
|
||||
if (authRepository.activeUserId != null) {
|
||||
authRepository.hasPendingAccountAddition = true
|
||||
}
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||
completeRegistrationData = data,
|
||||
timestamp = clock.millis(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,10 +438,23 @@ sealed class MainAction {
|
||||
*/
|
||||
data class ReceiveNewIntent(val intent: Intent) : MainAction()
|
||||
|
||||
/**
|
||||
* Receive event to open the debug menu.
|
||||
*/
|
||||
data object OpenDebugMenu : MainAction()
|
||||
|
||||
/**
|
||||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
sealed class Internal : MainAction() {
|
||||
/**
|
||||
* Indicates the user has manually selected the given [cipherView] for accessibility
|
||||
* autofill.
|
||||
*/
|
||||
data class AccessibilitySelectionReceive(
|
||||
val cipherView: CipherView,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the user has manually selected the given [cipherView] for autofill.
|
||||
*/
|
||||
@@ -341,6 +492,12 @@ sealed class MainAction {
|
||||
* Represents events that are emitted by the [MainViewModel].
|
||||
*/
|
||||
sealed class MainEvent {
|
||||
/**
|
||||
* Event indicating that the user has chosen the given [cipherView] for accessibility autofill
|
||||
* and that the process is ready to complete.
|
||||
*/
|
||||
data class CompleteAccessibilityAutofill(val cipherView: CipherView) : MainEvent()
|
||||
|
||||
/**
|
||||
* Event indicating that the user has chosen the given [cipherView] for autofill and that the
|
||||
* process is ready to complete.
|
||||
@@ -351,4 +508,14 @@ sealed class MainEvent {
|
||||
* Event indicating that the UI should recreate itself.
|
||||
*/
|
||||
data object Recreate : MainEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the debug menu.
|
||||
*/
|
||||
data object NavigateToDebugMenu : MainEvent()
|
||||
|
||||
/**
|
||||
* Show a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(val message: Text) : MainEvent()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
@@ -11,6 +13,13 @@ import kotlinx.coroutines.flow.Flow
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AuthDiskSource {
|
||||
|
||||
/**
|
||||
* The currently persisted authenticator sync symmetric key. This key is used for
|
||||
* encrypting IPC traffic.
|
||||
*/
|
||||
var authenticatorSyncSymmetricKey: ByteArray?
|
||||
|
||||
/**
|
||||
* Retrieves a unique ID for the application that is stored locally. This will generate a new
|
||||
* one if it does not yet exist and it will only be reset for new installs or when clearing
|
||||
@@ -45,12 +54,49 @@ interface AuthDiskSource {
|
||||
*/
|
||||
fun clearData(userId: String)
|
||||
|
||||
/**
|
||||
* Get the authenticator sync unlock key. Null means there is no key, which means the user
|
||||
* has not enabled authenticator syncing
|
||||
*/
|
||||
fun getAuthenticatorSyncUnlockKey(userId: String): String?
|
||||
|
||||
/**
|
||||
* Store the authenticator sync unlock key. Storing a null key effectively disables
|
||||
* authenticator syncing.
|
||||
*/
|
||||
fun storeAuthenticatorSyncUnlockKey(userId: String, authenticatorSyncUnlockKey: String?)
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user should use a key connector.
|
||||
*/
|
||||
fun getShouldUseKeyConnector(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user should use a key connector as a flow.
|
||||
*/
|
||||
fun getShouldUseKeyConnectorFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Stores the boolean indicating that the user should use a key connector.
|
||||
*/
|
||||
fun storeShouldUseKeyConnector(userId: String, shouldUseKeyConnector: Boolean?)
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user has completed login with TDE.
|
||||
*/
|
||||
fun getIsTdeLoginComplete(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the boolean indicating that the user has completed login with TDE.
|
||||
*/
|
||||
fun storeIsTdeLoginComplete(userId: String, isTdeLoginComplete: Boolean?)
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user has chosen to trust this device.
|
||||
*
|
||||
* Note: This indicates intent to trust the device, the device may not be trusted yet.
|
||||
*/
|
||||
fun getShouldTrustDevice(userId: String): Boolean
|
||||
fun getShouldTrustDevice(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the boolean indicating that the user has chosen to trust this device for the given
|
||||
@@ -136,6 +182,11 @@ interface AuthDiskSource {
|
||||
*/
|
||||
fun storeUserBiometricUnlockKey(userId: String, biometricsKey: String?)
|
||||
|
||||
/**
|
||||
* Gets the flow for the biometrics key for the given [userId].
|
||||
*/
|
||||
fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?>
|
||||
|
||||
/**
|
||||
* Retrieves a pin-protected user key for the given [userId].
|
||||
*/
|
||||
@@ -153,6 +204,11 @@ interface AuthDiskSource {
|
||||
inMemoryOnly: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieves a flow for the pin-protected user key for the given [userId].
|
||||
*/
|
||||
fun getPinProtectedUserKeyFlow(userId: String): Flow<String?>
|
||||
|
||||
/**
|
||||
* Gets a two-factor auth token using a user's [email].
|
||||
*/
|
||||
@@ -245,4 +301,45 @@ interface AuthDiskSource {
|
||||
* Stores the [accountTokens] for the given [userId].
|
||||
*/
|
||||
fun storeAccountTokens(userId: String, accountTokens: AccountTokensJson?)
|
||||
|
||||
/**
|
||||
* Gets the onboarding status for the given [userId].
|
||||
*/
|
||||
fun getOnboardingStatus(userId: String): OnboardingStatus?
|
||||
|
||||
/**
|
||||
* Stores the [onboardingStatus] for the given [userId].
|
||||
*/
|
||||
fun storeOnboardingStatus(userId: String, onboardingStatus: OnboardingStatus?)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getOnboardingStatus]. This will replay the last known value,
|
||||
* if any exists.
|
||||
*/
|
||||
fun getOnboardingStatusFlow(userId: String): Flow<OnboardingStatus?>
|
||||
|
||||
/**
|
||||
* Gets the show import logins flag for the given [userId].
|
||||
*/
|
||||
fun getShowImportLogins(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the show import logins flag for the given [userId].
|
||||
*/
|
||||
fun storeShowImportLogins(userId: String, showImportLogins: Boolean?)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getShowImportLogins]. This will replay the last known value.
|
||||
*/
|
||||
fun getShowImportLoginsFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets the new device notice state for the given [userId].
|
||||
*/
|
||||
fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState
|
||||
|
||||
/**
|
||||
* Stores the new device notice state for the given [userId].
|
||||
*/
|
||||
fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseEncryptedDiskSource
|
||||
@@ -18,6 +21,8 @@ import java.util.UUID
|
||||
|
||||
// These keys should be encrypted
|
||||
private const val ACCOUNT_TOKENS_KEY = "accountTokens"
|
||||
private const val AUTHENTICATOR_SYNC_SYMMETRIC_KEY = "authenticatorSyncSymmetric"
|
||||
private const val AUTHENTICATOR_SYNC_UNLOCK_KEY = "authenticatorSyncUnlock"
|
||||
private const val BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock"
|
||||
private const val USER_AUTO_UNLOCK_KEY_KEY = "userKeyAutoUnlock"
|
||||
private const val DEVICE_KEY_KEY = "deviceKey"
|
||||
@@ -39,6 +44,11 @@ private const val TWO_FACTOR_TOKEN_KEY = "twoFactorToken"
|
||||
private const val MASTER_PASSWORD_HASH_KEY = "keyHash"
|
||||
private const val POLICIES_KEY = "policies"
|
||||
private const val SHOULD_TRUST_DEVICE_KEY = "shouldTrustDevice"
|
||||
private const val TDE_LOGIN_COMPLETE = "tdeLoginComplete"
|
||||
private const val USES_KEY_CONNECTOR = "usesKeyConnector"
|
||||
private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
|
||||
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
|
||||
private const val NEW_DEVICE_NOTICE_STATE = "newDeviceNoticeState"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@@ -56,12 +66,21 @@ class AuthDiskSourceImpl(
|
||||
AuthDiskSource {
|
||||
|
||||
private val inMemoryPinProtectedUserKeys = mutableMapOf<String, String?>()
|
||||
private val mutableShouldUseKeyConnectorFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
private val mutableOrganizationsFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()
|
||||
private val mutablePoliciesFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Policy>?>>()
|
||||
private val mutableAccountTokensFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
|
||||
private val mutableOnboardingStatusFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
|
||||
private val mutableShowImportLoginsFlowMap = mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
private val mutableBiometricUnlockKeyFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<String?>>()
|
||||
private val mutablePinProtectedUserKeyFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<String?>>()
|
||||
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
|
||||
|
||||
override var userState: UserStateJson?
|
||||
@@ -84,6 +103,14 @@ class AuthDiskSourceImpl(
|
||||
migrateAccountTokens()
|
||||
}
|
||||
|
||||
override var authenticatorSyncSymmetricKey: ByteArray?
|
||||
set(value) {
|
||||
val asString = value?.let { value.toString(Charsets.ISO_8859_1) }
|
||||
putEncryptedString(AUTHENTICATOR_SYNC_SYMMETRIC_KEY, asString)
|
||||
}
|
||||
get() = getEncryptedString(AUTHENTICATOR_SYNC_SYMMETRIC_KEY)
|
||||
?.toByteArray(Charsets.ISO_8859_1)
|
||||
|
||||
override val uniqueAppId: String
|
||||
get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId()
|
||||
|
||||
@@ -122,15 +149,56 @@ class AuthDiskSourceImpl(
|
||||
storeMasterPasswordHash(userId = userId, passwordHash = null)
|
||||
storePolicies(userId = userId, policies = null)
|
||||
storeAccountTokens(userId = userId, accountTokens = null)
|
||||
storeShouldUseKeyConnector(userId = userId, shouldUseKeyConnector = null)
|
||||
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
|
||||
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
|
||||
storeShowImportLogins(userId = userId, showImportLogins = null)
|
||||
|
||||
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
|
||||
// indefinitely unless the TDE flow explicitly removes them.
|
||||
// Do not remove OnboardingStatus we want to keep track of this even after logout.
|
||||
}
|
||||
|
||||
override fun getShouldTrustDevice(userId: String): Boolean =
|
||||
requireNotNull(
|
||||
getBoolean(key = SHOULD_TRUST_DEVICE_KEY.appendIdentifier(userId), default = false),
|
||||
override fun getAuthenticatorSyncUnlockKey(userId: String): String? =
|
||||
getEncryptedString(AUTHENTICATOR_SYNC_UNLOCK_KEY.appendIdentifier(userId))
|
||||
|
||||
override fun storeAuthenticatorSyncUnlockKey(
|
||||
userId: String,
|
||||
authenticatorSyncUnlockKey: String?,
|
||||
) {
|
||||
putEncryptedString(
|
||||
key = AUTHENTICATOR_SYNC_UNLOCK_KEY.appendIdentifier(userId),
|
||||
value = authenticatorSyncUnlockKey,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getShouldUseKeyConnectorFlow(userId: String): Flow<Boolean?> =
|
||||
getMutableShouldUseKeyConnectorFlowMap(userId = userId)
|
||||
.onSubscription { emit(getShouldUseKeyConnector(userId = userId)) }
|
||||
|
||||
override fun getShouldUseKeyConnector(
|
||||
userId: String,
|
||||
): Boolean? = getBoolean(key = USES_KEY_CONNECTOR.appendIdentifier(userId))
|
||||
|
||||
override fun storeShouldUseKeyConnector(userId: String, shouldUseKeyConnector: Boolean?) {
|
||||
putBoolean(
|
||||
key = USES_KEY_CONNECTOR.appendIdentifier(userId),
|
||||
value = shouldUseKeyConnector,
|
||||
)
|
||||
getMutableShouldUseKeyConnectorFlowMap(userId = userId).tryEmit(shouldUseKeyConnector)
|
||||
}
|
||||
|
||||
override fun getIsTdeLoginComplete(
|
||||
userId: String,
|
||||
): Boolean? = getBoolean(key = TDE_LOGIN_COMPLETE.appendIdentifier(userId))
|
||||
|
||||
override fun storeIsTdeLoginComplete(userId: String, isTdeLoginComplete: Boolean?) {
|
||||
putBoolean(TDE_LOGIN_COMPLETE.appendIdentifier(userId), isTdeLoginComplete)
|
||||
}
|
||||
|
||||
override fun getShouldTrustDevice(
|
||||
userId: String,
|
||||
): Boolean? = getBoolean(key = SHOULD_TRUST_DEVICE_KEY.appendIdentifier(userId))
|
||||
|
||||
override fun storeShouldTrustDevice(userId: String, shouldTrustDevice: Boolean?) {
|
||||
putBoolean(SHOULD_TRUST_DEVICE_KEY.appendIdentifier(userId), shouldTrustDevice)
|
||||
@@ -223,8 +291,13 @@ class AuthDiskSourceImpl(
|
||||
key = BIOMETRICS_UNLOCK_KEY.appendIdentifier(userId),
|
||||
value = biometricsKey,
|
||||
)
|
||||
getMutableBiometricUnlockKeyFlow(userId).tryEmit(biometricsKey)
|
||||
}
|
||||
|
||||
override fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?> =
|
||||
getMutableBiometricUnlockKeyFlow(userId)
|
||||
.onSubscription { emit(getUserBiometricUnlockKey(userId = userId)) }
|
||||
|
||||
override fun getPinProtectedUserKey(userId: String): String? =
|
||||
inMemoryPinProtectedUserKeys[userId]
|
||||
?: getString(key = PIN_PROTECTED_USER_KEY_KEY.appendIdentifier(userId))
|
||||
@@ -240,8 +313,13 @@ class AuthDiskSourceImpl(
|
||||
key = PIN_PROTECTED_USER_KEY_KEY.appendIdentifier(userId),
|
||||
value = pinProtectedUserKey,
|
||||
)
|
||||
getMutablePinProtectedUserKeyFlow(userId).tryEmit(pinProtectedUserKey)
|
||||
}
|
||||
|
||||
override fun getPinProtectedUserKeyFlow(userId: String): Flow<String?> =
|
||||
getMutablePinProtectedUserKeyFlow(userId)
|
||||
.onSubscription { emit(getPinProtectedUserKey(userId = userId)) }
|
||||
|
||||
override fun getTwoFactorToken(email: String): String? =
|
||||
getString(key = TWO_FACTOR_TOKEN_KEY.appendIdentifier(email))
|
||||
|
||||
@@ -361,6 +439,57 @@ class AuthDiskSourceImpl(
|
||||
getMutableAccountTokensFlow(userId = userId).tryEmit(accountTokens)
|
||||
}
|
||||
|
||||
override fun getOnboardingStatus(userId: String): OnboardingStatus? {
|
||||
return getString(key = ONBOARDING_STATUS_KEY.appendIdentifier(userId))?.let {
|
||||
json.decodeFromStringOrNull(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeOnboardingStatus(userId: String, onboardingStatus: OnboardingStatus?) {
|
||||
putString(
|
||||
key = ONBOARDING_STATUS_KEY.appendIdentifier(userId),
|
||||
value = onboardingStatus?.let { json.encodeToString(it) },
|
||||
)
|
||||
getMutableOnboardingStatusFlow(userId = userId).tryEmit(onboardingStatus)
|
||||
}
|
||||
|
||||
override fun getOnboardingStatusFlow(userId: String): Flow<OnboardingStatus?> {
|
||||
return getMutableOnboardingStatusFlow(userId = userId)
|
||||
.onSubscription { emit(getOnboardingStatus(userId = userId)) }
|
||||
}
|
||||
|
||||
override fun getShowImportLogins(userId: String): Boolean? {
|
||||
return getBoolean(SHOW_IMPORT_LOGINS_KEY.appendIdentifier(userId))
|
||||
}
|
||||
|
||||
override fun storeShowImportLogins(userId: String, showImportLogins: Boolean?) {
|
||||
putBoolean(
|
||||
key = SHOW_IMPORT_LOGINS_KEY.appendIdentifier(userId),
|
||||
value = showImportLogins,
|
||||
)
|
||||
getMutableShowImportLoginsFlow(userId = userId).tryEmit(showImportLogins)
|
||||
}
|
||||
|
||||
override fun getShowImportLoginsFlow(userId: String): Flow<Boolean?> =
|
||||
getMutableShowImportLoginsFlow(userId)
|
||||
.onSubscription { emit(getShowImportLogins(userId)) }
|
||||
|
||||
override fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState {
|
||||
return getString(key = NEW_DEVICE_NOTICE_STATE.appendIdentifier(userId))?.let {
|
||||
json.decodeFromStringOrNull(it)
|
||||
} ?: NewDeviceNoticeState(
|
||||
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
|
||||
lastSeenDate = null,
|
||||
)
|
||||
}
|
||||
|
||||
override fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?) {
|
||||
putString(
|
||||
key = NEW_DEVICE_NOTICE_STATE.appendIdentifier(userId),
|
||||
value = newState?.let { json.encodeToString(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
@@ -369,6 +498,20 @@ class AuthDiskSourceImpl(
|
||||
putString(key = UNIQUE_APP_ID_KEY, value = it)
|
||||
}
|
||||
|
||||
private fun getMutableOnboardingStatusFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<OnboardingStatus?> =
|
||||
mutableOnboardingStatusFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableShouldUseKeyConnectorFlowMap(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> =
|
||||
mutableShouldUseKeyConnectorFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableOrganizationsFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?> =
|
||||
@@ -390,6 +533,24 @@ class AuthDiskSourceImpl(
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableShowImportLoginsFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> = mutableShowImportLoginsFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableBiometricUnlockKeyFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<String?> = mutableBiometricUnlockKeyFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutablePinProtectedUserKeyFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<String?> = mutablePinProtectedUserKeyFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun migrateAccountTokens() {
|
||||
userState
|
||||
?.accounts
|
||||
|
||||
@@ -2,8 +2,12 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.model
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Represents the current account information for a given user.
|
||||
@@ -33,6 +37,7 @@ data class AccountJson(
|
||||
* @property userId The ID of the user.
|
||||
* @property email The user's email address.
|
||||
* @property isEmailVerified Whether or not the user's email is verified.
|
||||
* @property isTwoFactorEnabled If the profile has two factor authentication enabled.
|
||||
* @property name The user's name (if applicable).
|
||||
* @property stamp The account's security stamp (if applicable).
|
||||
* @property organizationId The ID of the associated organization (if applicable).
|
||||
@@ -44,7 +49,9 @@ data class AccountJson(
|
||||
* @property kdfMemory The amount of memory to use when calculating a password hash (MB).
|
||||
* @property kdfParallelism The number of threads to use when calculating a password hash.
|
||||
* @property userDecryptionOptions The options available to a user for decryption.
|
||||
* @property creationDate The creation date of the account.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class Profile(
|
||||
@SerialName("userId")
|
||||
@@ -56,6 +63,9 @@ data class AccountJson(
|
||||
@SerialName("emailVerified")
|
||||
val isEmailVerified: Boolean?,
|
||||
|
||||
@SerialName("isTwoFactorEnabled")
|
||||
val isTwoFactorEnabled: Boolean?,
|
||||
|
||||
@SerialName("name")
|
||||
val name: String?,
|
||||
|
||||
@@ -86,8 +96,13 @@ data class AccountJson(
|
||||
@SerialName("kdfParallelism")
|
||||
val kdfParallelism: Int?,
|
||||
|
||||
@SerialName("accountDecryptionOptions")
|
||||
@SerialName("userDecryptionOptions")
|
||||
@JsonNames("accountDecryptionOptions")
|
||||
val userDecryptionOptions: UserDecryptionOptionsJson?,
|
||||
|
||||
@SerialName("creationDate")
|
||||
@Contextual
|
||||
val creationDate: ZonedDateTime?,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,7 @@ data class EnvironmentUrlDataJson(
|
||||
@SerialName("events")
|
||||
val events: String? = null,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
/**
|
||||
* Default [EnvironmentUrlDataJson] for the US region.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk.model
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Describes the current display status of the new device notice screen.
|
||||
*/
|
||||
@Serializable
|
||||
enum class NewDeviceNoticeDisplayStatus {
|
||||
/**
|
||||
* The user has seen the screen and indicated they can access their email.
|
||||
*/
|
||||
@SerialName("canAccessEmail")
|
||||
CAN_ACCESS_EMAIL,
|
||||
|
||||
/**
|
||||
* The user has indicated they can access their email
|
||||
* as specified by the Permanent mode of the notice.
|
||||
*/
|
||||
@SerialName("canAccessEmailPermanent")
|
||||
CAN_ACCESS_EMAIL_PERMANENT,
|
||||
|
||||
/**
|
||||
* The user has not seen the screen.
|
||||
*/
|
||||
@SerialName("hasNotSeen")
|
||||
HAS_NOT_SEEN,
|
||||
|
||||
/**
|
||||
* The user has seen the screen and selected "remind me later".
|
||||
*/
|
||||
@SerialName("hasSeen")
|
||||
HAS_SEEN,
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the new device notice screen.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@Serializable
|
||||
data class NewDeviceNoticeState(
|
||||
@SerialName("displayStatus")
|
||||
val displayStatus: NewDeviceNoticeDisplayStatus,
|
||||
|
||||
@SerialName("lastSeenDate")
|
||||
@Contextual
|
||||
val lastSeenDate: ZonedDateTime?,
|
||||
) {
|
||||
/**
|
||||
* Whether the [lastSeenDate] is at least 7 days old.
|
||||
*/
|
||||
val shouldDisplayNoticeIfSeen = lastSeenDate
|
||||
?.isBefore(
|
||||
ZonedDateTime.now().minusDays(7),
|
||||
)
|
||||
?: false
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Describes the current status of a user in the account onboarding steps.
|
||||
*/
|
||||
@Serializable
|
||||
enum class OnboardingStatus {
|
||||
|
||||
/**
|
||||
* Onboarding has not yet started.
|
||||
*/
|
||||
@SerialName("notStarted")
|
||||
NOT_STARTED,
|
||||
|
||||
/**
|
||||
* The user is completing the account lock setup.
|
||||
*/
|
||||
@SerialName("accountLockSetup")
|
||||
ACCOUNT_LOCK_SETUP,
|
||||
|
||||
/**
|
||||
* The user is completing the auto fill service setup.
|
||||
*/
|
||||
@SerialName("autofillSetup")
|
||||
AUTOFILL_SETUP,
|
||||
|
||||
/**
|
||||
* The user is completing the final step of the onboarding process.
|
||||
*/
|
||||
@SerialName("finalStep")
|
||||
FINAL_STEP,
|
||||
|
||||
/**
|
||||
* The user has completed all onboarding steps.
|
||||
*/
|
||||
@SerialName("complete")
|
||||
COMPLETE,
|
||||
}
|
||||
@@ -7,13 +7,21 @@ import kotlinx.serialization.Serializable
|
||||
* Container for the user's API tokens.
|
||||
*
|
||||
* @property requestId The ID of the pending Auth Request.
|
||||
* @property requestPrivateKey The private of the pending Auth Request.
|
||||
* @property requestPrivateKey The private key of the pending Auth Request.
|
||||
* @property requestAccessCode The access code of the pending Auth Request.
|
||||
* @property requestFingerprint The fingerprint of the pending Auth Request.
|
||||
*/
|
||||
@Serializable
|
||||
data class PendingAuthRequestJson(
|
||||
@SerialName("Id")
|
||||
@SerialName("id")
|
||||
val requestId: String,
|
||||
|
||||
@SerialName("PrivateKey")
|
||||
@SerialName("privateKey")
|
||||
val requestPrivateKey: String,
|
||||
|
||||
@SerialName("accessCode")
|
||||
val requestAccessCode: String,
|
||||
|
||||
@SerialName("fingerprint")
|
||||
val requestFingerprint: String,
|
||||
)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /accounts API.
|
||||
*/
|
||||
interface AccountsApi {
|
||||
@POST("/accounts/password-hint")
|
||||
suspend fun passwordHintRequest(
|
||||
@Body body: PasswordHintRequestJson,
|
||||
): Result<Unit>
|
||||
|
||||
@POST("/two-factor/send-email-login")
|
||||
suspend fun resendVerificationCodeEmail(
|
||||
@Body body: ResendEmailRequestJson,
|
||||
): Result<Unit>
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountReque
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.HTTP
|
||||
import retrofit2.http.POST
|
||||
@@ -13,41 +14,48 @@ import retrofit2.http.POST
|
||||
* Defines raw calls under the /accounts API with authentication applied.
|
||||
*/
|
||||
interface AuthenticatedAccountsApi {
|
||||
|
||||
/**
|
||||
* Converts the currently active account to a key-connector account.
|
||||
*/
|
||||
@POST("/accounts/convert-to-key-connector")
|
||||
suspend fun convertToKeyConnector(): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Creates the keys for the current account.
|
||||
*/
|
||||
@POST("/accounts/keys")
|
||||
suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): Result<Unit>
|
||||
suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Deletes the current account.
|
||||
*/
|
||||
@HTTP(method = "DELETE", path = "/accounts", hasBody = true)
|
||||
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): Result<Unit>
|
||||
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): NetworkResult<Unit>
|
||||
|
||||
@POST("/accounts/request-otp")
|
||||
suspend fun requestOtp(): Result<Unit>
|
||||
suspend fun requestOtp(): NetworkResult<Unit>
|
||||
|
||||
@POST("/accounts/verify-otp")
|
||||
suspend fun verifyOtp(
|
||||
@Body body: VerifyOtpRequestJson,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Resets the temporary password.
|
||||
*/
|
||||
@HTTP(method = "PUT", path = "/accounts/update-temp-password", hasBody = true)
|
||||
suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
|
||||
suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Resets the password.
|
||||
*/
|
||||
@HTTP(method = "POST", path = "/accounts/password", hasBody = true)
|
||||
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
|
||||
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Sets the password.
|
||||
*/
|
||||
@POST("/accounts/set-password")
|
||||
suspend fun setPassword(@Body body: SetPasswordRequestJson): Result<Unit>
|
||||
suspend fun setPassword(@Body body: SetPasswordRequestJson): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
@@ -22,7 +23,7 @@ interface AuthenticatedAuthRequestsApi {
|
||||
suspend fun createAdminAuthRequest(
|
||||
@Header("Device-Identifier") deviceIdentifier: String,
|
||||
@Body body: AuthRequestRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Updates an authentication request.
|
||||
@@ -31,13 +32,13 @@ interface AuthenticatedAuthRequestsApi {
|
||||
suspend fun updateAuthRequest(
|
||||
@Path("id") userId: String,
|
||||
@Body body: AuthRequestUpdateRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Gets a list of auth requests for this device.
|
||||
*/
|
||||
@GET("/auth-requests")
|
||||
suspend fun getAuthRequests(): Result<AuthRequestsResponseJson>
|
||||
suspend fun getAuthRequests(): NetworkResult<AuthRequestsResponseJson>
|
||||
|
||||
/**
|
||||
* Retrieves an existing authentication request by ID.
|
||||
@@ -45,5 +46,5 @@ interface AuthenticatedAuthRequestsApi {
|
||||
@GET("/auth-requests/{requestId}")
|
||||
suspend fun getAuthRequest(
|
||||
@Path("requestId") requestId: String,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
@@ -16,5 +17,5 @@ interface AuthenticatedDevicesApi {
|
||||
suspend fun updateTrustedDeviceKeys(
|
||||
@Path(value = "appId") appId: String,
|
||||
@Body request: TrustedDeviceKeysRequestJson,
|
||||
): Result<TrustedDeviceKeysResponseJson>
|
||||
): NetworkResult<TrustedDeviceKeysResponseJson>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
/**
|
||||
* Defines raw calls specific for key connectors that use custom urls.
|
||||
*/
|
||||
@Keep
|
||||
interface AuthenticatedKeyConnectorApi {
|
||||
@POST
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
@Url url: String,
|
||||
@Body body: KeyConnectorMasterKeyRequestJson,
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.PUT
|
||||
@@ -20,7 +21,7 @@ interface AuthenticatedOrganizationApi {
|
||||
@Path("orgId") organizationId: String,
|
||||
@Path("userId") userId: String,
|
||||
@Body body: OrganizationResetPasswordEnrollRequestJson,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Checks whether this organization auto enrolls users in password reset.
|
||||
@@ -28,7 +29,7 @@ interface AuthenticatedOrganizationApi {
|
||||
@GET("/organizations/{identifier}/auto-enroll-status")
|
||||
suspend fun getOrganizationAutoEnrollResponse(
|
||||
@Path("identifier") organizationIdentifier: String,
|
||||
): Result<OrganizationAutoEnrollStatusResponseJson>
|
||||
): NetworkResult<OrganizationAutoEnrollStatusResponseJson>
|
||||
|
||||
/**
|
||||
* Gets the public and private keys for this organization.
|
||||
@@ -36,5 +37,5 @@ interface AuthenticatedOrganizationApi {
|
||||
@GET("/organizations/{id}/keys")
|
||||
suspend fun getOrganizationKeys(
|
||||
@Path("id") organizationId: String,
|
||||
): Result<OrganizationKeysResponseJson>
|
||||
): NetworkResult<OrganizationKeysResponseJson>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
@@ -14,5 +15,5 @@ interface HaveIBeenPwnedApi {
|
||||
suspend fun fetchBreachedPasswords(
|
||||
@Path("hashPrefix")
|
||||
hashPrefix: String,
|
||||
): Result<ResponseBody>
|
||||
): NetworkResult<ResponseBody>
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /organizations API.
|
||||
*/
|
||||
interface OrganizationApi {
|
||||
/**
|
||||
* Checks for the claimed domain organization of an email for SSO purposes.
|
||||
*/
|
||||
@POST("/organizations/domain/sso/details")
|
||||
suspend fun getClaimedDomainOrganizationDetails(
|
||||
@Body body: OrganizationDomainSsoDetailsRequestJson,
|
||||
): Result<OrganizationDomainSsoDetailsResponseJson>
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /accounts API.
|
||||
*/
|
||||
interface UnauthenticatedAccountsApi {
|
||||
@POST("/accounts/password-hint")
|
||||
suspend fun passwordHintRequest(
|
||||
@Body body: PasswordHintRequestJson,
|
||||
): NetworkResult<Unit>
|
||||
|
||||
@POST("/two-factor/send-email-login")
|
||||
suspend fun resendVerificationCodeEmail(
|
||||
@Body body: ResendEmailRequestJson,
|
||||
): NetworkResult<Unit>
|
||||
|
||||
@POST("/accounts/set-key-connector-key")
|
||||
suspend fun setKeyConnectorKey(
|
||||
@Body body: KeyConnectorKeyRequestJson,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
@@ -21,7 +22,7 @@ interface UnauthenticatedAuthRequestsApi {
|
||||
suspend fun createAuthRequest(
|
||||
@Header("Device-Identifier") deviceIdentifier: String,
|
||||
@Body body: AuthRequestRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Queries for updates to a given auth request.
|
||||
@@ -30,5 +31,5 @@ interface UnauthenticatedAuthRequestsApi {
|
||||
suspend fun getAuthRequestUpdate(
|
||||
@Path("requestId") requestId: String,
|
||||
@Query("code") accessCode: String,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
|
||||
@@ -11,5 +12,5 @@ interface UnauthenticatedDevicesApi {
|
||||
suspend fun getIsKnownDevice(
|
||||
@Header(value = "X-Request-Email") emailAddress: String,
|
||||
@Header(value = "X-Device-Identifier") deviceId: String,
|
||||
): Result<Boolean>
|
||||
): NetworkResult<Boolean>
|
||||
}
|
||||
|
||||
@@ -5,8 +5,13 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJso
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Field
|
||||
@@ -19,7 +24,7 @@ import retrofit2.http.Query
|
||||
/**
|
||||
* Defines raw calls under the /identity API.
|
||||
*/
|
||||
interface IdentityApi {
|
||||
interface UnauthenticatedIdentityApi {
|
||||
|
||||
@POST("/connect/token")
|
||||
@Suppress("LongParameterList")
|
||||
@@ -42,12 +47,12 @@ interface IdentityApi {
|
||||
@Field(value = "twoFactorProvider") twoFactorMethod: String?,
|
||||
@Field(value = "twoFactorRemember") twoFactorRemember: String?,
|
||||
@Field(value = "authRequest") authRequestId: String?,
|
||||
): Result<GetTokenResponseJson.Success>
|
||||
): NetworkResult<GetTokenResponseJson.Success>
|
||||
|
||||
@GET("/sso/prevalidate")
|
||||
suspend fun prevalidateSso(
|
||||
@Query("domainHint") organizationIdentifier: String,
|
||||
): Result<PrevalidateSsoResponseJson>
|
||||
): NetworkResult<PrevalidateSsoResponseJson>
|
||||
|
||||
/**
|
||||
* This call needs to be synchronous so we need it to return a [Call] directly. The identity
|
||||
@@ -62,8 +67,25 @@ interface IdentityApi {
|
||||
): Call<RefreshTokenResponseJson>
|
||||
|
||||
@POST("/accounts/prelogin")
|
||||
suspend fun preLogin(@Body body: PreLoginRequestJson): Result<PreLoginResponseJson>
|
||||
suspend fun preLogin(@Body body: PreLoginRequestJson): NetworkResult<PreLoginResponseJson>
|
||||
|
||||
@POST("/accounts/register")
|
||||
suspend fun register(@Body body: RegisterRequestJson): Result<RegisterResponseJson.Success>
|
||||
suspend fun register(
|
||||
@Body body: RegisterRequestJson,
|
||||
): NetworkResult<RegisterResponseJson.Success>
|
||||
|
||||
@POST("/accounts/register/finish")
|
||||
suspend fun registerFinish(
|
||||
@Body body: RegisterFinishRequestJson,
|
||||
): NetworkResult<RegisterResponseJson.Success>
|
||||
|
||||
@POST("/accounts/register/send-verification-email")
|
||||
suspend fun sendVerificationEmail(
|
||||
@Body body: SendVerificationEmailRequestJson,
|
||||
): NetworkResult<JsonPrimitive?>
|
||||
|
||||
@POST("/accounts/register/verification-email-clicked")
|
||||
suspend fun verifyEmailToken(
|
||||
@Body body: VerifyEmailTokenRequestJson,
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
/**
|
||||
* Defines raw calls specific for key connectors that use custom urls.
|
||||
*/
|
||||
@Keep
|
||||
interface UnauthenticatedKeyConnectorApi {
|
||||
@POST
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
@Url url: String,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
@Body body: KeyConnectorMasterKeyRequestJson,
|
||||
): NetworkResult<Unit>
|
||||
|
||||
@GET
|
||||
suspend fun getMasterKeyFromKeyConnector(
|
||||
@Url url: String,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
): NetworkResult<KeyConnectorMasterKeyResponseJson>
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /organizations API.
|
||||
*/
|
||||
interface UnauthenticatedOrganizationApi {
|
||||
/**
|
||||
* Checks for the claimed domain organization of an email for SSO purposes.
|
||||
*/
|
||||
@POST("/organizations/domain/sso/details")
|
||||
suspend fun getClaimedDomainOrganizationDetails(
|
||||
@Body body: OrganizationDomainSsoDetailsRequestJson,
|
||||
): NetworkResult<OrganizationDomainSsoDetailsResponseJson>
|
||||
|
||||
/**
|
||||
* Checks for the verfied organization domains of an email for SSO purposes.
|
||||
*/
|
||||
@POST("/organizations/domain/sso/verified")
|
||||
suspend fun getVerifiedOrganizationDomainsByEmail(
|
||||
@Body body: VerifiedOrganizationDomainSsoDetailsRequest,
|
||||
): NetworkResult<VerifiedOrganizationDomainSsoDetailsResponse>
|
||||
}
|
||||
@@ -36,8 +36,12 @@ object AuthNetworkModule {
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): AccountsService = AccountsServiceImpl(
|
||||
accountsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
unauthenticatedAccountsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
authenticatedAccountsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedKeyConnectorApi = retrofits.createStaticRetrofit().create(),
|
||||
authenticatedKeyConnectorApi = retrofits
|
||||
.createStaticRetrofit(isAuthenticated = true)
|
||||
.create(),
|
||||
json = json,
|
||||
)
|
||||
|
||||
@@ -64,7 +68,7 @@ object AuthNetworkModule {
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): IdentityService = IdentityServiceImpl(
|
||||
api = retrofits.unauthenticatedIdentityRetrofit.create(),
|
||||
unauthenticatedIdentityApi = retrofits.unauthenticatedIdentityRetrofit.create(),
|
||||
json = json,
|
||||
)
|
||||
|
||||
@@ -73,10 +77,8 @@ object AuthNetworkModule {
|
||||
fun providesHaveIBeenPwnedService(
|
||||
retrofits: Retrofits,
|
||||
): HaveIBeenPwnedService = HaveIBeenPwnedServiceImpl(
|
||||
retrofits
|
||||
.staticRetrofitBuilder
|
||||
.baseUrl("https://api.pwnedpasswords.com")
|
||||
.build()
|
||||
api = retrofits
|
||||
.createStaticRetrofit(baseUrl = "https://api.pwnedpasswords.com")
|
||||
.create(),
|
||||
)
|
||||
|
||||
@@ -95,6 +97,6 @@ object AuthNetworkModule {
|
||||
retrofits: Retrofits,
|
||||
): OrganizationService = OrganizationServiceImpl(
|
||||
authenticatedOrganizationApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
organizationApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
unauthenticatedOrganizationApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ sealed class GetTokenResponseJson {
|
||||
* this token will be cached and used for future auth requests.
|
||||
* @property masterPasswordPolicyOptions The options available for a user's master password.
|
||||
* @property userDecryptionOptions The options available to a user for decryption.
|
||||
* @property keyConnectorUrl URL to the user's key connector.
|
||||
*/
|
||||
@Serializable
|
||||
data class Success(
|
||||
@@ -75,6 +76,9 @@ sealed class GetTokenResponseJson {
|
||||
|
||||
@SerialName("UserDecryptionOptions")
|
||||
val userDecryptionOptions: UserDecryptionOptionsJson?,
|
||||
|
||||
@SerialName("KeyConnectorUrl")
|
||||
val keyConnectorUrl: String?,
|
||||
) : GetTokenResponseJson()
|
||||
|
||||
/**
|
||||
@@ -92,9 +96,17 @@ sealed class GetTokenResponseJson {
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("ErrorModel")
|
||||
val errorModel: ErrorModel,
|
||||
val errorModel: ErrorModel?,
|
||||
@SerialName("errorModel")
|
||||
val legacyErrorModel: LegacyErrorModel?,
|
||||
) : GetTokenResponseJson() {
|
||||
|
||||
/**
|
||||
* The error message returned from the server, or null.
|
||||
*/
|
||||
val errorMessage: String?
|
||||
get() = errorModel?.errorMessage ?: legacyErrorModel?.errorMessage
|
||||
|
||||
/**
|
||||
* The error body of an invalid request containing a message.
|
||||
*/
|
||||
@@ -103,6 +115,18 @@ sealed class GetTokenResponseJson {
|
||||
@SerialName("Message")
|
||||
val errorMessage: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* The legacy error body of an invalid request containing a message.
|
||||
*
|
||||
* This model is used to support older versions of the error response model that used
|
||||
* lower-case keys.
|
||||
*/
|
||||
@Serializable
|
||||
data class LegacyErrorModel(
|
||||
@SerialName("message")
|
||||
val errorMessage: String,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the request body used to create the key connector keys for an account.
|
||||
*/
|
||||
@Serializable
|
||||
data class KeyConnectorKeyRequestJson(
|
||||
@SerialName("key") val userKey: String,
|
||||
@SerialName("keys") val keys: Keys,
|
||||
@SerialName("kdf") val kdfType: KdfTypeJson,
|
||||
@SerialName("kdfIterations") val kdfIterations: Int?,
|
||||
@SerialName("kdfMemory") val kdfMemory: Int?,
|
||||
@SerialName("kdfParallelism") val kdfParallelism: Int?,
|
||||
@SerialName("orgIdentifier") val organizationIdentifier: String,
|
||||
) {
|
||||
/**
|
||||
* A keys object containing public and private keys.
|
||||
*
|
||||
* @param publicKey the public key (encrypted).
|
||||
* @param encryptedPrivateKey the private key (encrypted).
|
||||
*/
|
||||
@Serializable
|
||||
data class Keys(
|
||||
@SerialName("publicKey")
|
||||
val publicKey: String,
|
||||
|
||||
@SerialName("encryptedPrivateKey")
|
||||
val encryptedPrivateKey: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the request body used to store the master key in the cloud.
|
||||
*/
|
||||
@Serializable
|
||||
data class KeyConnectorMasterKeyRequestJson(
|
||||
@SerialName("Key") val masterKey: String,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the response body used to retrieve the master key from the cloud.
|
||||
*/
|
||||
@Serializable
|
||||
data class KeyConnectorMasterKeyResponseJson(
|
||||
@SerialName("key") val masterKey: String,
|
||||
)
|
||||
@@ -1,15 +1,19 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
|
||||
/**
|
||||
* Decryption options related to a user's key connector.
|
||||
*
|
||||
* @property keyConnectorUrl URL to the user's key connector.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class KeyConnectorUserDecryptionOptionsJson(
|
||||
@SerialName("KeyConnectorUrl")
|
||||
@SerialName("keyConnectorUrl")
|
||||
@JsonNames("KeyConnectorUrl")
|
||||
val keyConnectorUrl: String,
|
||||
)
|
||||
|
||||
@@ -9,17 +9,18 @@ import java.time.ZonedDateTime
|
||||
* Response object returned when requesting organization domain SSO details.
|
||||
*
|
||||
* @property isSsoAvailable Whether or not SSO is available for this domain.
|
||||
* @property domainName The organization's domain name.
|
||||
* @property organizationIdentifier The organization's identifier.
|
||||
* @property isSsoRequired Whether or not SSO is required.
|
||||
* @property verifiedDate The date these details were verified.
|
||||
* @property verifiedDate The date the domain was verified.
|
||||
*/
|
||||
@Serializable
|
||||
data class OrganizationDomainSsoDetailsResponseJson(
|
||||
@SerialName("ssoAvailable") val isSsoAvailable: Boolean,
|
||||
@SerialName("domainName") val domainName: String,
|
||||
@SerialName("organizationIdentifier") val organizationIdentifier: String,
|
||||
@SerialName("ssoRequired") val isSsoRequired: Boolean,
|
||||
@SerialName("ssoAvailable")
|
||||
val isSsoAvailable: Boolean,
|
||||
|
||||
@SerialName("organizationIdentifier")
|
||||
val organizationIdentifier: String,
|
||||
|
||||
@SerialName("verifiedDate")
|
||||
@Contextual
|
||||
@SerialName("verifiedDate") val verifiedDate: ZonedDateTime?,
|
||||
val verifiedDate: ZonedDateTime?,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson.Keys
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request body for register.
|
||||
*
|
||||
* @param email the email to be registered.
|
||||
* @param emailVerificationToken token used to finish the registration process.
|
||||
* @param masterPasswordHash the master password (encrypted).
|
||||
* @param masterPasswordHint the hint for the master password (nullable).
|
||||
* @param captchaResponse the captcha bypass token.
|
||||
* @param userSymmetricKey the user key for the request (encrypted).
|
||||
* @param userAsymmetricKeys a [Keys] object containing public and private keys.
|
||||
* @param kdfType the kdf type represented as an [Int].
|
||||
* @param kdfIterations the number of kdf iterations.
|
||||
*/
|
||||
@Serializable
|
||||
data class RegisterFinishRequestJson(
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
|
||||
@SerialName("emailVerificationToken")
|
||||
val emailVerificationToken: String,
|
||||
|
||||
@SerialName("masterPasswordHash")
|
||||
val masterPasswordHash: String,
|
||||
|
||||
@SerialName("masterPasswordHint")
|
||||
val masterPasswordHint: String?,
|
||||
|
||||
@SerialName("captchaResponse")
|
||||
val captchaResponse: String?,
|
||||
|
||||
@SerialName("userSymmetricKey")
|
||||
val userSymmetricKey: String,
|
||||
|
||||
@SerialName("userAsymmetricKeys")
|
||||
val userAsymmetricKeys: Keys,
|
||||
|
||||
@SerialName("kdf")
|
||||
val kdfType: KdfTypeJson,
|
||||
|
||||
@SerialName("kdfIterations")
|
||||
val kdfIterations: UInt,
|
||||
) {
|
||||
|
||||
/**
|
||||
* A keys object containing public and private keys.
|
||||
*
|
||||
* @param publicKey the public key (encrypted).
|
||||
* @param encryptedPrivateKey the private key (encrypted).
|
||||
*/
|
||||
@Serializable
|
||||
data class Keys(
|
||||
@SerialName("publicKey")
|
||||
val publicKey: String,
|
||||
|
||||
@SerialName("encryptedPrivateKey")
|
||||
val encryptedPrivateKey: String,
|
||||
)
|
||||
}
|
||||
@@ -46,7 +46,6 @@ sealed class RegisterResponseJson {
|
||||
/**
|
||||
* Represents the json body of an invalid register request.
|
||||
*
|
||||
* @param message
|
||||
* @param validationErrors a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
@@ -54,18 +53,17 @@ sealed class RegisterResponseJson {
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
val message: String?,
|
||||
private val invalidMessage: String? = null,
|
||||
|
||||
@SerialName("Message")
|
||||
private val errorMessage: String? = null,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: Map<String, List<String>>?,
|
||||
) : RegisterResponseJson()
|
||||
|
||||
/**
|
||||
* A different register error with a message.
|
||||
*/
|
||||
@Serializable
|
||||
data class Error(
|
||||
@SerialName("Message")
|
||||
val message: String?,
|
||||
) : RegisterResponseJson()
|
||||
) : RegisterResponseJson() {
|
||||
/**
|
||||
* A generic error message.
|
||||
*/
|
||||
val message: String? get() = invalidMessage ?: errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request body for send verification email.
|
||||
*
|
||||
* @param email the email to be registered.
|
||||
* @param name the name to be registered.
|
||||
* @param receiveMarketingEmails the answer to receive marketing emails.
|
||||
*/
|
||||
@Serializable
|
||||
data class SendVerificationEmailRequestJson(
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
|
||||
@SerialName("name")
|
||||
val name: String?,
|
||||
|
||||
@SerialName("receiveMarketingEmails")
|
||||
val receiveMarketingEmails: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The response body for sending a verification email.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class SendVerificationEmailResponseJson {
|
||||
|
||||
/**
|
||||
* Models a successful json response.
|
||||
*
|
||||
* @param emailVerificationToken the token to verify the email.
|
||||
*/
|
||||
@Serializable
|
||||
data class Success(
|
||||
val emailVerificationToken: String?,
|
||||
) : SendVerificationEmailResponseJson()
|
||||
|
||||
/**
|
||||
* Represents the json body of an invalid request.
|
||||
*
|
||||
* @param validationErrors a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
private val invalidMessage: String? = null,
|
||||
|
||||
@SerialName("Message")
|
||||
private val errorMessage: String? = null,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: Map<String, List<String>>?,
|
||||
) : SendVerificationEmailResponseJson() {
|
||||
/**
|
||||
* A generic error message.
|
||||
*/
|
||||
val message: String? get() = invalidMessage ?: errorMessage
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
|
||||
/**
|
||||
* Decryption options related to a user's trusted device.
|
||||
@@ -13,20 +15,26 @@ import kotlinx.serialization.Serializable
|
||||
* @property hasManageResetPasswordPermission Whether or not the user has manage reset password
|
||||
* permission.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class TrustedDeviceUserDecryptionOptionsJson(
|
||||
@SerialName("EncryptedPrivateKey")
|
||||
@SerialName("encryptedPrivateKey")
|
||||
@JsonNames("EncryptedPrivateKey")
|
||||
val encryptedPrivateKey: String?,
|
||||
|
||||
@SerialName("EncryptedUserKey")
|
||||
@SerialName("encryptedUserKey")
|
||||
@JsonNames("EncryptedUserKey")
|
||||
val encryptedUserKey: String?,
|
||||
|
||||
@SerialName("HasAdminApproval")
|
||||
@SerialName("hasAdminApproval")
|
||||
@JsonNames("HasAdminApproval")
|
||||
val hasAdminApproval: Boolean,
|
||||
|
||||
@SerialName("HasLoginApprovingDevice")
|
||||
@SerialName("hasLoginApprovingDevice")
|
||||
@JsonNames("HasLoginApprovingDevice")
|
||||
val hasLoginApprovingDevice: Boolean,
|
||||
|
||||
@SerialName("HasManageResetPasswordPermission")
|
||||
@SerialName("hasManageResetPasswordPermission")
|
||||
@JsonNames("HasManageResetPasswordPermission")
|
||||
val hasManageResetPasswordPermission: Boolean,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
|
||||
/**
|
||||
* The options available to a user for decryption.
|
||||
@@ -12,14 +14,18 @@ import kotlinx.serialization.Serializable
|
||||
* device.
|
||||
* @property keyConnectorUserDecryptionOptions Decryption options related to a user's key connector.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class UserDecryptionOptionsJson(
|
||||
@SerialName("HasMasterPassword")
|
||||
@SerialName("hasMasterPassword")
|
||||
@JsonNames("HasMasterPassword")
|
||||
val hasMasterPassword: Boolean,
|
||||
|
||||
@SerialName("TrustedDeviceOption")
|
||||
@SerialName("trustedDeviceOption")
|
||||
@JsonNames("TrustedDeviceOption")
|
||||
val trustedDeviceUserDecryptionOptions: TrustedDeviceUserDecryptionOptionsJson?,
|
||||
|
||||
@SerialName("KeyConnectorOption")
|
||||
@SerialName("keyConnectorOption")
|
||||
@JsonNames("KeyConnectorOption")
|
||||
val keyConnectorUserDecryptionOptions: KeyConnectorUserDecryptionOptionsJson?,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request body object when retrieving organization verified domain SSO info.
|
||||
*
|
||||
* @param email The email address to check against.
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifiedOrganizationDomainSsoDetailsRequest(
|
||||
@SerialName("email") val email: String,
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Response object returned when requesting organization verified domain SSO details.
|
||||
*
|
||||
* @property verifiedOrganizationDomainSsoDetails The list of verified organization domain SSO
|
||||
* details.
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifiedOrganizationDomainSsoDetailsResponse(
|
||||
@SerialName("data")
|
||||
val verifiedOrganizationDomainSsoDetails: List<VerifiedOrganizationDomainSsoDetail>,
|
||||
) {
|
||||
/**
|
||||
* Response body for an organization verified domain SSO details.
|
||||
*
|
||||
* @property organizationName The name of the organization.
|
||||
* @property organizationIdentifier The identifier of the organization.
|
||||
* @property domainName The name of the domain.
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifiedOrganizationDomainSsoDetail(
|
||||
@SerialName("organizationName")
|
||||
val organizationName: String,
|
||||
|
||||
@SerialName("organizationIdentifier")
|
||||
val organizationIdentifier: String,
|
||||
|
||||
@SerialName("domainName")
|
||||
val domainName: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models the request body for verify email token endpoint.
|
||||
*
|
||||
* @param email the email address of the user to verify.
|
||||
* @param token the provided email verification token.
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifyEmailTokenRequestJson(
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
@SerialName("emailVerificationToken")
|
||||
val token: String,
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Model the response of a verify email token request.
|
||||
*
|
||||
* A valid response will be a [VerifyEmailTokenResponseJson.Valid]
|
||||
*
|
||||
* an invalid response will be a [VerifyEmailTokenResponseJson.Invalid] with a message.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class VerifyEmailTokenResponseJson {
|
||||
|
||||
/**
|
||||
* The token is confirmed as valid from the response.
|
||||
*/
|
||||
@Serializable
|
||||
data object Valid : VerifyEmailTokenResponseJson()
|
||||
|
||||
/**
|
||||
* The response is invalid.
|
||||
*
|
||||
* @property message The error message. Expected to explain the reason why the token is invalid.
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
val message: String,
|
||||
) : VerifyEmailTokenResponseJson()
|
||||
|
||||
/**
|
||||
* The token has expired. This is special case of similar to [Invalid].
|
||||
*/
|
||||
@Serializable
|
||||
data object TokenExpired : VerifyEmailTokenResponseJson()
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
@@ -9,8 +11,14 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequest
|
||||
/**
|
||||
* Provides an API for querying accounts endpoints.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AccountsService {
|
||||
|
||||
/**
|
||||
* Converts the currently active account to a key-connector account.
|
||||
*/
|
||||
suspend fun convertToKeyConnector(): Result<Unit>
|
||||
|
||||
/**
|
||||
* Creates a new account's keys.
|
||||
*/
|
||||
@@ -49,8 +57,50 @@ interface AccountsService {
|
||||
*/
|
||||
suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Set the key connector key.
|
||||
*
|
||||
* This API requires the [accessToken] to be passed in manually because it occurs during the
|
||||
* login process.
|
||||
*/
|
||||
suspend fun setKeyConnectorKey(
|
||||
accessToken: String,
|
||||
body: KeyConnectorKeyRequestJson,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Set the password.
|
||||
*/
|
||||
suspend fun setPassword(body: SetPasswordRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Retrieves the master key from the key connector.
|
||||
*
|
||||
* This API requires the [accessToken] to be passed in manually because it occurs during the
|
||||
* login process.
|
||||
*/
|
||||
suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson>
|
||||
|
||||
/**
|
||||
* Stores the master key to the key connector.
|
||||
*/
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
masterKey: String,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Stores the master key to the key connector.
|
||||
*
|
||||
* This API requires the [accessToken] to be passed in manually because it occurs during the
|
||||
* login process.
|
||||
*/
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
masterKey: String,
|
||||
): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedKeyConnectorApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedKeyConnectorApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.CreateAccountKeysRequest
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
@@ -12,25 +17,43 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordReque
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* The default implementation of the [AccountsService].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class AccountsServiceImpl(
|
||||
private val accountsApi: AccountsApi,
|
||||
private val unauthenticatedAccountsApi: UnauthenticatedAccountsApi,
|
||||
private val authenticatedAccountsApi: AuthenticatedAccountsApi,
|
||||
private val unauthenticatedKeyConnectorApi: UnauthenticatedKeyConnectorApi,
|
||||
private val authenticatedKeyConnectorApi: AuthenticatedKeyConnectorApi,
|
||||
private val json: Json,
|
||||
) : AccountsService {
|
||||
|
||||
/**
|
||||
* Converts the currently active account to a key-connector account.
|
||||
*/
|
||||
override suspend fun convertToKeyConnector(): Result<Unit> =
|
||||
authenticatedAccountsApi
|
||||
.convertToKeyConnector()
|
||||
.toResult()
|
||||
|
||||
override suspend fun createAccountKeys(
|
||||
publicKey: String,
|
||||
encryptedPrivateKey: String,
|
||||
): Result<Unit> =
|
||||
authenticatedAccountsApi.createAccountKeys(
|
||||
body = CreateAccountKeysRequest(
|
||||
publicKey = publicKey,
|
||||
encryptedPrivateKey = encryptedPrivateKey,
|
||||
),
|
||||
)
|
||||
authenticatedAccountsApi
|
||||
.createAccountKeys(
|
||||
body = CreateAccountKeysRequest(
|
||||
publicKey = publicKey,
|
||||
encryptedPrivateKey = encryptedPrivateKey,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun deleteAccount(
|
||||
masterPasswordHash: String?,
|
||||
@@ -43,9 +66,8 @@ class AccountsServiceImpl(
|
||||
oneTimePassword = oneTimePassword,
|
||||
),
|
||||
)
|
||||
.map {
|
||||
DeleteAccountResponseJson.Success
|
||||
}
|
||||
.toResult()
|
||||
.map { DeleteAccountResponseJson.Success }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
@@ -57,20 +79,25 @@ class AccountsServiceImpl(
|
||||
}
|
||||
|
||||
override suspend fun requestOneTimePasscode(): Result<Unit> =
|
||||
authenticatedAccountsApi.requestOtp()
|
||||
authenticatedAccountsApi
|
||||
.requestOtp()
|
||||
.toResult()
|
||||
|
||||
override suspend fun verifyOneTimePasscode(passcode: String): Result<Unit> =
|
||||
authenticatedAccountsApi.verifyOtp(
|
||||
VerifyOtpRequestJson(
|
||||
oneTimePasscode = passcode,
|
||||
),
|
||||
)
|
||||
authenticatedAccountsApi
|
||||
.verifyOtp(
|
||||
VerifyOtpRequestJson(
|
||||
oneTimePasscode = passcode,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun requestPasswordHint(
|
||||
email: String,
|
||||
): Result<PasswordHintResponseJson> =
|
||||
accountsApi
|
||||
unauthenticatedAccountsApi
|
||||
.passwordHintRequest(PasswordHintRequestJson(email))
|
||||
.toResult()
|
||||
.map { PasswordHintResponseJson.Success }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
@@ -83,17 +110,70 @@ class AccountsServiceImpl(
|
||||
}
|
||||
|
||||
override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> =
|
||||
accountsApi.resendVerificationCodeEmail(body = body)
|
||||
unauthenticatedAccountsApi
|
||||
.resendVerificationCodeEmail(body = body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> {
|
||||
return if (body.currentPasswordHash == null) {
|
||||
authenticatedAccountsApi.resetTempPassword(body = body)
|
||||
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
|
||||
if (body.currentPasswordHash == null) {
|
||||
authenticatedAccountsApi
|
||||
.resetTempPassword(body = body)
|
||||
.toResult()
|
||||
} else {
|
||||
authenticatedAccountsApi.resetPassword(body = body)
|
||||
authenticatedAccountsApi
|
||||
.resetPassword(body = body)
|
||||
.toResult()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setKeyConnectorKey(
|
||||
accessToken: String,
|
||||
body: KeyConnectorKeyRequestJson,
|
||||
): Result<Unit> =
|
||||
unauthenticatedAccountsApi
|
||||
.setKeyConnectorKey(
|
||||
body = body,
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun setPassword(
|
||||
body: SetPasswordRequestJson,
|
||||
): Result<Unit> = authenticatedAccountsApi.setPassword(body)
|
||||
): Result<Unit> = authenticatedAccountsApi
|
||||
.setPassword(body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson> =
|
||||
unauthenticatedKeyConnectorApi
|
||||
.getMasterKeyFromKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
masterKey: String,
|
||||
): Result<Unit> =
|
||||
authenticatedKeyConnectorApi
|
||||
.storeMasterKeyToKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
masterKey: String,
|
||||
): Result<Unit> =
|
||||
unauthenticatedKeyConnectorApi
|
||||
.storeMasterKeyToKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -3,17 +3,22 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
|
||||
class AuthRequestsServiceImpl(
|
||||
private val authenticatedAuthRequestsApi: AuthenticatedAuthRequestsApi,
|
||||
) : AuthRequestsService {
|
||||
override suspend fun getAuthRequests(): Result<AuthRequestsResponseJson> =
|
||||
authenticatedAuthRequestsApi.getAuthRequests()
|
||||
authenticatedAuthRequestsApi
|
||||
.getAuthRequests()
|
||||
.toResult()
|
||||
|
||||
override suspend fun getAuthRequest(
|
||||
requestId: String,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
authenticatedAuthRequestsApi.getAuthRequest(requestId = requestId)
|
||||
authenticatedAuthRequestsApi
|
||||
.getAuthRequest(requestId = requestId)
|
||||
.toResult()
|
||||
|
||||
override suspend fun updateAuthRequest(
|
||||
requestId: String,
|
||||
@@ -22,13 +27,15 @@ class AuthRequestsServiceImpl(
|
||||
deviceId: String,
|
||||
isApproved: Boolean,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
authenticatedAuthRequestsApi.updateAuthRequest(
|
||||
userId = requestId,
|
||||
body = AuthRequestUpdateRequestJson(
|
||||
key = key,
|
||||
masterPasswordHash = masterPasswordHash,
|
||||
deviceId = deviceId,
|
||||
isApproved = isApproved,
|
||||
),
|
||||
)
|
||||
authenticatedAuthRequestsApi
|
||||
.updateAuthRequest(
|
||||
userId = requestId,
|
||||
body = AuthRequestUpdateRequestJson(
|
||||
key = key,
|
||||
masterPasswordHash = masterPasswordHash,
|
||||
deviceId = deviceId,
|
||||
isApproved = isApproved,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedDevic
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
|
||||
class DevicesServiceImpl(
|
||||
private val authenticatedDevicesApi: AuthenticatedDevicesApi,
|
||||
@@ -13,22 +14,26 @@ class DevicesServiceImpl(
|
||||
override suspend fun getIsKnownDevice(
|
||||
emailAddress: String,
|
||||
deviceId: String,
|
||||
): Result<Boolean> = unauthenticatedDevicesApi.getIsKnownDevice(
|
||||
emailAddress = emailAddress.base64UrlEncode(),
|
||||
deviceId = deviceId,
|
||||
)
|
||||
): Result<Boolean> = unauthenticatedDevicesApi
|
||||
.getIsKnownDevice(
|
||||
emailAddress = emailAddress.base64UrlEncode(),
|
||||
deviceId = deviceId,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun trustDevice(
|
||||
appId: String,
|
||||
encryptedUserKey: String,
|
||||
encryptedDevicePublicKey: String,
|
||||
encryptedDevicePrivateKey: String,
|
||||
): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi.updateTrustedDeviceKeys(
|
||||
appId = appId,
|
||||
request = TrustedDeviceKeysRequestJson(
|
||||
encryptedUserKey = encryptedUserKey,
|
||||
encryptedDevicePublicKey = encryptedDevicePublicKey,
|
||||
encryptedDevicePrivateKey = encryptedDevicePrivateKey,
|
||||
),
|
||||
)
|
||||
): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi
|
||||
.updateTrustedDeviceKeys(
|
||||
appId = appId,
|
||||
request = TrustedDeviceKeysRequestJson(
|
||||
encryptedUserKey = encryptedUserKey,
|
||||
encryptedDevicePublicKey = encryptedDevicePublicKey,
|
||||
encryptedDevicePrivateKey = encryptedDevicePrivateKey,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.HaveIBeenPwnedApi
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import java.security.MessageDigest
|
||||
|
||||
class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenPwnedService {
|
||||
@@ -17,6 +18,7 @@ class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenP
|
||||
|
||||
return api
|
||||
.fetchBreachedPasswords(hashPrefix = hashPrefix)
|
||||
.toResult()
|
||||
.mapCatching { responseBody ->
|
||||
responseBody.string()
|
||||
// First split the response by newline: each hashed password is on a new line.
|
||||
|
||||
@@ -5,9 +5,14 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthM
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
|
||||
/**
|
||||
* Provides an API for querying identity endpoints.
|
||||
@@ -58,4 +63,24 @@ interface IdentityService {
|
||||
* @param refreshToken The refresh token needed to obtain a new token.
|
||||
*/
|
||||
fun refreshTokenSynchronously(refreshToken: String): Result<RefreshTokenResponseJson>
|
||||
|
||||
/**
|
||||
* Send a verification email.
|
||||
*/
|
||||
suspend fun sendVerificationEmail(
|
||||
body: SendVerificationEmailRequestJson,
|
||||
): Result<SendVerificationEmailResponseJson>
|
||||
|
||||
/**
|
||||
* Register a new account to Bitwarden using email verification flow.
|
||||
*/
|
||||
suspend fun registerFinish(body: RegisterFinishRequestJson): Result<RegisterResponseJson>
|
||||
|
||||
/**
|
||||
* Makes request to verify email registration token. If the token provided is
|
||||
* still valid will return success.
|
||||
*/
|
||||
suspend fun verifyEmailRegistrationToken(
|
||||
body: VerifyEmailTokenRequestJson,
|
||||
): Result<VerifyEmailTokenResponseJson>
|
||||
}
|
||||
|
||||
@@ -1,47 +1,56 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedIdentityApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForNetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class IdentityServiceImpl(
|
||||
private val api: IdentityApi,
|
||||
private val unauthenticatedIdentityApi: UnauthenticatedIdentityApi,
|
||||
private val json: Json,
|
||||
private val deviceModelProvider: DeviceModelProvider = DeviceModelProvider(),
|
||||
) : IdentityService {
|
||||
|
||||
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
|
||||
api.preLogin(PreLoginRequestJson(email = email))
|
||||
unauthenticatedIdentityApi
|
||||
.preLogin(PreLoginRequestJson(email = email))
|
||||
.toResult()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
|
||||
api
|
||||
unauthenticatedIdentityApi
|
||||
.register(body)
|
||||
.toResult()
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>(
|
||||
code = 400,
|
||||
json = json,
|
||||
) ?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(400, 429),
|
||||
json = json,
|
||||
) ?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Error>(
|
||||
code = 429,
|
||||
json = json,
|
||||
) ?: throw throwable
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(400, 429),
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@@ -51,7 +60,7 @@ class IdentityServiceImpl(
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel?,
|
||||
): Result<GetTokenResponseJson> = api
|
||||
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.getToken(
|
||||
scope = "api offline_access",
|
||||
clientId = "mobile",
|
||||
@@ -71,6 +80,7 @@ class IdentityServiceImpl(
|
||||
captchaResponse = captchaToken,
|
||||
authRequestId = authModel.authRequestId,
|
||||
)
|
||||
.toResult()
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
|
||||
@@ -87,18 +97,85 @@ class IdentityServiceImpl(
|
||||
|
||||
override suspend fun prevalidateSso(
|
||||
organizationIdentifier: String,
|
||||
): Result<PrevalidateSsoResponseJson> = api
|
||||
): Result<PrevalidateSsoResponseJson> = unauthenticatedIdentityApi
|
||||
.prevalidateSso(
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override fun refreshTokenSynchronously(
|
||||
refreshToken: String,
|
||||
): Result<RefreshTokenResponseJson> = api
|
||||
): Result<RefreshTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.refreshTokenCall(
|
||||
clientId = "mobile",
|
||||
grantType = "refresh_token",
|
||||
refreshToken = refreshToken,
|
||||
)
|
||||
.executeForResult()
|
||||
.executeForNetworkResult()
|
||||
.toResult()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun registerFinish(
|
||||
body: RegisterFinishRequestJson,
|
||||
): Result<RegisterResponseJson> =
|
||||
unauthenticatedIdentityApi
|
||||
.registerFinish(body)
|
||||
.toResult()
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(400, 429),
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override suspend fun sendVerificationEmail(
|
||||
body: SendVerificationEmailRequestJson,
|
||||
): Result<SendVerificationEmailResponseJson> {
|
||||
return unauthenticatedIdentityApi
|
||||
.sendVerificationEmail(body = body)
|
||||
.toResult()
|
||||
.map { SendVerificationEmailResponseJson.Success(it?.content) }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<SendVerificationEmailResponseJson.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun verifyEmailRegistrationToken(
|
||||
body: VerifyEmailTokenRequestJson,
|
||||
): Result<VerifyEmailTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.verifyEmailToken(
|
||||
body = body,
|
||||
)
|
||||
.toResult()
|
||||
.map { VerifyEmailTokenResponseJson.Valid }
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<VerifyEmailTokenResponseJson.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?.checkForExpiredMessage()
|
||||
?: throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the message body contains text related to the token being expired, return
|
||||
* the TokenExpired type. Otherwise, return the original Invalid response.
|
||||
*/
|
||||
private fun VerifyEmailTokenResponseJson.Invalid.checkForExpiredMessage() =
|
||||
if (message.contains(other = "expired", ignoreCase = true)) {
|
||||
VerifyEmailTokenResponseJson.TokenExpired
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAuthR
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
|
||||
/**
|
||||
@@ -24,17 +25,19 @@ class NewAuthRequestServiceImpl(
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
when (authRequestType) {
|
||||
AuthRequestTypeJson.LOGIN_WITH_DEVICE -> {
|
||||
unauthenticatedAuthRequestsApi.createAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = authRequestType,
|
||||
),
|
||||
)
|
||||
unauthenticatedAuthRequestsApi
|
||||
.createAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = authRequestType,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
AuthRequestTypeJson.UNLOCK -> {
|
||||
@@ -43,17 +46,19 @@ class NewAuthRequestServiceImpl(
|
||||
}
|
||||
|
||||
AuthRequestTypeJson.ADMIN_APPROVAL -> {
|
||||
authenticatedAuthRequestsApi.createAdminAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = authRequestType,
|
||||
),
|
||||
)
|
||||
authenticatedAuthRequestsApi
|
||||
.createAdminAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = authRequestType,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +68,15 @@ class NewAuthRequestServiceImpl(
|
||||
isSso: Boolean,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
if (isSso) {
|
||||
authenticatedAuthRequestsApi.getAuthRequest(requestId)
|
||||
authenticatedAuthRequestsApi
|
||||
.getAuthRequest(requestId = requestId)
|
||||
.toResult()
|
||||
} else {
|
||||
unauthenticatedAuthRequestsApi.getAuthRequestUpdate(
|
||||
requestId = requestId,
|
||||
accessCode = accessCode,
|
||||
)
|
||||
unauthenticatedAuthRequestsApi
|
||||
.getAuthRequestUpdate(
|
||||
requestId = requestId,
|
||||
accessCode = accessCode,
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
|
||||
|
||||
/**
|
||||
* Provides an API for querying organization endpoints.
|
||||
@@ -38,4 +39,12 @@ interface OrganizationService {
|
||||
suspend fun getOrganizationKeys(
|
||||
organizationId: String,
|
||||
): Result<OrganizationKeysResponseJson>
|
||||
|
||||
/**
|
||||
* Request organization verified domain details for an [email] needed for SSO
|
||||
* requests.
|
||||
*/
|
||||
suspend fun getVerifiedOrganizationDomainSsoDetails(
|
||||
email: String,
|
||||
): Result<VerifiedOrganizationDomainSsoDetailsResponse>
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedOrganizationApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.OrganizationApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedOrganizationApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
|
||||
/**
|
||||
* Default implementation of [OrganizationService].
|
||||
*/
|
||||
class OrganizationServiceImpl(
|
||||
private val authenticatedOrganizationApi: AuthenticatedOrganizationApi,
|
||||
private val organizationApi: OrganizationApi,
|
||||
private val unauthenticatedOrganizationApi: UnauthenticatedOrganizationApi,
|
||||
) : OrganizationService {
|
||||
override suspend fun organizationResetPasswordEnroll(
|
||||
organizationId: String,
|
||||
@@ -29,15 +32,17 @@ class OrganizationServiceImpl(
|
||||
resetPasswordKey = resetPasswordKey,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getOrganizationDomainSsoDetails(
|
||||
email: String,
|
||||
): Result<OrganizationDomainSsoDetailsResponseJson> = organizationApi
|
||||
): Result<OrganizationDomainSsoDetailsResponseJson> = unauthenticatedOrganizationApi
|
||||
.getClaimedDomainOrganizationDetails(
|
||||
body = OrganizationDomainSsoDetailsRequestJson(
|
||||
email = email,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getOrganizationAutoEnrollStatus(
|
||||
organizationIdentifier: String,
|
||||
@@ -45,6 +50,7 @@ class OrganizationServiceImpl(
|
||||
.getOrganizationAutoEnrollResponse(
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getOrganizationKeys(
|
||||
organizationId: String,
|
||||
@@ -52,4 +58,15 @@ class OrganizationServiceImpl(
|
||||
.getOrganizationKeys(
|
||||
organizationId = organizationId,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getVerifiedOrganizationDomainSsoDetails(
|
||||
email: String,
|
||||
): Result<VerifiedOrganizationDomainSsoDetailsResponse> = unauthenticatedOrganizationApi
|
||||
.getVerifiedOrganizationDomainsByEmail(
|
||||
body = VerifiedOrganizationDomainSsoDetailsRequest(
|
||||
email = email,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
@@ -37,6 +38,11 @@ interface AuthSdkSource {
|
||||
purpose: HashPurpose,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Creates a set of encryption key information for use with a key connector.
|
||||
*/
|
||||
suspend fun makeKeyConnectorKeys(): Result<KeyConnectorResponse>
|
||||
|
||||
/**
|
||||
* Creates a set of encryption key information for registration.
|
||||
*/
|
||||
|
||||
@@ -2,16 +2,17 @@ package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.FingerprintRequest
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.bitwarden.sdk.ClientAuth
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
|
||||
import com.x8bit.bitwarden.data.platform.datasource.sdk.BaseSdkSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
|
||||
/**
|
||||
@@ -19,12 +20,13 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
* [ClientAuth].
|
||||
*/
|
||||
class AuthSdkSourceImpl(
|
||||
private val sdkClientManager: SdkClientManager,
|
||||
) : AuthSdkSource {
|
||||
sdkClientManager: SdkClientManager,
|
||||
) : BaseSdkSource(sdkClientManager = sdkClientManager),
|
||||
AuthSdkSource {
|
||||
|
||||
override suspend fun getNewAuthRequest(
|
||||
email: String,
|
||||
): Result<AuthRequestResponse> = runCatching {
|
||||
): Result<AuthRequestResponse> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.newAuthRequest(
|
||||
@@ -35,7 +37,7 @@ class AuthSdkSourceImpl(
|
||||
override suspend fun getUserFingerprint(
|
||||
email: String,
|
||||
publicKey: String,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.platform()
|
||||
.fingerprint(
|
||||
@@ -51,7 +53,7 @@ class AuthSdkSourceImpl(
|
||||
password: String,
|
||||
kdf: Kdf,
|
||||
purpose: HashPurpose,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.hashPassword(
|
||||
@@ -62,11 +64,18 @@ class AuthSdkSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun makeKeyConnectorKeys(): Result<KeyConnectorResponse> =
|
||||
runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.makeKeyConnectorKeys()
|
||||
}
|
||||
|
||||
override suspend fun makeRegisterKeys(
|
||||
email: String,
|
||||
password: String,
|
||||
kdf: Kdf,
|
||||
): Result<RegisterKeyResponse> = runCatching {
|
||||
): Result<RegisterKeyResponse> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.makeRegisterKeys(
|
||||
@@ -81,7 +90,7 @@ class AuthSdkSourceImpl(
|
||||
email: String,
|
||||
orgPublicKey: String,
|
||||
rememberDevice: Boolean,
|
||||
): Result<RegisterTdeKeyResponse> = runCatching {
|
||||
): Result<RegisterTdeKeyResponse> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.auth()
|
||||
.makeRegisterTdeKeys(
|
||||
@@ -95,7 +104,7 @@ class AuthSdkSourceImpl(
|
||||
email: String,
|
||||
password: String,
|
||||
additionalInputs: List<String>,
|
||||
): Result<PasswordStrength> = runCatching {
|
||||
): Result<PasswordStrength> = runCatchingWithLogs {
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
getClient()
|
||||
.auth()
|
||||
@@ -111,7 +120,7 @@ class AuthSdkSourceImpl(
|
||||
password: String,
|
||||
passwordStrength: PasswordStrength,
|
||||
policy: MasterPasswordPolicyOptions,
|
||||
): Result<Boolean> = runCatching {
|
||||
): Result<Boolean> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.satisfiesPolicy(
|
||||
@@ -120,8 +129,4 @@ class AuthSdkSourceImpl(
|
||||
policy = policy,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getClient(
|
||||
userId: String? = null,
|
||||
): Client = sdkClientManager.getOrCreateClient(userId = userId)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
|
||||
/**
|
||||
* Manager for keeping track of requests from the Bitwarden Authenticator app to add a TOTP
|
||||
* item.
|
||||
*/
|
||||
interface AddTotpItemFromAuthenticatorManager {
|
||||
|
||||
/**
|
||||
* Current pending [TotpData] to be added from the Authenticator app.
|
||||
*/
|
||||
var pendingAddTotpLoginItemData: TotpData?
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
|
||||
/**
|
||||
* Default in memory implementation for [AddTotpItemFromAuthenticatorManager].
|
||||
*/
|
||||
class AddTotpItemFromAuthenticatorManagerImpl : AddTotpItemFromAuthenticatorManager {
|
||||
|
||||
override var pendingAddTotpLoginItemData: TotpData? = null
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* A manager class for handling authentication fo logging in with remote device.
|
||||
* A manager class for handling authentication for logging in with remote device.
|
||||
*/
|
||||
interface AuthRequestManager {
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.util.isSso
|
||||
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
@@ -65,7 +66,7 @@ class AuthRequestManagerImpl(
|
||||
email: String,
|
||||
authRequestType: AuthRequestType,
|
||||
): Flow<CreateAuthRequestResult> = flow {
|
||||
val initialResult = createNewAuthRequest(
|
||||
val initialResult = createNewAuthRequestIfNecessary(
|
||||
email = email,
|
||||
authRequestType = authRequestType.toAuthRequestTypeJson(),
|
||||
)
|
||||
@@ -74,7 +75,6 @@ class AuthRequestManagerImpl(
|
||||
emit(CreateAuthRequestResult.Error)
|
||||
return@flow
|
||||
}
|
||||
val authRequestResponse = initialResult.authRequestResponse
|
||||
var authRequest = initialResult.authRequest
|
||||
emit(CreateAuthRequestResult.Update(authRequest))
|
||||
|
||||
@@ -84,7 +84,7 @@ class AuthRequestManagerImpl(
|
||||
newAuthRequestService
|
||||
.getAuthRequestUpdate(
|
||||
requestId = authRequest.id,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
accessCode = initialResult.accessCode,
|
||||
isSso = authRequestType.isSso,
|
||||
)
|
||||
.map { request ->
|
||||
@@ -112,7 +112,8 @@ class AuthRequestManagerImpl(
|
||||
emit(
|
||||
CreateAuthRequestResult.Success(
|
||||
authRequest = updateAuthRequest,
|
||||
authRequestResponse = authRequestResponse,
|
||||
privateKey = initialResult.privateKey,
|
||||
accessCode = initialResult.accessCode,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -354,6 +355,52 @@ class AuthRequestManagerImpl(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new auth request for the given email and returns a [NewAuthRequestData].
|
||||
* If the auth request type is [AuthRequestTypeJson.ADMIN_APPROVAL], check for a
|
||||
* pending auth request and return it if it exists we should return that request.
|
||||
*/
|
||||
private suspend fun createNewAuthRequestIfNecessary(
|
||||
email: String,
|
||||
authRequestType: AuthRequestTypeJson,
|
||||
): Result<NewAuthRequestData> {
|
||||
return if (authRequestType == AuthRequestTypeJson.ADMIN_APPROVAL) {
|
||||
authDiskSource
|
||||
.getPendingAuthRequest(requireNotNull(activeUserId))
|
||||
?.let { pendingAuthRequest ->
|
||||
authRequestsService
|
||||
.getAuthRequest(pendingAuthRequest.requestId)
|
||||
.map {
|
||||
NewAuthRequestData(
|
||||
authRequest = AuthRequest(
|
||||
id = it.id,
|
||||
publicKey = it.publicKey,
|
||||
platform = it.platform,
|
||||
ipAddress = it.ipAddress,
|
||||
key = it.key,
|
||||
masterPasswordHash = it.masterPasswordHash,
|
||||
creationDate = it.creationDate,
|
||||
responseDate = it.responseDate,
|
||||
requestApproved = it.requestApproved ?: false,
|
||||
originUrl = it.originUrl,
|
||||
fingerprint = pendingAuthRequest.requestFingerprint,
|
||||
),
|
||||
privateKey = pendingAuthRequest.requestPrivateKey,
|
||||
accessCode = pendingAuthRequest.requestAccessCode,
|
||||
)
|
||||
.asSuccess()
|
||||
}
|
||||
.getOrNull()
|
||||
}
|
||||
?: createNewAuthRequest(email = email, authRequestType = authRequestType)
|
||||
} else {
|
||||
createNewAuthRequest(
|
||||
email = email,
|
||||
authRequestType = authRequestType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to create a new auth request for the given email and returns a [NewAuthRequestData]
|
||||
* with the [AuthRequest] and [AuthRequestResponse].
|
||||
@@ -381,6 +428,8 @@ class AuthRequestManagerImpl(
|
||||
pendingAuthRequest = PendingAuthRequestJson(
|
||||
requestId = it.id,
|
||||
requestPrivateKey = authRequestResponse.privateKey,
|
||||
requestAccessCode = authRequestResponse.accessCode,
|
||||
requestFingerprint = authRequestResponse.fingerprint,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -400,7 +449,13 @@ class AuthRequestManagerImpl(
|
||||
fingerprint = authRequestResponse.fingerprint,
|
||||
)
|
||||
}
|
||||
.map { NewAuthRequestData(it, authRequestResponse) }
|
||||
.map {
|
||||
NewAuthRequestData(
|
||||
authRequest = it,
|
||||
privateKey = authRequestResponse.privateKey,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getFingerprintPhrase(
|
||||
@@ -420,5 +475,6 @@ class AuthRequestManagerImpl(
|
||||
*/
|
||||
private data class NewAuthRequestData(
|
||||
val authRequest: AuthRequest,
|
||||
val authRequestResponse: AuthRequestResponse,
|
||||
val privateKey: String,
|
||||
val accessCode: String,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
|
||||
/**
|
||||
* Manager used to interface with a key connector.
|
||||
*/
|
||||
interface KeyConnectorManager {
|
||||
/**
|
||||
* Retrieves the master key from the key connector.
|
||||
*/
|
||||
suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson>
|
||||
|
||||
/**
|
||||
* Migrates an existing user to use the key connector.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun migrateExistingUserToKeyConnector(
|
||||
userId: String,
|
||||
url: String,
|
||||
userKeyEncrypted: String,
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
kdf: Kdf,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Migrates a new user to use the key connector.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun migrateNewUserToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
kdfType: KdfTypeJson,
|
||||
kdfIterations: Int?,
|
||||
kdfMemory: Int?,
|
||||
kdfParallelism: Int?,
|
||||
organizationIdentifier: String,
|
||||
): Result<KeyConnectorResponse>
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
|
||||
/**
|
||||
* The default implementation of the [KeyConnectorManager].
|
||||
*/
|
||||
class KeyConnectorManagerImpl(
|
||||
private val accountsService: AccountsService,
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
) : KeyConnectorManager {
|
||||
override suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson> =
|
||||
accountsService.getMasterKeyFromKeyConnector(
|
||||
url = url,
|
||||
accessToken = accessToken,
|
||||
)
|
||||
|
||||
override suspend fun migrateExistingUserToKeyConnector(
|
||||
userId: String,
|
||||
url: String,
|
||||
userKeyEncrypted: String,
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
kdf: Kdf,
|
||||
): Result<Unit> =
|
||||
vaultSdkSource
|
||||
.deriveKeyConnector(
|
||||
userId = userId,
|
||||
userKeyEncrypted = userKeyEncrypted,
|
||||
email = email,
|
||||
password = masterPassword,
|
||||
kdf = kdf,
|
||||
)
|
||||
.flatMap { masterKey ->
|
||||
accountsService.storeMasterKeyToKeyConnector(url = url, masterKey = masterKey)
|
||||
}
|
||||
.flatMap { accountsService.convertToKeyConnector() }
|
||||
|
||||
override suspend fun migrateNewUserToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
kdfType: KdfTypeJson,
|
||||
kdfIterations: Int?,
|
||||
kdfMemory: Int?,
|
||||
kdfParallelism: Int?,
|
||||
organizationIdentifier: String,
|
||||
): Result<KeyConnectorResponse> =
|
||||
authSdkSource
|
||||
.makeKeyConnectorKeys()
|
||||
.flatMap { keyConnectorResponse ->
|
||||
accountsService
|
||||
.storeMasterKeyToKeyConnector(
|
||||
url = url,
|
||||
accessToken = accessToken,
|
||||
masterKey = keyConnectorResponse.masterKey,
|
||||
)
|
||||
.flatMap {
|
||||
accountsService.setKeyConnectorKey(
|
||||
accessToken = accessToken,
|
||||
body = KeyConnectorKeyRequestJson(
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
keys = KeyConnectorKeyRequestJson.Keys(
|
||||
publicKey = keyConnectorResponse.keys.public,
|
||||
encryptedPrivateKey = keyConnectorResponse.keys.private,
|
||||
),
|
||||
kdfType = kdfType,
|
||||
kdfIterations = kdfIterations,
|
||||
kdfMemory = kdfMemory,
|
||||
kdfParallelism = kdfParallelism,
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
),
|
||||
)
|
||||
}
|
||||
.map { keyConnectorResponse }
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,8 @@ class TrustedDeviceManagerImpl(
|
||||
private val devicesService: DevicesService,
|
||||
) : TrustedDeviceManager {
|
||||
override suspend fun trustThisDeviceIfNecessary(userId: String): Result<Boolean> =
|
||||
if (!authDiskSource.getShouldTrustDevice(userId = userId)) {
|
||||
if (authDiskSource.getShouldTrustDevice(userId = userId) != true) {
|
||||
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
|
||||
false.asSuccess()
|
||||
} else {
|
||||
vaultSdkSource
|
||||
@@ -51,7 +52,8 @@ class TrustedDeviceManagerImpl(
|
||||
userId = userId,
|
||||
previousUserState = requireNotNull(authDiskSource.userState),
|
||||
)
|
||||
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
|
||||
}
|
||||
.also { authDiskSource.storeShouldTrustDevice(userId = userId, shouldTrustDevice = null) }
|
||||
.map { Unit }
|
||||
.map { }
|
||||
}
|
||||
|
||||
@@ -14,15 +14,15 @@ interface UserLogoutManager {
|
||||
val logoutEventFlow: SharedFlow<LogoutEvent>
|
||||
|
||||
/**
|
||||
* Completely logs out the given [userId], removing all data.
|
||||
* If [isExpired] is true, a toast will be displayed
|
||||
* letting the user know the session has expired.
|
||||
* Completely logs out the given [userId], removing all data. If [isExpired] is true, a toast
|
||||
* will be displayed letting the user know the session has expired.
|
||||
*/
|
||||
fun logout(userId: String, isExpired: Boolean = false)
|
||||
|
||||
/**
|
||||
* Partially logs out the given [userId]. All data for the given [userId] will be removed with
|
||||
* the exception of basic account data.
|
||||
* the exception of basic account data. If [isExpired] is true, a toast will be displayed
|
||||
* letting the user know the session has expired.
|
||||
*/
|
||||
fun softLogout(userId: String)
|
||||
fun softLogout(userId: String, isExpired: Boolean = false)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,10 @@ class UserLogoutManagerImpl(
|
||||
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
|
||||
}
|
||||
|
||||
override fun softLogout(userId: String) {
|
||||
override fun softLogout(userId: String, isExpired: Boolean) {
|
||||
if (isExpired) {
|
||||
showToast(message = R.string.login_expired)
|
||||
}
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = null,
|
||||
@@ -74,7 +77,11 @@ class UserLogoutManagerImpl(
|
||||
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
||||
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
||||
|
||||
switchUserIfAvailable(currentUserId = userId, removeCurrentUserFromAccounts = false)
|
||||
switchUserIfAvailable(
|
||||
currentUserId = userId,
|
||||
removeCurrentUserFromAccounts = false,
|
||||
isExpired = isExpired,
|
||||
)
|
||||
|
||||
clearData(userId = userId)
|
||||
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
|
||||
|
||||
@@ -2,14 +2,19 @@ package com.x8bit.bitwarden.data.auth.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
@@ -71,6 +76,19 @@ object AuthManagerModule {
|
||||
authDiskSource = authDiskSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideKeyConnectorManager(
|
||||
accountsService: AccountsService,
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
): KeyConnectorManager =
|
||||
KeyConnectorManagerImpl(
|
||||
accountsService = accountsService,
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTrustedDeviceManager(
|
||||
@@ -108,4 +126,9 @@ object AuthManagerModule {
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAddTotpItemFromAuthenticatorManager(): AddTotpItemFromAuthenticatorManager =
|
||||
AddTotpItemFromAuthenticatorManagerImpl()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager.model
|
||||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
|
||||
/**
|
||||
* Models result of creating a new login approval request.
|
||||
*/
|
||||
@@ -18,7 +16,8 @@ sealed class CreateAuthRequestResult {
|
||||
*/
|
||||
data class Success(
|
||||
val authRequest: AuthRequest,
|
||||
val authRequestResponse: AuthRequestResponse,
|
||||
val privateKey: String,
|
||||
val accessCode: String,
|
||||
) : CreateAuthRequestResult()
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ val AuthRequestType.isSso: Boolean
|
||||
AuthRequestType.OTHER_DEVICE -> false
|
||||
AuthRequestType.SSO_OTHER_DEVICE,
|
||||
AuthRequestType.SSO_ADMIN_APPROVAL,
|
||||
-> true
|
||||
-> true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,7 +21,7 @@ fun AuthRequestType.toAuthRequestTypeJson(): AuthRequestTypeJson =
|
||||
when (this) {
|
||||
AuthRequestType.OTHER_DEVICE,
|
||||
AuthRequestType.SSO_OTHER_DEVICE,
|
||||
-> AuthRequestTypeJson.LOGIN_WITH_DEVICE
|
||||
-> AuthRequestTypeJson.LOGIN_WITH_DEVICE
|
||||
|
||||
AuthRequestType.SSO_ADMIN_APPROVAL -> AuthRequestTypeJson.ADMIN_APPROVAL
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
|
||||
@@ -16,14 +19,17 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
@@ -101,6 +107,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
var rememberedOrgIdentifier: String?
|
||||
|
||||
/**
|
||||
* The currently persisted state indicating whether the user has completed login via TDE.
|
||||
*/
|
||||
val tdeLoginComplete: Boolean?
|
||||
|
||||
/**
|
||||
* The currently persisted state indicating whether the user has trusted this device.
|
||||
*/
|
||||
@@ -203,6 +214,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
password: String?,
|
||||
twoFactorData: TwoFactorDataModel,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
@@ -253,6 +265,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String? = null,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
@@ -265,6 +278,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
): PasswordHintResult
|
||||
|
||||
/**
|
||||
* Removes the users password from the account. This used used when migrating from master
|
||||
* password login to key connector login.
|
||||
*/
|
||||
suspend fun removePassword(masterPassword: String): RemovePasswordResult
|
||||
|
||||
/**
|
||||
* Resets the users password from the [currentPassword] (or null for account recovery resets),
|
||||
* to the [newPassword] and optional [passwordHint].
|
||||
@@ -312,6 +331,13 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
): OrganizationDomainSsoDetailsResult
|
||||
|
||||
/**
|
||||
* Get the verified organization domain SSO details for the given [email].
|
||||
*/
|
||||
suspend fun getVerifiedOrganizationDomainSsoDetails(
|
||||
email: String,
|
||||
): VerifiedOrganizationDomainSsoDetailsResult
|
||||
|
||||
/**
|
||||
* Prevalidates the organization identifier used in an SSO request.
|
||||
*/
|
||||
@@ -354,4 +380,41 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
* policies for the current user.
|
||||
*/
|
||||
suspend fun validatePasswordAgainstPolicies(password: String): Boolean
|
||||
|
||||
/**
|
||||
* Send a verification email.
|
||||
*/
|
||||
suspend fun sendVerificationEmail(
|
||||
email: String,
|
||||
name: String,
|
||||
receiveMarketingEmails: Boolean,
|
||||
): SendVerificationEmailResult
|
||||
|
||||
/**
|
||||
* Validates the given [token] for the given [email]. Part of th new account registration flow.
|
||||
*/
|
||||
suspend fun validateEmailToken(
|
||||
email: String,
|
||||
token: String,
|
||||
): EmailTokenResult
|
||||
|
||||
/**
|
||||
* Update the value of the onboarding status for the user.
|
||||
*/
|
||||
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
|
||||
|
||||
/**
|
||||
* Checks if a new device notice should be displayed.
|
||||
*/
|
||||
fun checkUserNeedsNewDeviceTwoFactorNotice(): Boolean
|
||||
|
||||
/**
|
||||
* Gets the new device notice state of active user.
|
||||
*/
|
||||
fun getNewDeviceNoticeState(): NewDeviceNoticeState?
|
||||
|
||||
/**
|
||||
* Stores the new device notice state for active user.
|
||||
*/
|
||||
fun setNewDeviceNoticeState(newState: NewDeviceNoticeState?)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.bitwarden.core.AuthRequestMethod
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
|
||||
@@ -16,14 +18,19 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJs
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
@@ -33,11 +40,13 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
|
||||
@@ -47,17 +56,21 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
@@ -65,34 +78,46 @@ import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonWithPassword
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.isSslHandShakeError
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
|
||||
@@ -119,6 +144,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.time.ZonedDateTime
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -135,14 +161,18 @@ class AuthRepositoryImpl(
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val configDiskSource: ConfigDiskSource,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRequestManager: AuthRequestManager,
|
||||
private val keyConnectorManager: KeyConnectorManager,
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val policyManager: PolicyManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AuthRepository,
|
||||
@@ -235,6 +265,9 @@ class AuthRepositoryImpl(
|
||||
authDiskSource.userStateFlow,
|
||||
authDiskSource.userAccountTokensFlow,
|
||||
authDiskSource.userOrganizationsListFlow,
|
||||
authDiskSource.userKeyConnectorStateFlow,
|
||||
authDiskSource.onboardingStatusChangesFlow,
|
||||
firstTimeActionManager.firstTimeStateFlow,
|
||||
vaultRepository.vaultUnlockDataStateFlow,
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
// Ignore the data in the merge, but trigger an update when they emit.
|
||||
@@ -246,16 +279,22 @@ class AuthRepositoryImpl(
|
||||
val userStateJson = array[0] as UserStateJson?
|
||||
val userAccountTokens = array[1] as List<UserAccountTokens>
|
||||
val userOrganizationsList = array[2] as List<UserOrganizations>
|
||||
val vaultState = array[3] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[4] as Boolean
|
||||
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
|
||||
val onboardingStatus = array[4] as OnboardingStatus?
|
||||
val firstTimeState = array[5] as FirstTimeState
|
||||
val vaultState = array[6] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[7] as Boolean
|
||||
userStateJson?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userAccountTokens = userAccountTokens,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
onboardingStatus = onboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeState,
|
||||
)
|
||||
}
|
||||
.filterNot { mutableHasPendingAccountDeletionStateFlow.value }
|
||||
@@ -269,10 +308,13 @@ class AuthRepositoryImpl(
|
||||
vaultState = vaultRepository.vaultUnlockDataStateFlow.value,
|
||||
userAccountTokens = authDiskSource.userAccountTokens,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
|
||||
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
|
||||
onboardingStatus = authDiskSource.currentOnboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -297,6 +339,9 @@ class AuthRepositoryImpl(
|
||||
|
||||
override var rememberedOrgIdentifier: String? by authDiskSource::rememberedOrgIdentifier
|
||||
|
||||
override val tdeLoginComplete: Boolean?
|
||||
get() = activeUserId?.let { authDiskSource.getIsTdeLoginComplete(userId = it) }
|
||||
|
||||
override var shouldTrustDevice: Boolean
|
||||
get() = activeUserId?.let { authDiskSource.getShouldTrustDevice(userId = it) } ?: false
|
||||
set(value) {
|
||||
@@ -326,6 +371,24 @@ class AuthRepositoryImpl(
|
||||
featureFlagManager.getFeatureFlag(FlagKey.OnboardingCarousel)
|
||||
|
||||
init {
|
||||
combine(
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
authDiskSource.userStateFlow,
|
||||
environmentRepository.environmentStateFlow,
|
||||
) { hasPendingAddition, userState, environment ->
|
||||
logsManager.setUserData(
|
||||
userId = userState?.activeUserId.takeUnless { hasPendingAddition },
|
||||
environmentType = userState
|
||||
?.activeAccount
|
||||
?.settings
|
||||
?.environmentUrlData
|
||||
?.toEnvironmentUrls()
|
||||
?.type
|
||||
.takeUnless { hasPendingAddition }
|
||||
?: environment.type,
|
||||
)
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
pushManager
|
||||
.syncOrgKeysFlow
|
||||
.onEach {
|
||||
@@ -467,7 +530,8 @@ class AuthRepositoryImpl(
|
||||
userId = userId,
|
||||
email = account.profile.email,
|
||||
orgPublicKey = organizationKeys.publicKey,
|
||||
rememberDevice = authDiskSource.getShouldTrustDevice(userId = userId),
|
||||
rememberDevice = authDiskSource
|
||||
.getShouldTrustDevice(userId = userId) == true,
|
||||
)
|
||||
}
|
||||
.flatMap { keys ->
|
||||
@@ -534,8 +598,7 @@ class AuthRepositoryImpl(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = asymmetricalKey)
|
||||
settingsRepository.storeUserHasLoggedInValue(userId)
|
||||
vaultRepository.syncIfNecessary()
|
||||
return LoginResult.Success
|
||||
}
|
||||
@@ -566,7 +629,12 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { LoginResult.Error(errorMessage = null) },
|
||||
onFailure = { throwable ->
|
||||
when {
|
||||
throwable.isSslHandShakeError() -> LoginResult.CertificateError
|
||||
else -> LoginResult.Error(errorMessage = null)
|
||||
}
|
||||
},
|
||||
onSuccess = { it },
|
||||
)
|
||||
|
||||
@@ -600,6 +668,7 @@ class AuthRepositoryImpl(
|
||||
password: String?,
|
||||
twoFactorData: TwoFactorDataModel,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = identityTokenAuthModel
|
||||
?.let {
|
||||
loginCommon(
|
||||
@@ -609,6 +678,7 @@ class AuthRepositoryImpl(
|
||||
twoFactorData = twoFactorData,
|
||||
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
|
||||
deviceData = twoFactorDeviceData,
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
?: LoginResult.Error(errorMessage = null)
|
||||
@@ -723,6 +793,7 @@ class AuthRepositoryImpl(
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String?,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
@@ -751,21 +822,41 @@ class AuthRepositoryImpl(
|
||||
kdf = kdf,
|
||||
)
|
||||
.flatMap { registerKeyResponse ->
|
||||
identityService.register(
|
||||
body = RegisterRequestJson(
|
||||
email = email,
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
captchaResponse = captchaToken,
|
||||
key = registerKeyResponse.encryptedUserKey,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
encryptedPrivateKey = registerKeyResponse.keys.private,
|
||||
if (emailVerificationToken == null) {
|
||||
// TODO PM-6675: Remove register call and service implementation
|
||||
identityService.register(
|
||||
body = RegisterRequestJson(
|
||||
email = email,
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
captchaResponse = captchaToken,
|
||||
key = registerKeyResponse.encryptedUserKey,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
encryptedPrivateKey = registerKeyResponse.keys.private,
|
||||
),
|
||||
kdfType = kdf.toKdfTypeJson(),
|
||||
kdfIterations = kdf.iterations,
|
||||
),
|
||||
kdfType = kdf.toKdfTypeJson(),
|
||||
kdfIterations = kdf.iterations,
|
||||
),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
identityService.registerFinish(
|
||||
body = RegisterFinishRequestJson(
|
||||
email = email,
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
emailVerificationToken = emailVerificationToken,
|
||||
captchaResponse = captchaToken,
|
||||
userSymmetricKey = registerKeyResponse.encryptedUserKey,
|
||||
userAsymmetricKeys = RegisterFinishRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
encryptedPrivateKey = registerKeyResponse.keys.private,
|
||||
),
|
||||
kdfType = kdf.toKdfTypeJson(),
|
||||
kdfIterations = kdf.iterations,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onSuccess = {
|
||||
@@ -791,10 +882,6 @@ class AuthRepositoryImpl(
|
||||
?: it.message,
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Error -> {
|
||||
RegisterResult.Error(it.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { RegisterResult.Error(errorMessage = null) },
|
||||
@@ -813,6 +900,46 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removePassword(masterPassword: String): RemovePasswordResult {
|
||||
val activeAccount = authDiskSource
|
||||
.userState
|
||||
?.activeAccount
|
||||
?: return RemovePasswordResult.Error
|
||||
val profile = activeAccount.profile
|
||||
val userId = profile.userId
|
||||
val userKey = authDiskSource
|
||||
.getUserKey(userId = userId)
|
||||
?: return RemovePasswordResult.Error
|
||||
val keyConnectorUrl = organizations
|
||||
.find {
|
||||
it.shouldUseKeyConnector &&
|
||||
it.type != OrganizationType.OWNER &&
|
||||
it.type != OrganizationType.ADMIN
|
||||
}
|
||||
?.keyConnectorUrl
|
||||
?: return RemovePasswordResult.Error
|
||||
return keyConnectorManager
|
||||
.migrateExistingUserToKeyConnector(
|
||||
userId = userId,
|
||||
url = keyConnectorUrl,
|
||||
userKeyEncrypted = userKey,
|
||||
email = profile.email,
|
||||
masterPassword = masterPassword,
|
||||
kdf = profile.toSdkParams(),
|
||||
)
|
||||
.onSuccess {
|
||||
authDiskSource.userState = authDiskSource
|
||||
.userState
|
||||
?.toRemovedPasswordUserStateJson(userId = userId)
|
||||
vaultRepository.sync()
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { RemovePasswordResult.Error },
|
||||
onSuccess = { RemovePasswordResult.Success },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun resetPassword(
|
||||
currentPassword: String?,
|
||||
newPassword: String,
|
||||
@@ -913,7 +1040,7 @@ class AuthRepositoryImpl(
|
||||
ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
|
||||
ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
|
||||
null,
|
||||
-> {
|
||||
-> {
|
||||
authSdkSource
|
||||
.makeRegisterKeys(
|
||||
email = activeAccount.profile.email,
|
||||
@@ -963,7 +1090,7 @@ class AuthRepositoryImpl(
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
VaultUnlockResult.GenericError,
|
||||
-> {
|
||||
-> {
|
||||
IllegalStateException("Failed to unlock vault").asFailure()
|
||||
}
|
||||
}
|
||||
@@ -1006,11 +1133,27 @@ class AuthRepositoryImpl(
|
||||
OrganizationDomainSsoDetailsResult.Success(
|
||||
isSsoAvailable = it.isSsoAvailable,
|
||||
organizationIdentifier = it.organizationIdentifier,
|
||||
verifiedDate = it.verifiedDate,
|
||||
)
|
||||
},
|
||||
onFailure = { OrganizationDomainSsoDetailsResult.Failure },
|
||||
)
|
||||
|
||||
override suspend fun getVerifiedOrganizationDomainSsoDetails(
|
||||
email: String,
|
||||
): VerifiedOrganizationDomainSsoDetailsResult = organizationService
|
||||
.getVerifiedOrganizationDomainSsoDetails(
|
||||
email = email,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
VerifiedOrganizationDomainSsoDetailsResult.Success(
|
||||
verifiedOrganizationDomainSsoDetails = it.verifiedOrganizationDomainSsoDetails,
|
||||
)
|
||||
},
|
||||
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure },
|
||||
)
|
||||
|
||||
override suspend fun prevalidateSso(
|
||||
organizationIdentifier: String,
|
||||
): PrevalidateSsoResult = identityService
|
||||
@@ -1115,41 +1258,17 @@ class AuthRepositoryImpl(
|
||||
?.activeAccount
|
||||
?.profile
|
||||
?: return ValidatePinResult.Error
|
||||
val privateKey = authDiskSource
|
||||
.getPrivateKey(userId = activeAccount.userId)
|
||||
?: return ValidatePinResult.Error
|
||||
val pinProtectedUserKey = authDiskSource
|
||||
.getPinProtectedUserKey(userId = activeAccount.userId)
|
||||
?: return ValidatePinResult.Error
|
||||
|
||||
// HACK: As the SDK doesn't provide a way to directly validate the pin yet, we instead
|
||||
// try to initialize the user crypto, and if it succeeds then the PIN is correct, otherwise
|
||||
// the PIN is incorrect.
|
||||
return vaultSdkSource
|
||||
.initializeCrypto(
|
||||
.validatePin(
|
||||
userId = activeAccount.userId,
|
||||
request = InitUserCryptoRequest(
|
||||
kdfParams = activeAccount.toSdkParams(),
|
||||
email = activeAccount.email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.Pin(
|
||||
pin = pin,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
),
|
||||
),
|
||||
pin = pin,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
InitializeCryptoResult.Success -> {
|
||||
ValidatePinResult.Success(isValid = true)
|
||||
}
|
||||
|
||||
is InitializeCryptoResult.AuthenticationError -> {
|
||||
ValidatePinResult.Success(isValid = false)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess = { ValidatePinResult.Success(isValid = it) },
|
||||
onFailure = { ValidatePinResult.Error },
|
||||
)
|
||||
}
|
||||
@@ -1159,6 +1278,148 @@ class AuthRepositoryImpl(
|
||||
): Boolean = passwordPolicies
|
||||
.all { validatePasswordAgainstPolicy(password, it) }
|
||||
|
||||
override suspend fun sendVerificationEmail(
|
||||
email: String,
|
||||
name: String,
|
||||
receiveMarketingEmails: Boolean,
|
||||
): SendVerificationEmailResult =
|
||||
identityService
|
||||
.sendVerificationEmail(
|
||||
SendVerificationEmailRequestJson(
|
||||
email = email,
|
||||
name = name.takeUnless { it.isBlank() },
|
||||
receiveMarketingEmails = receiveMarketingEmails,
|
||||
),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is SendVerificationEmailResponseJson.Invalid -> {
|
||||
SendVerificationEmailResult.Error(it.message)
|
||||
}
|
||||
|
||||
is SendVerificationEmailResponseJson.Success -> {
|
||||
SendVerificationEmailResult.Success(it.emailVerificationToken)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
SendVerificationEmailResult.Error(null)
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun validateEmailToken(email: String, token: String): EmailTokenResult {
|
||||
return identityService
|
||||
.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
email = email,
|
||||
token = token,
|
||||
),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (val json = it) {
|
||||
VerifyEmailTokenResponseJson.Valid -> EmailTokenResult.Success
|
||||
is VerifyEmailTokenResponseJson.Invalid -> {
|
||||
EmailTokenResult.Error(json.message)
|
||||
}
|
||||
|
||||
VerifyEmailTokenResponseJson.TokenExpired -> EmailTokenResult.Expired
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
EmailTokenResult.Error(message = null)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun setOnboardingStatus(userId: String, status: OnboardingStatus?) {
|
||||
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
|
||||
}
|
||||
|
||||
override fun getNewDeviceNoticeState(): NewDeviceNoticeState? {
|
||||
return activeUserId?.let { userId ->
|
||||
authDiskSource.getNewDeviceNoticeState(userId = userId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setNewDeviceNoticeState(newState: NewDeviceNoticeState?) {
|
||||
activeUserId?.let { userId ->
|
||||
authDiskSource.storeNewDeviceNoticeState(userId = userId, newState = newState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkUserNeedsNewDeviceTwoFactorNotice(): Boolean {
|
||||
return activeUserId?.let { userId ->
|
||||
val temporaryFlag = featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
|
||||
val permanentFlag = featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
|
||||
|
||||
// check if feature flags are disabled
|
||||
if (!temporaryFlag && !permanentFlag) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!newDeviceNoticePreConditionsValid()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val newDeviceNoticeState = authDiskSource.getNewDeviceNoticeState(userId = userId)
|
||||
return when (newDeviceNoticeState.displayStatus) {
|
||||
// if the user has already attested email access but permanent flag is enabled,
|
||||
// the notice needs to appear again
|
||||
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL -> permanentFlag
|
||||
// if the user has already seen but 7 days have already passed,
|
||||
// the notice needs to appear again
|
||||
NewDeviceNoticeDisplayStatus.HAS_SEEN ->
|
||||
newDeviceNoticeState.shouldDisplayNoticeIfSeen
|
||||
NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN -> true
|
||||
// the user never needs to see the notice again
|
||||
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT -> false
|
||||
}
|
||||
}
|
||||
?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the preconditions are met for a user to see a new device notice:
|
||||
* - Must be a Bitwarden cloud user.
|
||||
* - The account must be at least one week old.
|
||||
* - Cannot have an active policy requiring SSO to be enabled.
|
||||
* - Cannot have two-factor authentication enabled.
|
||||
*/
|
||||
private fun newDeviceNoticePreConditionsValid(): Boolean {
|
||||
if (environmentRepository.environment.type == Environment.Type.SELF_HOSTED) {
|
||||
return false
|
||||
}
|
||||
|
||||
val userProfile = authDiskSource.userState?.activeAccount?.profile
|
||||
val isProfileAtLeastWeekOld = userProfile
|
||||
?.let {
|
||||
it.creationDate
|
||||
?.plusWeeks(1)
|
||||
?.isBefore(
|
||||
ZonedDateTime.now(),
|
||||
)
|
||||
}
|
||||
?: false
|
||||
if (!isProfileAtLeastWeekOld) {
|
||||
return false
|
||||
}
|
||||
|
||||
val hasTwoFactorEnabled = userProfile
|
||||
?.isTwoFactorEnabled
|
||||
?: false
|
||||
if (hasTwoFactorEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val hasSSOPolicy =
|
||||
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
|
||||
.any { p -> p.isEnabled }
|
||||
|
||||
return !hasSSOPolicy
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private suspend fun validatePasswordAgainstPolicy(
|
||||
password: String,
|
||||
@@ -1323,7 +1584,15 @@ class AuthRepositoryImpl(
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
.fold(
|
||||
onFailure = { LoginResult.Error(errorMessage = null) },
|
||||
onFailure = { throwable ->
|
||||
when {
|
||||
throwable.isSslHandShakeError() -> LoginResult.CertificateError
|
||||
configDiskSource.serverConfig?.isOfficialBitwardenServer == false -> {
|
||||
LoginResult.UnofficialServerError
|
||||
}
|
||||
else -> LoginResult.Error(errorMessage = null)
|
||||
}
|
||||
},
|
||||
onSuccess = { loginResponse ->
|
||||
when (loginResponse) {
|
||||
is GetTokenResponseJson.CaptchaRequired -> LoginResult.CaptchaRequired(
|
||||
@@ -1346,7 +1615,7 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
|
||||
is GetTokenResponseJson.Invalid -> LoginResult.Error(
|
||||
errorMessage = loginResponse.errorModel.errorMessage,
|
||||
errorMessage = loginResponse.errorMessage,
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -1367,30 +1636,42 @@ class AuthRepositoryImpl(
|
||||
previousUserState = authDiskSource.userState,
|
||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||
)
|
||||
val userId = userStateJson.activeUserId
|
||||
val profile = userStateJson.activeAccount.profile
|
||||
val userId = profile.userId
|
||||
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { vaultUnlockError ->
|
||||
return@userStateTransaction vaultUnlockError.toLoginErrorResult()
|
||||
},
|
||||
) {
|
||||
val keyConnectorUrl = loginResponse
|
||||
.keyConnectorUrl
|
||||
?: loginResponse
|
||||
.userDecryptionOptions
|
||||
?.keyConnectorUserDecryptionOptions
|
||||
?.keyConnectorUrl
|
||||
val isDeviceUnlockAvailable = deviceData != null ||
|
||||
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions != null
|
||||
// if possible attempt to unlock the vault with trusted device data
|
||||
if (isDeviceUnlockAvailable) {
|
||||
unlockVaultWithTdeOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
userStateJson = userStateJson,
|
||||
profile = profile,
|
||||
deviceData = deviceData,
|
||||
)
|
||||
} else if (keyConnectorUrl != null && orgIdentifier != null) {
|
||||
unlockVaultWithKeyConnectorOnLoginSuccess(
|
||||
profile = profile,
|
||||
keyConnectorUrl = keyConnectorUrl,
|
||||
orgIdentifier = orgIdentifier,
|
||||
loginResponse = loginResponse,
|
||||
)
|
||||
} else {
|
||||
password?.let {
|
||||
unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
userStateJson = userStateJson,
|
||||
password = it,
|
||||
)
|
||||
}
|
||||
unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
profile = profile,
|
||||
password = password,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1400,7 +1681,7 @@ class AuthRepositoryImpl(
|
||||
.hashPassword(
|
||||
email = email,
|
||||
password = it,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
kdf = profile.toSdkParams(),
|
||||
purpose = HashPurpose.LOCAL_AUTHORIZATION,
|
||||
)
|
||||
.onSuccess { passwordHash ->
|
||||
@@ -1422,14 +1703,27 @@ class AuthRepositoryImpl(
|
||||
),
|
||||
)
|
||||
settingsRepository.hasUserLoggedInOrCreatedAccount = true
|
||||
|
||||
val shouldSetOnboardingStatus = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow) &&
|
||||
!settingsRepository.getUserHasLoggedInValue(userId = userId)
|
||||
if (shouldSetOnboardingStatus) {
|
||||
setOnboardingStatus(
|
||||
userId = userId,
|
||||
status = OnboardingStatus.NOT_STARTED,
|
||||
)
|
||||
}
|
||||
|
||||
authDiskSource.userState = userStateJson
|
||||
loginResponse.key?.let {
|
||||
// Only set the value if it's present, since we may have set it already
|
||||
// when we completed the pending admin auth request.
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = it)
|
||||
}
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = loginResponse.privateKey)
|
||||
|
||||
loginResponse.privateKey?.let {
|
||||
// Only set the value if it's present, since we may have set it already
|
||||
// when we completed the key connector conversion.
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
|
||||
}
|
||||
// If the user just authenticated with a two-factor code and selected the option to
|
||||
// remember it, then the API response will return a token that will be used in place
|
||||
// of the two-factor code on the next login attempt.
|
||||
@@ -1448,6 +1742,7 @@ class AuthRepositoryImpl(
|
||||
resendEmailRequestJson = null
|
||||
twoFactorDeviceData = null
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
settingsRepository.storeUserHasLoggedInValue(userId)
|
||||
vaultRepository.syncIfNecessary()
|
||||
hasPendingAccountAddition = false
|
||||
LoginResult.Success
|
||||
@@ -1478,12 +1773,89 @@ class AuthRepositoryImpl(
|
||||
return LoginResult.TwoFactorRequired
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the current user's vault with key connector data.
|
||||
*/
|
||||
private suspend fun unlockVaultWithKeyConnectorOnLoginSuccess(
|
||||
profile: AccountJson.Profile,
|
||||
keyConnectorUrl: String,
|
||||
orgIdentifier: String,
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
): VaultUnlockResult? =
|
||||
if (loginResponse.userDecryptionOptions?.hasMasterPassword != false) {
|
||||
// This user has a master password, so we skip the key-connector logic as it is not
|
||||
// setup yet. The user can still unlock the vault with their master password.
|
||||
null
|
||||
} else if (loginResponse.key != null && loginResponse.privateKey != null) {
|
||||
// This is a returning user who should already have the key connector setup
|
||||
keyConnectorManager
|
||||
.getMasterKeyFromKeyConnector(
|
||||
url = keyConnectorUrl,
|
||||
accessToken = loginResponse.accessToken,
|
||||
)
|
||||
.map {
|
||||
unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = loginResponse.privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||
masterKey = it.masterKey,
|
||||
userKey = loginResponse.key,
|
||||
),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
// If the request failed, we want to abort the login process
|
||||
onFailure = { VaultUnlockResult.GenericError },
|
||||
onSuccess = { it },
|
||||
)
|
||||
} else {
|
||||
// This is a new user who needs to setup the key connector
|
||||
keyConnectorManager
|
||||
.migrateNewUserToKeyConnector(
|
||||
url = keyConnectorUrl,
|
||||
accessToken = loginResponse.accessToken,
|
||||
kdfType = loginResponse.kdfType,
|
||||
kdfIterations = loginResponse.kdfIterations,
|
||||
kdfMemory = loginResponse.kdfMemory,
|
||||
kdfParallelism = loginResponse.kdfParallelism,
|
||||
organizationIdentifier = orgIdentifier,
|
||||
)
|
||||
.map { keyConnectorResponse ->
|
||||
val result = unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = keyConnectorResponse.keys.private,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||
masterKey = keyConnectorResponse.masterKey,
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
),
|
||||
)
|
||||
if (result is VaultUnlockResult.Success) {
|
||||
// We now know that login/unlock was successful, so we store the userKey
|
||||
// and privateKey we now have since it didn't exist on the loginResponse
|
||||
authDiskSource.storeUserKey(
|
||||
userId = profile.userId,
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
)
|
||||
authDiskSource.storePrivateKey(
|
||||
userId = profile.userId,
|
||||
privateKey = keyConnectorResponse.keys.private,
|
||||
)
|
||||
}
|
||||
result
|
||||
}
|
||||
.fold(
|
||||
// If the request failed, we want to abort the login process
|
||||
onFailure = { VaultUnlockResult.GenericError },
|
||||
onSuccess = { it },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the current user's vault with password data.
|
||||
*/
|
||||
private suspend fun unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
userStateJson: UserStateJson,
|
||||
profile: AccountJson.Profile,
|
||||
password: String?,
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with password if possible.
|
||||
@@ -1491,7 +1863,7 @@ class AuthRepositoryImpl(
|
||||
val privateKey = loginResponse.privateKey ?: return null
|
||||
val key = loginResponse.key ?: return null
|
||||
return unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.Password(
|
||||
password = masterPassword,
|
||||
@@ -1505,7 +1877,7 @@ class AuthRepositoryImpl(
|
||||
*/
|
||||
private suspend fun unlockVaultWithTdeOnLoginSuccess(
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
userStateJson: UserStateJson,
|
||||
profile: AccountJson.Profile,
|
||||
deviceData: DeviceDataModel?,
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with auth request if possible.
|
||||
@@ -1513,7 +1885,7 @@ class AuthRepositoryImpl(
|
||||
if (loginResponse.privateKey != null && loginResponse.key != null) {
|
||||
deviceData?.let { model ->
|
||||
return unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
accountProfile = profile,
|
||||
privateKey = loginResponse.privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = model.privateKey,
|
||||
@@ -1542,7 +1914,7 @@ class AuthRepositoryImpl(
|
||||
loginResponse.privateKey?.let { privateKey ->
|
||||
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options = options,
|
||||
userStateJson = userStateJson,
|
||||
profile = profile,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
}
|
||||
@@ -1555,11 +1927,11 @@ class AuthRepositoryImpl(
|
||||
*/
|
||||
private suspend fun unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options: TrustedDeviceUserDecryptionOptionsJson,
|
||||
userStateJson: UserStateJson,
|
||||
profile: AccountJson.Profile,
|
||||
privateKey: String,
|
||||
): VaultUnlockResult? {
|
||||
var vaultUnlockResult: VaultUnlockResult? = null
|
||||
val userId = userStateJson.activeUserId
|
||||
val userId = profile.userId
|
||||
val deviceKey = authDiskSource.getDeviceKey(userId = userId)
|
||||
if (deviceKey == null) {
|
||||
// A null device key means this device is not trusted.
|
||||
@@ -1573,7 +1945,7 @@ class AuthRepositoryImpl(
|
||||
// For approved requests the key will always be present.
|
||||
val userKey = requireNotNull(request.key)
|
||||
vaultUnlockResult = unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = pendingRequest.requestPrivateKey,
|
||||
@@ -1600,7 +1972,7 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
|
||||
vaultUnlockResult = unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DeviceKey(
|
||||
deviceKey = deviceKey,
|
||||
|
||||
@@ -8,11 +8,15 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.OrganizationService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
@@ -44,16 +48,20 @@ object AuthRepositoryModule {
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
configDiskSource: ConfigDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
keyConnectorManager: KeyConnectorManager,
|
||||
authRequestManager: AuthRequestManager,
|
||||
trustedDeviceManager: TrustedDeviceManager,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
pushManager: PushManager,
|
||||
policyManager: PolicyManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
accountsService = accountsService,
|
||||
devicesService = devicesService,
|
||||
@@ -62,16 +70,20 @@ object AuthRepositoryModule {
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = authDiskSource,
|
||||
configDiskSource = configDiskSource,
|
||||
haveIBeenPwnedService = haveIBeenPwnedService,
|
||||
dispatcherManager = dispatcherManager,
|
||||
environmentRepository = environmentRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
keyConnectorManager = keyConnectorManager,
|
||||
authRequestManager = authRequestManager,
|
||||
trustedDeviceManager = trustedDeviceManager,
|
||||
userLogoutManager = userLogoutManager,
|
||||
pushManager = pushManager,
|
||||
policyManager = policyManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
logsManager = logsManager,
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user