mirror of
https://github.com/bitwarden/android.git
synced 2026-05-09 13:29:18 -05:00
Compare commits
529 Commits
release/20
...
release-no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf2ac7cac9 | ||
|
|
5bbbf7c5e4 | ||
|
|
39d10fa77d | ||
|
|
f0f240f8e5 | ||
|
|
ac6ff98041 | ||
|
|
ec030f2c2e | ||
|
|
be08c1a536 | ||
|
|
84edf4ead0 | ||
|
|
915aac561c | ||
|
|
2027a66f02 | ||
|
|
ef6d9bc68c | ||
|
|
3ceda9e40a | ||
|
|
3f1a6e97fd | ||
|
|
4448ab05ce | ||
|
|
d584391843 | ||
|
|
e0d91d7682 | ||
|
|
e8a98dd3ed | ||
|
|
b6163cf53c | ||
|
|
a24c6f2719 | ||
|
|
b3219d4040 | ||
|
|
27043e28a8 | ||
|
|
7c36b7cb82 | ||
|
|
fcfcf48cee | ||
|
|
757994ec18 | ||
|
|
548de56d60 | ||
|
|
71cd917328 | ||
|
|
01188c21b2 | ||
|
|
5c076871ab | ||
|
|
dca7284230 | ||
|
|
07d3849c4b | ||
|
|
33c3fd28e9 | ||
|
|
88609c2f5b | ||
|
|
af8cfcd2f0 | ||
|
|
7cc8108498 | ||
|
|
a31c499b15 | ||
|
|
d7d099477f | ||
|
|
0ba240852f | ||
|
|
727d943fae | ||
|
|
234f49a92c | ||
|
|
4f49d3d504 | ||
|
|
aba8344df1 | ||
|
|
6da8e2c47b | ||
|
|
78d5965271 | ||
|
|
6953d5e132 | ||
|
|
7073124495 | ||
|
|
0cc7067808 | ||
|
|
9e920f1cf5 | ||
|
|
537e743891 | ||
|
|
60da236f3e | ||
|
|
7804d8430f | ||
|
|
2893c3871f | ||
|
|
d04ac5e672 | ||
|
|
55e03565a6 | ||
|
|
768f7a3fd9 | ||
|
|
1d02737093 | ||
|
|
dab06b0ed4 | ||
|
|
fb792a668b | ||
|
|
64da29ffaa | ||
|
|
675cbb7c4f | ||
|
|
6b63218839 | ||
|
|
30a1bba796 | ||
|
|
c2d9e4858b | ||
|
|
d8e42083b7 | ||
|
|
ac7fbfd129 | ||
|
|
25dfa74bdf | ||
|
|
85a98e86c4 | ||
|
|
078d80d3f6 | ||
|
|
00eb78f02e | ||
|
|
eadfac5ea8 | ||
|
|
a651d9b1fc | ||
|
|
6308aed34a | ||
|
|
011d637f7c | ||
|
|
0b03d2c0d5 | ||
|
|
bb7e4061cc | ||
|
|
d1308cb936 | ||
|
|
892f817b2a | ||
|
|
86e5789f30 | ||
|
|
80bd1bfde2 | ||
|
|
4943df24b3 | ||
|
|
b7464b87d9 | ||
|
|
3fb7904a36 | ||
|
|
9d9f9e3e72 | ||
|
|
a061cbb1d3 | ||
|
|
ad03f8c996 | ||
|
|
aac2345a64 | ||
|
|
61c48bf673 | ||
|
|
77b631b021 | ||
|
|
c59a28a9df | ||
|
|
1349165156 | ||
|
|
65cc0a0dd8 | ||
|
|
0ba67f5887 | ||
|
|
87f64d7aba | ||
|
|
063003b4aa | ||
|
|
f2eb524da4 | ||
|
|
e929ca8a7d | ||
|
|
acc5e30b7a | ||
|
|
26d4a397a6 | ||
|
|
ba9a0d8884 | ||
|
|
bb2e98eac6 | ||
|
|
133d8548e8 | ||
|
|
9f19a99eb9 | ||
|
|
22931bbd38 | ||
|
|
359dedc9d7 | ||
|
|
be416f87de | ||
|
|
6a3a534304 | ||
|
|
09254a2285 | ||
|
|
f0d6599eb3 | ||
|
|
d7a50b092b | ||
|
|
9f616b67c9 | ||
|
|
1198de2a74 | ||
|
|
3db7b17e3a | ||
|
|
631be3fca5 | ||
|
|
e8e6040318 | ||
|
|
5790397a27 | ||
|
|
8b98e8f461 | ||
|
|
f3482bd3e2 | ||
|
|
14ef7351bd | ||
|
|
0820061466 | ||
|
|
c5616b9e38 | ||
|
|
0538becf7f | ||
|
|
2ae3d5e100 | ||
|
|
2aa371a972 | ||
|
|
72ac8d3699 | ||
|
|
00f30c922d | ||
|
|
a68b370cba | ||
|
|
60a6b2a545 | ||
|
|
2c63c1ab94 | ||
|
|
fdc92712c4 | ||
|
|
4eb4d5f256 | ||
|
|
6fdb39026f | ||
|
|
ac6c90eb7a | ||
|
|
eb3bc838d6 | ||
|
|
30e882d7d1 | ||
|
|
3adc80fdf8 | ||
|
|
e70b21c951 | ||
|
|
571b8368e1 | ||
|
|
fd26472f71 | ||
|
|
6ffe9df353 | ||
|
|
7e0fc07a7c | ||
|
|
d27ab1d31a | ||
|
|
7b9cbb7bef | ||
|
|
a1a3d55656 | ||
|
|
c672bff18c | ||
|
|
7ab5972893 | ||
|
|
13aa3251d5 | ||
|
|
a5f6864512 | ||
|
|
c3506c1c25 | ||
|
|
1710b563c0 | ||
|
|
01b646afa7 | ||
|
|
445bd90f67 | ||
|
|
de7416d96e | ||
|
|
5e29df5f30 | ||
|
|
5f48d7bebd | ||
|
|
b342f21cbb | ||
|
|
4b3ec1d918 | ||
|
|
5f52c49a68 | ||
|
|
efe1475fbf | ||
|
|
ffe5ddb4a6 | ||
|
|
6f82251332 | ||
|
|
76e780f813 | ||
|
|
161d8517a4 | ||
|
|
0786ab98a7 | ||
|
|
3d12c98969 | ||
|
|
07fe6e53ea | ||
|
|
2bbadf8726 | ||
|
|
f004e10d41 | ||
|
|
38c9d6cfbc | ||
|
|
62d26491d5 | ||
|
|
925db01b44 | ||
|
|
2b79cc9a17 | ||
|
|
f141465b41 | ||
|
|
ecc5d4ce30 | ||
|
|
de558bf94d | ||
|
|
9b3bb32c87 | ||
|
|
6f5cb0df96 | ||
|
|
2195d07f78 | ||
|
|
c09b02f1d0 | ||
|
|
76c7f8c41d | ||
|
|
6a1f37b243 | ||
|
|
590fc21820 | ||
|
|
4575fa96c1 | ||
|
|
1a7ddbef7a | ||
|
|
555d30645f | ||
|
|
80d04fdf9c | ||
|
|
4ea92223c0 | ||
|
|
f8e41caaf2 | ||
|
|
3963623407 | ||
|
|
bbb2d8dc05 | ||
|
|
db035bbd7d | ||
|
|
69f33ddca9 | ||
|
|
2eca28d571 | ||
|
|
d9ac445149 | ||
|
|
fe06bf48e7 | ||
|
|
3f1f9983e3 | ||
|
|
a681402956 | ||
|
|
3c7262d2b3 | ||
|
|
7a25aafc23 | ||
|
|
b2c4fbb593 | ||
|
|
e2dec1f2fe | ||
|
|
5f1ef71b3b | ||
|
|
b3550bc933 | ||
|
|
09b898d655 | ||
|
|
92b92403c6 | ||
|
|
b84d393208 | ||
|
|
464f8de5f5 | ||
|
|
3a6db38172 | ||
|
|
dbefcfe342 | ||
|
|
e2d2d7fd7d | ||
|
|
31ccf490ae | ||
|
|
50ae0902f0 | ||
|
|
873f416fec | ||
|
|
70e72da404 | ||
|
|
d573ce6e10 | ||
|
|
e9159cc8c1 | ||
|
|
bf60f8f9e3 | ||
|
|
2787edbf45 | ||
|
|
bb5aeaaf15 | ||
|
|
f68f44615c | ||
|
|
66ff5942e4 | ||
|
|
a11d8eff4a | ||
|
|
b233505c24 | ||
|
|
ad564f98ef | ||
|
|
d488e7d5dd | ||
|
|
2d66fb5702 | ||
|
|
2b94e01c56 | ||
|
|
08e51fde98 | ||
|
|
e25743e3f0 | ||
|
|
055c598491 | ||
|
|
a185a94d56 | ||
|
|
f1ee9c89c0 | ||
|
|
5b81c4dc7c | ||
|
|
e54d9ab5a9 | ||
|
|
f9fc61ecf5 | ||
|
|
a0eadc282b | ||
|
|
412649ed9e | ||
|
|
be4014962c | ||
|
|
5840bcdf85 | ||
|
|
2672f426dc | ||
|
|
20f9421ea4 | ||
|
|
c1bb58cf17 | ||
|
|
f92d748de3 | ||
|
|
21597ba746 | ||
|
|
efbb959ecc | ||
|
|
b128d5de0a | ||
|
|
41d9e96406 | ||
|
|
ac6bb35d76 | ||
|
|
1316882ef4 | ||
|
|
7fdb669786 | ||
|
|
f595cee7f6 | ||
|
|
43844739a2 | ||
|
|
a3096c04f1 | ||
|
|
0684011bda | ||
|
|
eef0b1cbbb | ||
|
|
542d2ad1e9 | ||
|
|
40056f3181 | ||
|
|
0ca97de1b2 | ||
|
|
2acede98f3 | ||
|
|
fb2edf43be | ||
|
|
9635bd9b43 | ||
|
|
8dd9ba65d0 | ||
|
|
ad90785c39 | ||
|
|
ea1a4e4710 | ||
|
|
2f678ba32e | ||
|
|
5b0b3b6c70 | ||
|
|
65d361625e | ||
|
|
8d09d9d604 | ||
|
|
41cf116e6d | ||
|
|
ff09bdd110 | ||
|
|
ec788f2472 | ||
|
|
6e643cb43b | ||
|
|
6cb734fb94 | ||
|
|
00ce3c038b | ||
|
|
d4e7211156 | ||
|
|
1b3e254a3f | ||
|
|
52748ba66f | ||
|
|
86057b5923 | ||
|
|
7d1e2fec90 | ||
|
|
5e94b1b689 | ||
|
|
f3f51cf244 | ||
|
|
5f109b5085 | ||
|
|
a0cc8a8a3d | ||
|
|
5b53b50b01 | ||
|
|
abbf6565d4 | ||
|
|
5263329ab5 | ||
|
|
c3d2c17830 | ||
|
|
29213421ec | ||
|
|
5184be0e98 | ||
|
|
0643ecc00b | ||
|
|
7e5dcd3814 | ||
|
|
6763bfd997 | ||
|
|
aeba03a769 | ||
|
|
7f25fa07a4 | ||
|
|
8b00773c84 | ||
|
|
b42ec0ae13 | ||
|
|
78d7865207 | ||
|
|
5ac5e31dd2 | ||
|
|
bcc7e6756e | ||
|
|
aa42e0ad18 | ||
|
|
c0abc1d647 | ||
|
|
5de6dc3473 | ||
|
|
bb6255b6a4 | ||
|
|
408d01d546 | ||
|
|
3b9e42a256 | ||
|
|
6d500e52e7 | ||
|
|
3f72b49286 | ||
|
|
8b9f13e9e7 | ||
|
|
1d2095b0b3 | ||
|
|
5fdfb26950 | ||
|
|
5d7656323b | ||
|
|
932c26dfc6 | ||
|
|
5c38522ec6 | ||
|
|
ab4d9b9984 | ||
|
|
74b1bc8303 | ||
|
|
25e01c86bc | ||
|
|
b127021701 | ||
|
|
123d227c13 | ||
|
|
88b96812de | ||
|
|
8abe62e53e | ||
|
|
d07a9dcf81 | ||
|
|
1820073b65 | ||
|
|
1669fa4044 | ||
|
|
d3c1f8e26a | ||
|
|
a78f81014e | ||
|
|
4c74f720eb | ||
|
|
03251abe88 | ||
|
|
43fb630393 | ||
|
|
54e59cc61b | ||
|
|
d6ae7da44c | ||
|
|
280aa35b73 | ||
|
|
f3bce23942 | ||
|
|
1015654d27 | ||
|
|
d8c80f7e28 | ||
|
|
3aaa93675d | ||
|
|
af0f894dd3 | ||
|
|
6e7f5a31f1 | ||
|
|
79b7866e30 | ||
|
|
d8da7a8cc2 | ||
|
|
e36e9e878f | ||
|
|
93423d4b13 | ||
|
|
ee165491c9 | ||
|
|
8ad9bc5eed | ||
|
|
e2e49a4555 | ||
|
|
a92fd5b35d | ||
|
|
5e5b29677a | ||
|
|
9f4a85d373 | ||
|
|
b0edb7cf3b | ||
|
|
0d5bbc177c | ||
|
|
f7c14890f1 | ||
|
|
924ff87979 | ||
|
|
713bb51974 | ||
|
|
6faf720076 | ||
|
|
a13a7c363f | ||
|
|
9d6192ba1f | ||
|
|
ddeece0a5c | ||
|
|
3445191a05 | ||
|
|
2d75679586 | ||
|
|
d676a6c76c | ||
|
|
26eeb39f88 | ||
|
|
49c2e3030c | ||
|
|
ea267a0298 | ||
|
|
6e0063c977 | ||
|
|
1f89314bbb | ||
|
|
afd7097b53 | ||
|
|
087cd5a093 | ||
|
|
06d742bd16 | ||
|
|
05bcaf9dbc | ||
|
|
d37fa652bb | ||
|
|
170e74db55 | ||
|
|
b75133516d | ||
|
|
e90c41a3b7 | ||
|
|
5758b34dcf | ||
|
|
b49d8a18a2 | ||
|
|
0d77e7085b | ||
|
|
d0203eedc4 | ||
|
|
3c74a342d1 | ||
|
|
8503610b0b | ||
|
|
ae1225b948 | ||
|
|
959361adc2 | ||
|
|
f9ccb766c2 | ||
|
|
e5e5d3c67c | ||
|
|
ea1813a1b6 | ||
|
|
b1f0a10a55 | ||
|
|
1af16bfbef | ||
|
|
524e0941e4 | ||
|
|
bcea5a6b25 | ||
|
|
b2a256e2fc | ||
|
|
83c8db6867 | ||
|
|
96e2985aed | ||
|
|
5e544ff1f9 | ||
|
|
58c31937a2 | ||
|
|
01ac1f3f93 | ||
|
|
f93d11fe36 | ||
|
|
44a53f18d3 | ||
|
|
72218b643d | ||
|
|
d00a77e247 | ||
|
|
6e9f9d62a1 | ||
|
|
48f30f022d | ||
|
|
2f526a7725 | ||
|
|
43855ab555 | ||
|
|
9fdeb8e639 | ||
|
|
a19523a3f8 | ||
|
|
023e5dd2fc | ||
|
|
0af9bb887a | ||
|
|
1e1beec6e4 | ||
|
|
9a72c0c8e5 | ||
|
|
cff5c7b9a2 | ||
|
|
be96f1b9d4 | ||
|
|
b8511ec4f8 | ||
|
|
e5f7488a7c | ||
|
|
a2b7132cf3 | ||
|
|
8be57a6ef7 | ||
|
|
49a5ff34df | ||
|
|
53ac8ac49c | ||
|
|
2b721ac52c | ||
|
|
31f45ae5a4 | ||
|
|
f912e00d21 | ||
|
|
7ca1eab4b2 | ||
|
|
cbb469e5ab | ||
|
|
17c9008f95 | ||
|
|
2640d28468 | ||
|
|
f695254fc1 | ||
|
|
18e5b711bb | ||
|
|
9912123293 | ||
|
|
e737f3260d | ||
|
|
be8562b4db | ||
|
|
dc0413b416 | ||
|
|
d1b5f3078e | ||
|
|
c8194545a4 | ||
|
|
c67a5d1a8d | ||
|
|
00b35bd3ab | ||
|
|
a40f3b91db | ||
|
|
9537bfaf6f | ||
|
|
e9fb28d374 | ||
|
|
1161c3c446 | ||
|
|
d3e4b56d30 | ||
|
|
f9dd295188 | ||
|
|
6fae5b8b77 | ||
|
|
2529ed3fb9 | ||
|
|
a1e7e92d6d | ||
|
|
15251c840c | ||
|
|
663265d7b3 | ||
|
|
f2365771ff | ||
|
|
97e6758449 | ||
|
|
91e197db2c | ||
|
|
8e10170347 | ||
|
|
74b6fc7e2e | ||
|
|
6b4f959b12 | ||
|
|
578efd3b7f | ||
|
|
31669cf457 | ||
|
|
d9e29d3c81 | ||
|
|
5c38d62a61 | ||
|
|
9b5449f657 | ||
|
|
6da31f797d | ||
|
|
f546bd2640 | ||
|
|
d32ed06516 | ||
|
|
26c22295fc | ||
|
|
de9aebbda9 | ||
|
|
f7ea3a7972 | ||
|
|
51c3f2b04a | ||
|
|
61d80793e7 | ||
|
|
b59814aa83 | ||
|
|
5c46e913a2 | ||
|
|
1a592273bf | ||
|
|
b597a2fb62 | ||
|
|
ea55cbc914 | ||
|
|
86d8a2ed8d | ||
|
|
4bfe787f01 | ||
|
|
c0b04694d9 | ||
|
|
dd4a0a502d | ||
|
|
1eb4a9770e | ||
|
|
165ae77113 | ||
|
|
dad060e8c1 | ||
|
|
75fb200eaf | ||
|
|
2bd0df2910 | ||
|
|
2b6fe1b28a | ||
|
|
f39669e615 | ||
|
|
1f0881f74e | ||
|
|
cb158b0947 | ||
|
|
3adc43683a | ||
|
|
3ae4c72b46 | ||
|
|
7e93c1a74b | ||
|
|
f191f02296 | ||
|
|
9e86332c5e | ||
|
|
d85904748e | ||
|
|
5167b2c491 | ||
|
|
6b88bcf02f | ||
|
|
eb97a33e8e | ||
|
|
e829e1e2ca | ||
|
|
015cbdd37a | ||
|
|
66d834c7e9 | ||
|
|
066d5c5628 | ||
|
|
34c547a431 | ||
|
|
af1ab4c953 | ||
|
|
186766960f | ||
|
|
de02a6999d | ||
|
|
736761ee93 | ||
|
|
7bad184849 | ||
|
|
612c8e8aa3 | ||
|
|
8da98f95e1 | ||
|
|
6d4df646af | ||
|
|
d98db6ee67 | ||
|
|
12e5314c61 | ||
|
|
b6a165aef5 | ||
|
|
6b025832d7 | ||
|
|
ea58313e31 | ||
|
|
a27002af0f | ||
|
|
8ad7c184f1 | ||
|
|
168e662a38 | ||
|
|
a00452faf1 | ||
|
|
32b0c90f17 | ||
|
|
cd992a6994 | ||
|
|
38a92042de | ||
|
|
65263ebad9 | ||
|
|
50530b57c6 | ||
|
|
04a565e5a2 | ||
|
|
9aa091818d | ||
|
|
6a226f21bd | ||
|
|
67d641572e | ||
|
|
a92ed17d65 | ||
|
|
580a0eecdd | ||
|
|
d28a754ee9 | ||
|
|
5fd8593095 | ||
|
|
bcaf00dc97 | ||
|
|
c012e3cb7e | ||
|
|
4575f52fe0 | ||
|
|
c01a101f83 | ||
|
|
8553fb3995 | ||
|
|
3636da5c4e | ||
|
|
8bd430b793 |
@@ -8,4 +8,4 @@ checkmarx:
|
||||
configs:
|
||||
sast:
|
||||
# Exclude test directories
|
||||
filter: "!app/src/test/**"
|
||||
filter: "**/test/**,!**/androidTest/**,!**/commonTest/**,!**/jvmTest/**,!**/jsTest/**,!**/iosTest/**"
|
||||
|
||||
129
.editorconfig
129
.editorconfig
@@ -12,127 +12,20 @@ end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
guidelines = 120
|
||||
|
||||
# Code files
|
||||
[*.{cs,csx,vb,vbx}]
|
||||
indent_size = 4
|
||||
|
||||
# Xml project files
|
||||
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
|
||||
indent_size = 2
|
||||
|
||||
# Xml config files
|
||||
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
|
||||
indent_size = 2
|
||||
|
||||
# JSON files
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
# JS files
|
||||
[*.{js,ts,scss,html}]
|
||||
# Kotlin files
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
[*.{kt,kts}]
|
||||
# https://pinterest.github.io/ktlint/1.0.1/rules/configuration-ktlint/#trailing-comma-on-declaration-site
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
# https://pinterest.github.io/ktlint/1.0.1/rules/configuration-ktlint/#trailing-comma-on-declaration-site
|
||||
trailing-comma-on-declaration-site = true
|
||||
# https://pinterest.github.io/ktlint/1.0.1/rules/configuration-ktlint/#trailing-comma-on-call-site
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
|
||||
[*.{scss,yml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{ts}]
|
||||
quote_type = single
|
||||
|
||||
[*.{scss,yml,csproj}]
|
||||
indent_size = 2
|
||||
|
||||
[*.sln]
|
||||
indent_style = tab
|
||||
|
||||
# Dotnet code style settings:
|
||||
[*.{cs,vb}]
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
|
||||
# Use language keywords instead of framework type names for type references
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
|
||||
# Prefix private members with underscore
|
||||
dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
|
||||
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
|
||||
dotnet_naming_rule.private_members_with_underscore.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.private_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
|
||||
|
||||
dotnet_naming_style.prefix_underscore.capitalization = camel_case
|
||||
dotnet_naming_style.prefix_underscore.required_prefix = _
|
||||
|
||||
# Async methods should have "Async" suffix
|
||||
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
|
||||
dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
|
||||
dotnet_naming_rule.async_methods_end_in_async.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.any_async_methods.applicable_kinds = method
|
||||
dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.any_async_methods.required_modifiers = async
|
||||
|
||||
dotnet_naming_style.end_in_async.required_prefix =
|
||||
dotnet_naming_style.end_in_async.required_suffix = Async
|
||||
dotnet_naming_style.end_in_async.capitalization = pascal_case
|
||||
dotnet_naming_style.end_in_async.word_separator =
|
||||
|
||||
# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items.
|
||||
dotnet_diagnostic.CS0618.severity = suggestion
|
||||
|
||||
# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items.
|
||||
dotnet_diagnostic.CS0612.severity = suggestion
|
||||
|
||||
# Remove unnecessary using directives https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
|
||||
# CSharp code style settings:
|
||||
[*.cs]
|
||||
# Prefer "var" everywhere
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
|
||||
# Prefer method-like constructs to have a expression-body
|
||||
csharp_style_expression_bodied_methods = true:none
|
||||
csharp_style_expression_bodied_constructors = true:none
|
||||
csharp_style_expression_bodied_operators = true:none
|
||||
|
||||
# Prefer property-like constructs to have an expression-body
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Suggest more modern language features when available
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Newline settings
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
|
||||
# Namespace settings
|
||||
csharp_style_namespace_declarations = file_scoped:warning
|
||||
|
||||
# Switch expression
|
||||
dotnet_diagnostic.CS8509.severity = error # missing switch case for named enum value
|
||||
dotnet_diagnostic.CS8524.severity = none # missing switch case for unnamed enum value
|
||||
|
||||
84
.github/ISSUE_TEMPLATE/bug-bwa.yml
vendored
Normal file
84
.github/ISSUE_TEMPLATE/bug-bwa.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: Authenticator Android App Bug Report
|
||||
description: File a bug report
|
||||
labels: [ "app:authenticator", "bug" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
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: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: How can we reproduce the behavior.
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. Click on '...'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Result
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual Result
|
||||
description: A clear and concise description of what is happening.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots or Videos
|
||||
description: If applicable, add screenshots and/or a short video to help explain your problem.
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here.
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Build Version
|
||||
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:
|
||||
label: Environment Details
|
||||
placeholder: |
|
||||
- Device: [e.g. Pixel Tablet, Samsung Galaxy S24 ]
|
||||
- OS Version: [e.g. API 32, Tiramisu ]
|
||||
- type: checkboxes
|
||||
id: issue-tracking-info
|
||||
attributes:
|
||||
label: Issue Tracking Info
|
||||
description: |
|
||||
Issue tracking information
|
||||
options:
|
||||
- label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.
|
||||
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Android Bug Report
|
||||
name: Password Manager Android App Bug Report
|
||||
description: File a bug report
|
||||
labels: [ bug ]
|
||||
labels: [ "app:password-manager", "bug" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Legacy Android Bug Reports
|
||||
url: https://github.com/bitwarden/mobile/issues
|
||||
about: Bugs found in the publicly available .NET MAUI app should be reported in [bitwarden/mobile](https://github.com/bitwarden/mobile)
|
||||
- name: Feature Requests
|
||||
url: https://community.bitwarden.com/c/feature-requests/
|
||||
about: Request new features using the Community Forums. Please search existing feature requests before making a new one.
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -15,10 +15,11 @@
|
||||
- Contributor guidelines followed
|
||||
- All formatters and local linters executed and passed
|
||||
- Written new unit and / or integration tests where applicable
|
||||
- Protected functional changes with optionality (feature flags)
|
||||
- Used internationalization (i18n) for all UI strings
|
||||
- CI builds passed
|
||||
- Communicated to DevOps any deployment requirements
|
||||
- Updated any necessary documentation or informed the documentation team
|
||||
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team
|
||||
|
||||
## 🦮 Reviewer guidelines
|
||||
|
||||
@@ -27,8 +28,7 @@
|
||||
- 👍 (`:+1:`) or similar for great changes
|
||||
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
|
||||
- ❓ (`:question:`) for questions
|
||||
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed
|
||||
issue and could potentially benefit from discussion
|
||||
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
|
||||
- 🎨 (`:art:`) for suggestions / improvements
|
||||
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
|
||||
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
|
||||
|
||||
2
.github/codecov.yml
vendored
Normal file
2
.github/codecov.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
ignore:
|
||||
- "src/test/**" # Tests
|
||||
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
@@ -18,6 +18,7 @@
|
||||
"description": "Kotlin and Compose dependencies that must be updated together to maintain compatibility.",
|
||||
"matchPackagePatterns": [
|
||||
"androidx.compose:compose-bom",
|
||||
"androidx.lifecycle:*",
|
||||
"org.jetbrains.kotlin.*",
|
||||
"com.google.devtools.ksp"
|
||||
],
|
||||
|
||||
64
.github/scripts/release-notes/linked_issues.py
vendored
Normal file
64
.github/scripts/release-notes/linked_issues.py
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
import sys
|
||||
import subprocess
|
||||
from typing import List
|
||||
|
||||
def create_linked_issue_comment(repo_owner: str, repo_name: str, release_name: str, release_link: str, pr_numbers: List[int]) -> str:
|
||||
if len(pr_numbers) == 0:
|
||||
return ""
|
||||
|
||||
pr_links = [f"* https://github.com/{repo_owner}/{repo_name}/pull/{pr_number}" for pr_number in pr_numbers]
|
||||
|
||||
return f":shipit: Pull Request(s) linked to this issue released in [{release_name}]({release_link}):\n\n"+ "\n".join(pr_links)
|
||||
|
||||
def comment_linked_issues_in_pr(owner: str, repo: str, pr_number: int) -> None:
|
||||
"""Use GitHub CLI to comment all issues linked to a PR.
|
||||
"""
|
||||
|
||||
|
||||
linked_issues = get_linked_issues(owner, repo, pr_number)
|
||||
for issue_number in linked_issues:
|
||||
comment_github_issue(owner, repo, issue_number, comment)
|
||||
|
||||
def comment_github_issue(owner: str, repo: str, issue_number: int, comment: str) -> None:
|
||||
"""Use GitHub CLI to comment on an issue.
|
||||
"""
|
||||
subprocess.run([
|
||||
'gh', 'issue', 'comment', str(issue_number), '--body', comment, '--repo', f'{owner}/{repo}'
|
||||
], check=True)
|
||||
|
||||
def get_linked_issues(owner: str, repo: str, pr_number: int) -> List[int]:
|
||||
"""Use GitHub CLI to retrieve linked issue numbers for a PR.
|
||||
"""
|
||||
|
||||
query = """
|
||||
query ($owner: String!, $repo: String!, $pr: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pr) {
|
||||
closingIssuesReferences(first: 100) {
|
||||
nodes {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
try:
|
||||
result = subprocess.run([
|
||||
'gh', 'api', 'graphql',
|
||||
'-F', f'owner={owner}',
|
||||
'-F', f'repo={repo}',
|
||||
'-F', f'pr={pr_number}',
|
||||
'-f', f'query={query}',
|
||||
'--jq', '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'
|
||||
], capture_output=True, text=True, check=True)
|
||||
|
||||
# Split output into lines and convert to integers
|
||||
if result.stdout.strip():
|
||||
return [int(num) for num in result.stdout.strip().split('\n')]
|
||||
return []
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
print(f"Error fetching linked issues for PR #{pr_number}")
|
||||
return []
|
||||
112
.github/scripts/release-notes/process_release_notes.py
vendored
Normal file
112
.github/scripts/release-notes/process_release_notes.py
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
from typing import List, Tuple
|
||||
|
||||
def extract_jira_tickets(line: str) -> List[str]:
|
||||
# Find all Jira tickets in format ABC-123 (with any prefix/suffix)
|
||||
return re.findall(r'[A-Z]+-\d+', line)
|
||||
|
||||
def extract_pr_numbers(line: str) -> List[str]:
|
||||
# Match PR numbers from GitHub format (#123)
|
||||
return re.findall(r'#(\d+)', line)
|
||||
|
||||
def process_line(line: str) -> str:
|
||||
"""Process a single line from release notes by removing Jira tickets, conventional commit prefixes and other common patterns.
|
||||
|
||||
Args:
|
||||
line: A single line from release notes
|
||||
|
||||
Returns:
|
||||
Processed line with tickets and prefixes removed
|
||||
|
||||
Example:
|
||||
>>> process_line("[ABC-123] feat(ui): Add new button")
|
||||
"Add new button"
|
||||
"""
|
||||
original = line
|
||||
|
||||
# Remove Jira ticket patterns:
|
||||
# line = re.sub(r'\[[A-Z]+-\d+\]', '', line) # [ABC-123] -> ""
|
||||
# line = re.sub(r'[A-Z]+-\d+:\s', '', line) # ABC-123: -> ""
|
||||
# line = re.sub(r'[A-Z]+-\d+\s-\s', '', line) # ABC-123 - -> ""
|
||||
|
||||
# Remove keywords and their variations
|
||||
patterns = [
|
||||
r'BACKPORT', # BACKPORT -> ""
|
||||
r'[deps]:', # [deps]: -> ""
|
||||
r'feat(?:\([^)]*\))?:', # feat: or feat(ui): -> ""
|
||||
r'bug(?:\([^)]*\))?:', # bug: or bug(core): -> ""
|
||||
r'ci(?:\([^)]*\))?:' # ci: or ci(workflow): -> ""
|
||||
]
|
||||
for pattern in patterns:
|
||||
line = re.sub(pattern, '', line)
|
||||
|
||||
cleaned = line.strip()
|
||||
if cleaned != original.strip():
|
||||
print(f"Processed: {original.strip()} -> {cleaned}")
|
||||
return cleaned
|
||||
|
||||
def process_file(input_file: str) -> Tuple[List[str], List[str], List[str]]:
|
||||
jira_tickets: List[str] = []
|
||||
pr_numbers: List[str] = []
|
||||
processed_lines: List[str] = []
|
||||
|
||||
print("Processing file: ", input_file)
|
||||
|
||||
with open(input_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
should_process = line and not line.endswith(':')
|
||||
|
||||
if should_process:
|
||||
tickets = extract_jira_tickets(line)
|
||||
jira_tickets.extend(tickets)
|
||||
|
||||
prs = extract_pr_numbers(line)
|
||||
pr_numbers.extend(prs)
|
||||
processed_lines.append(process_line(line))
|
||||
else:
|
||||
processed_lines.append(line)
|
||||
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
jira_tickets = list(dict.fromkeys(jira_tickets))
|
||||
pr_numbers = list(dict.fromkeys(pr_numbers))
|
||||
|
||||
print("Jira tickets:", ",".join(jira_tickets))
|
||||
print("PR numbers:", ",".join(pr_numbers))
|
||||
print("Finished processing file: ", input_file)
|
||||
return jira_tickets, pr_numbers, processed_lines
|
||||
|
||||
def save_results(jira_tickets: List[str], pr_numbers: List[str], processed_lines: List[str],
|
||||
jira_file: str = 'jira_tickets.txt',
|
||||
pr_file: str = 'pr_numbers.txt',
|
||||
processed_file: str = 'processed_notes.txt') -> None:
|
||||
with open(jira_file, 'w') as f:
|
||||
f.write('\n'.join(jira_tickets))
|
||||
|
||||
with open(pr_file, 'w') as f:
|
||||
f.write('\n'.join(pr_numbers))
|
||||
|
||||
with open(processed_file, 'w') as f:
|
||||
f.write('\n'.join(processed_lines))
|
||||
|
||||
if __name__ == '__main__':
|
||||
input_file = 'release_notes.txt'
|
||||
jira_file = 'jira_tickets.txt'
|
||||
pr_file = 'pr_numbers.txt'
|
||||
processed_file = 'processed_notes.txt'
|
||||
|
||||
if len(sys.argv) >= 2:
|
||||
input_file = sys.argv[1]
|
||||
if len(sys.argv) >= 3:
|
||||
jira_file = sys.argv[2]
|
||||
if len(sys.argv) >= 4:
|
||||
pr_file = sys.argv[3]
|
||||
if len(sys.argv) >= 5:
|
||||
processed_file = sys.argv[4]
|
||||
|
||||
jira_tickets, pr_numbers, processed_lines = process_file(input_file)
|
||||
save_results(jira_tickets, pr_numbers, processed_lines, jira_file, pr_file, processed_file)
|
||||
4
.github/scripts/release-notes/pyproject.toml
vendored
Normal file
4
.github/scripts/release-notes/pyproject.toml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[project]
|
||||
name = "release-notes-processor"
|
||||
description = "Process GitHub release notes to clean up formatting and extract relevant IDs."
|
||||
requires-python = ">=3.13"
|
||||
30
.github/scripts/release-notes/test_linked_issues.py
vendored
Normal file
30
.github/scripts/release-notes/test_linked_issues.py
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
import unittest
|
||||
from linked_issues import get_linked_issues, create_linked_issue_comment
|
||||
|
||||
class TestLinkedIssues(unittest.TestCase):
|
||||
def test_create_linked_issue_comment(self):
|
||||
test_cases = [
|
||||
("bitwarden", "android", "v2025.1.0", "https://github.com/bitwarden/android/releases/tag/v2025.1.0", [4696]),
|
||||
("bitwarden", "android", "v2025.2.0", "https://github.com/bitwarden/android/releases/tag/v2025.2.0", [4809, 1, 2, 3]),
|
||||
("bitwarden", "android", "v2025.3.0", "https://github.com/bitwarden/android/releases/tag/v2025.3.0", []),
|
||||
]
|
||||
|
||||
for owner, repo, release_name, release_link, pr_numbers in test_cases:
|
||||
with self.subTest(msg=f"Creating comment for issue in release {release_name}"):
|
||||
comment = create_linked_issue_comment(owner, repo, release_name, release_link, pr_numbers)
|
||||
print(comment + "\n")
|
||||
|
||||
def test_get_linked_issues(self):
|
||||
test_cases = [
|
||||
("bitwarden", "android", 4696, [4659]),
|
||||
("bitwarden", "android", 4809, [])
|
||||
]
|
||||
|
||||
for owner, repo, pr_id, expected_linked_issues in test_cases:
|
||||
with self.subTest(msg=f"Testing PR #{pr_id} for {owner}/{repo}"):
|
||||
result = get_linked_issues(owner, repo, pr_id)
|
||||
self.assertEqual(sorted(result), sorted(expected_linked_issues))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
95
.github/scripts/release-notes/test_process_release_notes.py
vendored
Normal file
95
.github/scripts/release-notes/test_process_release_notes.py
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
import unittest
|
||||
import tempfile
|
||||
import os
|
||||
from process_release_notes import extract_jira_tickets, extract_pr_numbers, process_line, process_file, get_linked_issues
|
||||
|
||||
class TestProcessReleaseNotes(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.test_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
|
||||
def tearDown(self):
|
||||
os.unlink(self.test_file.name)
|
||||
|
||||
def test_extract_jira_tickets(self):
|
||||
test_cases = [
|
||||
("[ABC-123] Some text", ["ABC-123"]),
|
||||
("DEF-456: Some text", ["DEF-456"]),
|
||||
("GHI-789 - Some text", ["GHI-789"]),
|
||||
("Multiple [ABC-123] and DEF-456: tickets", ["ABC-123", "DEF-456"]),
|
||||
("No tickets here", []),
|
||||
("Mixed formats ABC-123 [DEF-456] GHI-789:", ["ABC-123", "DEF-456", "GHI-789"])
|
||||
]
|
||||
for input_text, expected in test_cases:
|
||||
with self.subTest(input_text=input_text):
|
||||
result = extract_jira_tickets(input_text)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_extract_pr_numbers(self):
|
||||
test_cases = [
|
||||
("PR #123 text", ["123"]),
|
||||
("Multiple PRs #456 and #789", ["456", "789"]),
|
||||
("No PR numbers", [])
|
||||
]
|
||||
for input_text, expected in test_cases:
|
||||
with self.subTest(input_text=input_text):
|
||||
result = extract_pr_numbers(input_text)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_process_line(self):
|
||||
test_cases = [
|
||||
("[ABC-123] BACKPORT Some text", "Some text"),
|
||||
("DEF-456: feat(component): Some text", "Some text"),
|
||||
("GHI-789 - bug(fix): Some text", "Some text"),
|
||||
("ci: Some text", "Some text"),
|
||||
("ci(workflow): Some text", "Some text"),
|
||||
("feat: Direct feature", "Direct feature"),
|
||||
("bug: Simple bugfix", "Simple bugfix"),
|
||||
("Normal text", "Normal text")
|
||||
]
|
||||
for input_text, expected in test_cases:
|
||||
with self.subTest(input_text=input_text):
|
||||
result = process_line(input_text)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_process_file(self):
|
||||
content = """
|
||||
### Features:
|
||||
[ABC-123] feat(comp): Feature 1 #123
|
||||
DEF-456: bug(fix): Bug fix #456
|
||||
GHI-789 - BACKPORT Some text #789
|
||||
|
||||
### Bug Fixes:
|
||||
Another line without changes
|
||||
"""
|
||||
with open(self.test_file.name, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
jira_tickets, pr_numbers, processed_lines = process_file(self.test_file.name)
|
||||
|
||||
self.assertEqual(jira_tickets, ["ABC-123", "DEF-456", "GHI-789"])
|
||||
self.assertEqual(pr_numbers, ["123", "456", "789"])
|
||||
self.assertEqual(processed_lines, [
|
||||
'',
|
||||
'### Features:',
|
||||
'Feature 1 #123',
|
||||
'Bug fix #456',
|
||||
'Some text #789',
|
||||
'',
|
||||
'### Bug Fixes:',
|
||||
'Another line without changes'
|
||||
])
|
||||
|
||||
def test_get_linked_issues(self):
|
||||
test_cases = [
|
||||
("bitwarden", "android", 4696, [4659]),
|
||||
("bitwarden", "android", 4809, [])
|
||||
]
|
||||
|
||||
for owner, repo, pr_id, expected_linked_issues in test_cases:
|
||||
with self.subTest(msg=f"Testing PR #{pr_id} for {owner}/{repo}"):
|
||||
result = get_linked_issues(owner, repo, pr_id)
|
||||
self.assertEqual(sorted(result), sorted(expected_linked_issues))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
289
.github/workflows/build-authenticator.yml
vendored
Normal file
289
.github/workflows/build-authenticator.yml
vendored
Normal file
@@ -0,0 +1,289 @@
|
||||
name: Build Authenticator
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
description: "Optional. Version string to use, in X.Y.Z format. Overrides default in the project."
|
||||
required: false
|
||||
type: string
|
||||
version-code:
|
||||
description: "Optional. Build number to use. Overrides default of GitHub run number."
|
||||
required: false
|
||||
type: number
|
||||
distribute-to-firebase:
|
||||
description: "Optional. Distribute artifacts to Firebase."
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
publish-to-play-store:
|
||||
description: "Optional. Deploy bundle artifact to Google Play Store"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 17
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Authenticator
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Check Authenticator
|
||||
run: bundle exec fastlane checkAuthenticator
|
||||
|
||||
- name: Build Authenticator
|
||||
run: bundle exec fastlane buildAuthenticatorDebug
|
||||
|
||||
publish_playstore:
|
||||
name: Publish Authenticator Play Store artifacts
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: ["aab", "apk"]
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/secrets
|
||||
mkdir -p ${{ github.workspace }}/keystores
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_apk-keystore.jks --file ${{ github.workspace }}/keystores/authenticator_apk-keystore.jks --output none
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_aab-keystore.jks --file ${{ github.workspace }}/keystores/authenticator_aab-keystore.jks --output none
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name com.bitwarden.authenticator-google-services.json --file ${{ github.workspace }}/authenticator/src/google-services.json --output none
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name com.bitwarden.authenticator.dev-google-services.json --file ${{ github.workspace }}/authenticator/src/debug/google-services.json --output none
|
||||
|
||||
- name: Download Firebase credentials
|
||||
if : ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/secrets
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_play_firebase-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_firebase-creds.json --output none
|
||||
|
||||
- name: Download Play Store credentials
|
||||
if: ${{ inputs.publish-to-play-store }}
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/secrets
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
|
||||
|
||||
- name: Verify Play Store credentials
|
||||
if: ${{ inputs.publish-to-play-store }}
|
||||
run: |
|
||||
bundle exec fastlane run validate_play_store_json_key \
|
||||
json_key:${{ github.workspace }}/secrets/authenticator_play_store-creds.json }}
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
DEFAULT_VERSION_CODE=$GITHUB_RUN_NUMBER
|
||||
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
|
||||
bundle exec fastlane setAuthenticatorBuildVersionInfo \
|
||||
versionCode:$VERSION_CODE \
|
||||
versionName:${{ inputs.version-name || '' }}
|
||||
|
||||
regex='versionName = "([^"]+)"'
|
||||
if [[ "$(cat authenticator/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 release Play Store bundle
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
run: |
|
||||
bundle exec fastlane bundleAuthenticatorRelease \
|
||||
storeFile:${{ github.workspace }}/keystores/authenticator_aab-keystore.jks \
|
||||
storePassword:'${{ secrets.BWA_AAB_KEYSTORE_STORE_PASSWORD }}' \
|
||||
keyAlias:authenticatorupload \
|
||||
keyPassword:'${{ secrets.BWA_AAB_KEYSTORE_KEY_PASSWORD }}'
|
||||
|
||||
- name: Generate release Play Store APK
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
run: |
|
||||
bundle exec fastlane buildAuthenticatorRelease \
|
||||
storeFile:${{ github.workspace }}/keystores/authenticator_apk-keystore.jks \
|
||||
storePassword:'${{ secrets.BWA_APK_KEYSTORE_STORE_PASSWORD }}' \
|
||||
keyAlias:bitwardenauthenticator \
|
||||
keyPassword:'${{ secrets.BWA_APK_KEYSTORE_KEY_PASSWORD }}'
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.bitwarden.authenticator.aab
|
||||
path: authenticator/build/outputs/bundle/release/com.bitwarden.authenticator.aab
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload release .apk artifact
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.bitwarden.authenticator.apk
|
||||
path: authenticator/build/outputs/apk/release/com.bitwarden.authenticator.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create checksum file for Release AAB
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
run: |
|
||||
sha256sum "authenticator/build/outputs/bundle/release/com.bitwarden.authenticator.aab" \
|
||||
> ./authenticator-android-aab-sha256.txt
|
||||
|
||||
- name: Create checksum for release .apk artifact
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
run: |
|
||||
sha256sum "authenticator/build/outputs/apk/release/com.bitwarden.authenticator.apk" \
|
||||
> ./authenticator-android-apk-sha256.txt
|
||||
|
||||
- name: Upload .apk SHA file for release
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: authenticator-android-apk-sha256.txt
|
||||
path: ./authenticator-android-apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .aab SHA file for release
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: authenticator-android-aab-sha256.txt
|
||||
path: ./authenticator-android-aab-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install Firebase app distribution plugin
|
||||
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
|
||||
run: bundle exec fastlane add_plugin firebase_app_distribution
|
||||
|
||||
- name: Publish release bundle to Firebase
|
||||
if: ${{ matrix.variant == 'aab' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
env:
|
||||
FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/authenticator_play_firebase-creds.json
|
||||
run: |
|
||||
bundle exec fastlane distributeAuthenticatorReleaseBundleToFirebase \
|
||||
serviceCredentialsFile:${{ env.FIREBASE_CREDS_PATH }}
|
||||
|
||||
# Only publish bundles to Play Store when `publish-to-play-store` is true while building
|
||||
# bundles
|
||||
- name: Publish release bundle to Google Play Store
|
||||
if: ${{ inputs.publish-to-play-store && matrix.variant == 'aab' }}
|
||||
env:
|
||||
PLAY_STORE_CREDS_FILE: ${{ github.workspace }}/secrets/authenticator_play_store-creds.json
|
||||
run: |
|
||||
bundle exec fastlane publishAuthenticatorReleaseToGooglePlayStore \
|
||||
serviceCredentialsFile:${{ env.PLAY_STORE_CREDS_FILE }} \
|
||||
36
.github/workflows/build.yml
vendored
36
.github/workflows/build.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
run: bundle exec fastlane assembleDebugApks
|
||||
|
||||
- name: Upload test reports on failure
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
if: failure()
|
||||
with:
|
||||
name: test-reports
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
|
||||
- name: Upload beta Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
|
||||
@@ -269,7 +269,7 @@ jobs:
|
||||
|
||||
- name: Upload release .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
|
||||
@@ -277,7 +277,7 @@ jobs:
|
||||
|
||||
- name: Upload beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
# When building variants other than 'prod'
|
||||
- name: Upload debug .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
|
||||
@@ -324,7 +324,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
@@ -332,7 +332,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
@@ -340,7 +340,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
@@ -348,7 +348,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
@@ -356,7 +356,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for debug
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
@@ -405,7 +405,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -515,7 +515,7 @@ jobs:
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
|
||||
@@ -527,14 +527,14 @@ jobs:
|
||||
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid SHA file
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk
|
||||
@@ -546,7 +546,7 @@ jobs:
|
||||
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid Beta SHA file
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
56
.github/workflows/crowdin-pull-authenticator.yml
vendored
Normal file
56
.github/workflows/crowdin-pull-authenticator.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Crowdin Sync - Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
schedule:
|
||||
- cron: '0 0 * * 5'
|
||||
|
||||
jobs:
|
||||
crowdin-sync:
|
||||
name: Autosync
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "673718"
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Log in to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
||||
with:
|
||||
config: crowdin-bwa.yml
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
github_user_name: "bitwarden-devops-bot"
|
||||
github_user_email: "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
commit_message: "Autosync the updated translations"
|
||||
localization_branch_name: crowdin-auto-sync
|
||||
create_pull_request: true
|
||||
pull_request_title: "Autosync Crowdin Translations"
|
||||
pull_request_body: "Autosync the updated translations"
|
||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@8dfaf9c206381653e3767e3cb5ea5f08b45f02bf # v2.5.0
|
||||
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
30
.github/workflows/crowdin-push-authenticator.yml
vendored
Normal file
30
.github/workflows/crowdin-push-authenticator.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Crowdin Push - Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 17
|
||||
|
||||
jobs:
|
||||
crowdin-push:
|
||||
name: Crowdin Push
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "673718"
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
||||
with:
|
||||
config: crowdin-bwa.yml
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
2
.github/workflows/crowdin-push.yml
vendored
2
.github/workflows/crowdin-push.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@8dfaf9c206381653e3767e3cb5ea5f08b45f02bf # v2.5.0
|
||||
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
2
.github/workflows/github-release.yml
vendored
2
.github/workflows/github-release.yml
vendored
@@ -95,7 +95,7 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0
|
||||
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
|
||||
with:
|
||||
tag_name: "v${{ inputs.version-name }}"
|
||||
name: "${{ inputs.version-name }} (${{ inputs.version-number }})"
|
||||
|
||||
76
.github/workflows/scan-authenticator.yml
vendored
Normal file
76
.github/workflows/scan-authenticator.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
name: Scan Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
name: Check PR run
|
||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||
|
||||
sast:
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
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 . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- 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 }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
3
.github/workflows/scan-ci.yml
vendored
3
.github/workflows/scan-ci.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
--output-path .
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@@ -58,3 +58,4 @@ jobs:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
|
||||
5
.github/workflows/scan.yml
vendored
5
.github/workflows/scan.yml
vendored
@@ -4,8 +4,6 @@ on:
|
||||
workflow_dispatch:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
@@ -43,7 +41,7 @@ jobs:
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@@ -70,3 +68,4 @@ jobs:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
|
||||
82
.github/workflows/test-authenticator.yml
vendored
Normal file
82
.github/workflows/test-authenticator.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Test Authenticator
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
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-24.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Build and test Authenticator
|
||||
run: |
|
||||
bundle exec fastlane checkAuthenticator
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
with:
|
||||
files: authenticator/build/reports/kover/reportDebug.xml
|
||||
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.0
|
||||
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
bundle exec fastlane check
|
||||
|
||||
- name: Upload test reports
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
if: always()
|
||||
with:
|
||||
name: test-reports
|
||||
@@ -90,17 +90,19 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
if: always()
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- name: Download test artifacts
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
with:
|
||||
name: test-reports
|
||||
|
||||
- name: Upload to codecov.io
|
||||
id: upload-to-codecov
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
os: linux
|
||||
@@ -108,7 +110,7 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
|
||||
- name: Comment PR if tests failed
|
||||
if: steps.upload-to-codecov.outcome == 'failure'
|
||||
if: steps.upload-to-codecov.outcome == 'failure' && (github.event_name == 'push' || github.event_name == 'pull_request')
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -25,5 +25,12 @@ user.properties
|
||||
|
||||
# Secrets
|
||||
/keystores/*.jks
|
||||
/app/src/standardDebug/google-services.json
|
||||
/app/src/standardBeta/google-services.json
|
||||
/app/src/standardRelease/google-services.json
|
||||
/authenticator/src/google-services.json
|
||||
|
||||
# python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
1
.husky/pre-commit
Executable file
1
.husky/pre-commit
Executable file
@@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
24
Gemfile.lock
24
Gemfile.lock
@@ -10,20 +10,20 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1027.0)
|
||||
aws-sdk-core (3.214.0)
|
||||
aws-partitions (1.1040.0)
|
||||
aws-sdk-core (3.216.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (1.97.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.176.1)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-s3 (1.178.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
@@ -68,7 +68,7 @@ GEM
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.226.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
@@ -111,7 +111,7 @@ GEM
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.4.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-plugin-firebase_app_distribution (0.9.1)
|
||||
fastlane-plugin-firebase_app_distribution (0.10.0)
|
||||
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
|
||||
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
|
||||
fastlane-sirp (1.0.0)
|
||||
@@ -163,7 +163,7 @@ GEM
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.9.1)
|
||||
jwt (2.9.3)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
@@ -174,7 +174,7 @@ GEM
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.1)
|
||||
plist (3.7.2)
|
||||
public_suffix (6.0.1)
|
||||
rake (13.2.1)
|
||||
representable (3.2.0)
|
||||
@@ -185,7 +185,7 @@ GEM
|
||||
rexml (3.4.0)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
|
||||
61
README-bwa.md
Normal file
61
README-bwa.md
Normal file
@@ -0,0 +1,61 @@
|
||||
[](https://github.com/bitwarden/authenticator-android/actions/workflows/build-authenticator.yml?query=branch:main)
|
||||
[](https://gitter.im/bitwarden/Lobby)
|
||||
|
||||
# Bitwarden Authenticator Android App
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=com.bitwarden.authenticator" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a>
|
||||
|
||||
Bitwarden Authenticator allows you easily store and generate two-factor authentication codes on your device. The Bitwarden Authenticator Android application is written in Kotlin.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/authenticator-android-codes.png" alt="" width="325" height="650" />
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Minimum SDK**: 28
|
||||
- **Target SDK**: 34
|
||||
- **Device Types Supported**: Phone and Tablet
|
||||
- **Orientations Supported**: Portrait and Landscape
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/bitwarden/authenticator-android
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
3. Setup the code style formatter:
|
||||
|
||||
All code must follow the guidelines described in the [Code Style Guidelines document](docs/STYLE_AND_BEST_PRACTICES.md). To aid in adhering to these rules, all contributors should apply `docs/bitwarden-style.xml` as their code style scheme. In IntelliJ / Android Studio:
|
||||
|
||||
- Navigate to `Preferences > Editor > Code Style`.
|
||||
- Hit the `Manage` button next to `Scheme`.
|
||||
- Select `Import`.
|
||||
- Find the `bitwarden-style.xml` file in the project's `docs/` directory.
|
||||
- Import "from" `BitwardenStyle` "to" `BitwardenStyle`.
|
||||
- Hit `Apply` and `OK` to save the changes and exit Preferences.
|
||||
|
||||
Note that in some cases you may need to restart Android Studio for the changes to take effect.
|
||||
|
||||
All code should be formatted before submitting a pull request. This can be done manually but it can also be helpful to create a macro with a custom keyboard binding to auto-format when saving. In Android Studio on OS X:
|
||||
|
||||
- Select `Edit > Macros > Start Macro Recording`
|
||||
- Select `Code > Optimize Imports`
|
||||
- Select `Code > Reformat Code`
|
||||
- Select `File > Save All`
|
||||
- Select `Edit > Macros > Stop Macro Recording`
|
||||
|
||||
This can then be mapped to a set of keys by navigating to `Android Studio > Preferences` and editing the macro under `Keymap` (ex : shift + command + s).
|
||||
|
||||
Please avoid mixing formatting and logical changes in the same commit/PR. When possible, fix any large formatting issues in a separate PR before opening one to make logical changes to the same code. This helps others focus on the meaningful code changes when reviewing the code.
|
||||
|
||||
## Contribute
|
||||
|
||||
Code contributions are welcome! Please commit any pull requests against the `main` branch. Learn more about how to contribute by reading the [Contributing Guidelines](https://contributing.bitwarden.com/contributing/). Check out the [Contributing Documentation](https://contributing.bitwarden.com/) for how to get started with your first contribution.
|
||||
|
||||
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
|
||||
31
SECURITY.md
31
SECURITY.md
@@ -1,21 +1,32 @@
|
||||
Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance!
|
||||
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
|
||||
users safe. If you believe you've found a security issue in our product or service, we encourage you
|
||||
to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We
|
||||
welcome working with you to resolve the issue promptly. Thanks in advance!
|
||||
|
||||
# Disclosure Policy
|
||||
|
||||
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue.
|
||||
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
||||
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder.
|
||||
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
|
||||
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
|
||||
effort to quickly resolve the issue.
|
||||
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or
|
||||
a third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
||||
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
|
||||
degradation of our service. Only interact with accounts you own or with explicit permission of the
|
||||
account holder.
|
||||
- If you would like to encrypt your report, please use the PGP key with long ID
|
||||
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
|
||||
|
||||
While researching, we'd like to ask you to refrain from:
|
||||
|
||||
- Denial of service
|
||||
- Spamming
|
||||
- Social engineering (including phishing) of Bitwarden staff or contractors
|
||||
- Any physical attempts against Bitwarden property or data centers
|
||||
- Denial of service
|
||||
- Spamming
|
||||
- Social engineering (including phishing) of Bitwarden staff or contractors
|
||||
- Any physical attempts against Bitwarden property or data centers
|
||||
|
||||
# We want to help you!
|
||||
|
||||
If you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.
|
||||
If you have something that you feel is close to exploitation, or if you'd like some information
|
||||
regarding the internal API, or generally have any questions regarding the app that would help in
|
||||
your efforts, please email us at https://bitwarden.com/contact and ask for that information. As
|
||||
stated above, Bitwarden wants to help you find issues, and is more than willing to help.
|
||||
|
||||
Thank you for helping keep Bitwarden and our users safe!
|
||||
|
||||
@@ -325,6 +325,7 @@ kover {
|
||||
"*_*Factory\$*",
|
||||
"*.Hilt_*",
|
||||
"*_HiltModules",
|
||||
"*_HiltModules*",
|
||||
"*_HiltModules\$*",
|
||||
"*_Impl",
|
||||
"*_Impl\$*",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_USER_DICTIONARY"/>
|
||||
<!-- Protect access to AuthenticatorBridgeService using this custom permission.
|
||||
|
||||
Note that each build type uses a different value for knownCerts.
|
||||
@@ -320,6 +320,11 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent>
|
||||
<!-- To Query Chrome Beta: -->
|
||||
<package android:name="com.chrome.beta" />
|
||||
|
||||
<!-- To Query Chrome Stable: -->
|
||||
<package android:name="com.android.chrome" />
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -12,6 +12,20 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "net.quetta.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
@@ -36,6 +50,18 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.ironfoxoss.ironfox",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
@@ -59,34 +85,6 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ 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.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.network.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -19,6 +20,7 @@ import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityComp
|
||||
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.manager.util.ObserveScreenDataEffect
|
||||
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
|
||||
@@ -53,6 +55,7 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
@@ -66,13 +69,14 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
// Within the app the language will change dynamically and will be managed
|
||||
// by the OS, but we need to ensure we properly set the language when
|
||||
// upgrading from older versions that handle this differently.
|
||||
// Within the app the language and theme will change dynamically and will be managed by the
|
||||
// OS, but we need to ensure we properly set the values when upgrading from older versions
|
||||
// that handle this differently or when the activity restarts.
|
||||
settingsRepository.appLanguage.localeName?.let { localeName ->
|
||||
val localeList = LocaleListCompat.forLanguageTags(localeName)
|
||||
AppCompatDelegate.setApplicationLocales(localeList)
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
|
||||
setContent {
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
@@ -94,10 +98,29 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
.show()
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppLocale -> {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(event.localeName),
|
||||
)
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppTheme -> {
|
||||
AppCompatDelegate.setDefaultNightMode(event.osTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
LocalManagerProvider {
|
||||
ObserveScreenDataEffect(
|
||||
onDataUpdate = remember(mainViewModel) {
|
||||
{
|
||||
mainViewModel.trySendAction(
|
||||
MainAction.ResumeScreenDataReceived(it),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
BitwardenTheme(theme = state.theme) {
|
||||
RootNavScreen(
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
|
||||
@@ -13,13 +13,15 @@ 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
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CreateCredentialRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2GetCredentialsRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
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.AppResumeScreenData
|
||||
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
|
||||
@@ -71,6 +73,7 @@ class MainViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val appResumeManager: AppResumeManager,
|
||||
private val clock: Clock,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
initialState = MainState(
|
||||
@@ -108,6 +111,11 @@ class MainViewModel @Inject constructor(
|
||||
.appThemeStateFlow
|
||||
.onEach { trySendAction(MainAction.Internal.ThemeUpdate(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
settingsRepository
|
||||
.appLanguageStateFlow
|
||||
.map { MainEvent.UpdateAppLocale(it.localeName) }
|
||||
.onEach(::sendEvent)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository
|
||||
.isScreenCaptureAllowedStateFlow
|
||||
@@ -180,6 +188,14 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
|
||||
when (val data = action.screenResumeData) {
|
||||
null -> appResumeManager.clearResumeScreen()
|
||||
else -> appResumeManager.setResumeScreen(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +227,7 @@ class MainViewModel @Inject constructor(
|
||||
|
||||
private fun handleAppThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
|
||||
mutableStateFlow.update { it.copy(theme = action.theme) }
|
||||
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
|
||||
}
|
||||
|
||||
private fun handleVaultUnlockStateChange() {
|
||||
@@ -257,7 +274,7 @@ class MainViewModel @Inject constructor(
|
||||
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
|
||||
val hasVaultShortcut = intent.isMyVaultShortcut
|
||||
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
|
||||
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
|
||||
val fido2CreateCredentialRequestData = intent.getFido2CreateCredentialRequestOrNull()
|
||||
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
|
||||
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
|
||||
@@ -318,25 +335,31 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fido2CredentialRequestData != null -> {
|
||||
fido2CreateCredentialRequestData != null -> {
|
||||
// Set the user's verification status when a new FIDO 2 request is received to force
|
||||
// explicit verification if the user's vault is unlocked when the request is
|
||||
// received.
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
fido2CreateCredentialRequestData.isUserVerified
|
||||
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = fido2CredentialRequestData,
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequestData,
|
||||
)
|
||||
|
||||
// Switch accounts if the selected user is not the active user.
|
||||
if (authRepository.activeUserId != null &&
|
||||
authRepository.activeUserId != fido2CredentialRequestData.userId
|
||||
authRepository.activeUserId != fido2CreateCredentialRequestData.userId
|
||||
) {
|
||||
authRepository.switchAccount(fido2CredentialRequestData.userId)
|
||||
authRepository.switchAccount(fido2CreateCredentialRequestData.userId)
|
||||
}
|
||||
}
|
||||
|
||||
fido2CredentialAssertionRequest != null -> {
|
||||
// If device biometric verification was performed as part of single-tap
|
||||
// authentication, set the user's verification state to the device result.
|
||||
// Otherwise, retain the verification state as-is.
|
||||
fido2CredentialAssertionRequest.isUserVerified
|
||||
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Assertion(
|
||||
fido2AssertionRequest = fido2CredentialAssertionRequest,
|
||||
@@ -443,6 +466,11 @@ sealed class MainAction {
|
||||
*/
|
||||
data object OpenDebugMenu : MainAction()
|
||||
|
||||
/**
|
||||
* Receive event to save the app resume screen
|
||||
*/
|
||||
data class ResumeScreenDataReceived(val screenResumeData: AppResumeScreenData?) : MainAction()
|
||||
|
||||
/**
|
||||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
@@ -518,4 +546,18 @@ sealed class MainEvent {
|
||||
* Show a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(val message: Text) : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app language has been updated.
|
||||
*/
|
||||
data class UpdateAppLocale(
|
||||
val localeName: String?,
|
||||
) : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app theme has been updated.
|
||||
*/
|
||||
data class UpdateAppTheme(
|
||||
val osTheme: Int,
|
||||
) : MainEvent()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJso
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Primary access point for disk information.
|
||||
@@ -352,4 +353,14 @@ interface AuthDiskSource {
|
||||
* Stores the new device notice state for the given [userId].
|
||||
*/
|
||||
fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?)
|
||||
|
||||
/**
|
||||
* Gets the last lock timestamp for the given [userId].
|
||||
*/
|
||||
fun getLastLockTimestamp(userId: String): Instant?
|
||||
|
||||
/**
|
||||
* Stores the last lock timestamp for the given [userId].
|
||||
*/
|
||||
fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
// These keys should be encrypted
|
||||
@@ -50,6 +50,7 @@ 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"
|
||||
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@@ -155,6 +156,7 @@ class AuthDiskSourceImpl(
|
||||
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
|
||||
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
|
||||
storeShowImportLogins(userId = userId, showImportLogins = null)
|
||||
storeLastLockTimestamp(userId = userId, lastLockTimestamp = null)
|
||||
|
||||
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
|
||||
// indefinitely unless the TDE flow explicitly removes them.
|
||||
@@ -503,6 +505,19 @@ class AuthDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getLastLockTimestamp(userId: String): Instant? {
|
||||
return getLong(key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId))?.let {
|
||||
Instant.ofEpochMilli(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?) {
|
||||
putLong(
|
||||
key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId),
|
||||
value = lastLockTimestamp?.toEpochMilli(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
|
||||
* Represents URLs for various Bitwarden domains.
|
||||
*
|
||||
* @property base The overall base URL.
|
||||
* @property keyUri A Uri containing the alias and host of the key used for mutual TLS.
|
||||
* @property api Separate base URL for the "/api" domain (if applicable).
|
||||
* @property identity Separate base URL for the "/identity" domain (if applicable).
|
||||
* @property icon Separate base URL for the icon domain (if applicable).
|
||||
@@ -19,6 +20,9 @@ data class EnvironmentUrlDataJson(
|
||||
@SerialName("base")
|
||||
val base: String,
|
||||
|
||||
@SerialName("keyUri")
|
||||
val keyUri: String? = null,
|
||||
|
||||
@SerialName("api")
|
||||
val api: String? = null,
|
||||
|
||||
@@ -51,6 +55,7 @@ data class EnvironmentUrlDataJson(
|
||||
*/
|
||||
val DEFAULT_LEGACY_US: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
|
||||
base = "https://vault.bitwarden.com",
|
||||
keyUri = null,
|
||||
api = "https://api.bitwarden.com",
|
||||
identity = "https://identity.bitwarden.com",
|
||||
icon = "https://icons.bitwarden.net",
|
||||
@@ -71,6 +76,7 @@ data class EnvironmentUrlDataJson(
|
||||
*/
|
||||
val DEFAULT_LEGACY_EU: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
|
||||
base = "https://vault.bitwarden.eu",
|
||||
keyUri = null,
|
||||
api = "https://api.bitwarden.eu",
|
||||
identity = "https://identity.bitwarden.eu",
|
||||
icon = "https://icons.bitwarden.eu",
|
||||
|
||||
@@ -3,6 +3,7 @@ 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.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
|
||||
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
|
||||
@@ -28,4 +29,9 @@ interface UnauthenticatedAccountsApi {
|
||||
@Body body: KeyConnectorKeyRequestJson,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
): NetworkResult<Unit>
|
||||
|
||||
@POST("/accounts/resend-new-device-otp")
|
||||
suspend fun resendNewDeviceOtp(
|
||||
@Body body: ResendNewDeviceOtpRequestJson,
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -47,12 +47,13 @@ interface UnauthenticatedIdentityApi {
|
||||
@Field(value = "twoFactorProvider") twoFactorMethod: String?,
|
||||
@Field(value = "twoFactorRemember") twoFactorRemember: String?,
|
||||
@Field(value = "authRequest") authRequestId: String?,
|
||||
@Field(value = "newDeviceOtp") newDeviceOtp: String?,
|
||||
): NetworkResult<GetTokenResponseJson.Success>
|
||||
|
||||
@GET("/sso/prevalidate")
|
||||
suspend fun prevalidateSso(
|
||||
@Query("domainHint") organizationIdentifier: String,
|
||||
): NetworkResult<PrevalidateSsoResponseJson>
|
||||
): NetworkResult<PrevalidateSsoResponseJson.Success>
|
||||
|
||||
/**
|
||||
* This call needs to be synchronous so we need it to return a [Call] directly. The identity
|
||||
|
||||
@@ -21,5 +21,7 @@ enum class AuthRequestTypeJson {
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class AuthRequestTypeSerializer :
|
||||
BaseEnumeratedIntSerializer<AuthRequestTypeJson>(AuthRequestTypeJson.entries.toTypedArray())
|
||||
private class AuthRequestTypeSerializer : BaseEnumeratedIntSerializer<AuthRequestTypeJson>(
|
||||
className = "AuthRequestTypeJson",
|
||||
values = AuthRequestTypeJson.entries.toTypedArray(),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
/**
|
||||
@@ -92,41 +94,56 @@ sealed class GetTokenResponseJson {
|
||||
|
||||
/**
|
||||
* Models json body of an invalid request.
|
||||
*
|
||||
* This model supports older versions of the error response model that used lower-case keys.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@JsonNames("errorModel")
|
||||
@SerialName("ErrorModel")
|
||||
val errorModel: ErrorModel?,
|
||||
@SerialName("errorModel")
|
||||
val legacyErrorModel: LegacyErrorModel?,
|
||||
private val errorModel: ErrorModel?,
|
||||
) : GetTokenResponseJson() {
|
||||
|
||||
/**
|
||||
* The error message returned from the server, or null.
|
||||
*/
|
||||
val errorMessage: String?
|
||||
get() = errorModel?.errorMessage ?: legacyErrorModel?.errorMessage
|
||||
val errorMessage: String? get() = errorModel?.errorMessage
|
||||
|
||||
/**
|
||||
* The type of invalid responses that can be received.
|
||||
*/
|
||||
sealed class InvalidType {
|
||||
/**
|
||||
* Represents an invalid response indicating that a new device verification is required.
|
||||
*/
|
||||
data object NewDeviceVerification : InvalidType()
|
||||
|
||||
/**
|
||||
* Represents generic invalid response
|
||||
*/
|
||||
data object GenericInvalid : InvalidType()
|
||||
}
|
||||
|
||||
val invalidType: InvalidType
|
||||
get() = if (errorMessage?.lowercase() == "new device verification required") {
|
||||
InvalidType.NewDeviceVerification
|
||||
} else {
|
||||
InvalidType.GenericInvalid
|
||||
}
|
||||
|
||||
/**
|
||||
* The error body of an invalid request containing a message.
|
||||
*
|
||||
* This model supports older versions of the error response model that used lower-case
|
||||
* keys.
|
||||
*/
|
||||
@Serializable
|
||||
data class ErrorModel(
|
||||
@JsonNames("message")
|
||||
@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,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,5 +18,7 @@ enum class KdfTypeJson {
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class KdfTypeSerializer :
|
||||
BaseEnumeratedIntSerializer<KdfTypeJson>(KdfTypeJson.entries.toTypedArray())
|
||||
private class KdfTypeSerializer : BaseEnumeratedIntSerializer<KdfTypeJson>(
|
||||
className = "KdfTypeJson",
|
||||
values = KdfTypeJson.entries.toTypedArray(),
|
||||
)
|
||||
|
||||
@@ -7,6 +7,20 @@ import kotlinx.serialization.Serializable
|
||||
* Response body from the SSO prevalidate request.
|
||||
*/
|
||||
@Serializable
|
||||
data class PrevalidateSsoResponseJson(
|
||||
@SerialName("token") val token: String?,
|
||||
)
|
||||
sealed class PrevalidateSsoResponseJson {
|
||||
/**
|
||||
* Models json body of a successful response.
|
||||
*/
|
||||
@Serializable
|
||||
data class Success(
|
||||
@SerialName("token") val token: String?,
|
||||
) : PrevalidateSsoResponseJson()
|
||||
|
||||
/**
|
||||
* Models json body of an error response.
|
||||
*/
|
||||
@Serializable
|
||||
data class Error(
|
||||
@SerialName("message") val message: String?,
|
||||
) : PrevalidateSsoResponseJson()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
* Models response bodies for the register request.
|
||||
@@ -50,20 +52,24 @@ sealed class RegisterResponseJson {
|
||||
* 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)
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
@JsonNames("message")
|
||||
@SerialName("Message")
|
||||
private val invalidMessage: String? = null,
|
||||
|
||||
@SerialName("Message")
|
||||
private val errorMessage: String? = null,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: Map<String, List<String>>?,
|
||||
private val validationErrors: Map<String, List<String>>?,
|
||||
) : RegisterResponseJson() {
|
||||
/**
|
||||
* A generic error message.
|
||||
*/
|
||||
val message: String? get() = invalidMessage ?: errorMessage
|
||||
val message: String?
|
||||
get() = validationErrors
|
||||
?.values
|
||||
?.firstOrNull()
|
||||
?.firstOrNull()
|
||||
?: invalidMessage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Hold the information necessary to resend the email with the
|
||||
* new device verification code.
|
||||
*
|
||||
* @property email The user's email address.
|
||||
* @property passwordHash The master password hash
|
||||
*/
|
||||
@Serializable
|
||||
data class ResendNewDeviceOtpRequestJson(
|
||||
@SerialName("Email")
|
||||
val email: String,
|
||||
|
||||
@SerialName("MasterPasswordHash")
|
||||
val passwordHash: String?,
|
||||
)
|
||||
@@ -39,5 +39,7 @@ enum class TwoFactorAuthMethod(val value: UInt) {
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class TwoFactorAuthMethodSerializer :
|
||||
BaseEnumeratedIntSerializer<TwoFactorAuthMethod>(TwoFactorAuthMethod.entries.toTypedArray())
|
||||
private class TwoFactorAuthMethodSerializer : BaseEnumeratedIntSerializer<TwoFactorAuthMethod>(
|
||||
className = "TwoFactorAuthMethod",
|
||||
values = TwoFactorAuthMethod.entries.toTypedArray(),
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyReq
|
||||
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.ResendNewDeviceOtpRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
|
||||
@@ -52,6 +53,11 @@ interface AccountsService {
|
||||
*/
|
||||
suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Resend the email with the verification code for new devices
|
||||
*/
|
||||
suspend fun resendNewDeviceOtp(body: ResendNewDeviceOtpRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Reset the password.
|
||||
*/
|
||||
|
||||
@@ -13,11 +13,13 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMaster
|
||||
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
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
|
||||
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.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.NetworkErrorCode
|
||||
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
|
||||
@@ -72,7 +74,7 @@ class AccountsServiceImpl(
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<DeleteAccountResponseJson.Invalid>(
|
||||
code = 400,
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
@@ -103,7 +105,7 @@ class AccountsServiceImpl(
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<PasswordHintResponseJson.Error>(
|
||||
code = 429,
|
||||
code = NetworkErrorCode.TOO_MANY_REQUESTS,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
@@ -114,6 +116,11 @@ class AccountsServiceImpl(
|
||||
.resendVerificationCodeEmail(body = body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun resendNewDeviceOtp(body: ResendNewDeviceOtpRequestJson): Result<Unit> =
|
||||
unauthenticatedAccountsApi
|
||||
.resendNewDeviceOtp(body = body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
|
||||
if (body.currentPasswordHash == null) {
|
||||
authenticatedAccountsApi
|
||||
|
||||
@@ -46,6 +46,7 @@ interface IdentityService {
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel? = null,
|
||||
newDeviceOtp: String? = null,
|
||||
): Result<GetTokenResponseJson>
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@ 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.NetworkErrorCode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForNetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
@@ -34,7 +35,6 @@ class IdentityServiceImpl(
|
||||
.preLogin(PreLoginRequestJson(email = email))
|
||||
.toResult()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
|
||||
unauthenticatedIdentityApi
|
||||
.register(body)
|
||||
@@ -43,23 +43,26 @@ class IdentityServiceImpl(
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>(
|
||||
code = 400,
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(400, 429),
|
||||
codes = listOf(
|
||||
NetworkErrorCode.BAD_REQUEST,
|
||||
NetworkErrorCode.TOO_MANY_REQUESTS,
|
||||
),
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun getToken(
|
||||
uniqueAppId: String,
|
||||
email: String,
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel?,
|
||||
newDeviceOtp: String?,
|
||||
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.getToken(
|
||||
scope = "api offline_access",
|
||||
@@ -79,20 +82,25 @@ class IdentityServiceImpl(
|
||||
twoFactorRemember = twoFactorData?.remember?.let { if (it) "1" else "0 " },
|
||||
captchaResponse = captchaToken,
|
||||
authRequestId = authModel.authRequestId,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
)
|
||||
.toResult()
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
|
||||
code = 400,
|
||||
json = json,
|
||||
) ?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
|
||||
code = 400,
|
||||
json = json,
|
||||
) ?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
) ?: throw throwable
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.Invalid>(
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override suspend fun prevalidateSso(
|
||||
@@ -102,6 +110,15 @@ class IdentityServiceImpl(
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
)
|
||||
.toResult()
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<PrevalidateSsoResponseJson.Error>(
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override fun refreshTokenSynchronously(
|
||||
refreshToken: String,
|
||||
@@ -114,7 +131,6 @@ class IdentityServiceImpl(
|
||||
.executeForNetworkResult()
|
||||
.toResult()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun registerFinish(
|
||||
body: RegisterFinishRequestJson,
|
||||
): Result<RegisterResponseJson> =
|
||||
@@ -125,7 +141,10 @@ class IdentityServiceImpl(
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(400, 429),
|
||||
codes = listOf(
|
||||
NetworkErrorCode.BAD_REQUEST,
|
||||
NetworkErrorCode.TOO_MANY_REQUESTS,
|
||||
),
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
@@ -142,7 +161,7 @@ class IdentityServiceImpl(
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<SendVerificationEmailResponseJson.Invalid>(
|
||||
code = 400,
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
@@ -161,7 +180,7 @@ class IdentityServiceImpl(
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<VerifyEmailTokenResponseJson.Invalid>(
|
||||
code = 400,
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?.checkForExpiredMessage()
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.sdk.ClientAuth
|
||||
import com.bitwarden.sdk.AuthClient
|
||||
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
|
||||
@@ -17,7 +17,7 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthSdkSource] that serves as a convenience wrapper around a
|
||||
* [ClientAuth].
|
||||
* [AuthClient].
|
||||
*/
|
||||
class AuthSdkSourceImpl(
|
||||
sdkClientManager: SdkClientManager,
|
||||
|
||||
@@ -230,6 +230,19 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
organizationIdentifier: String,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
* Repeat the previous login attempt but this time with New Device OTP
|
||||
* information. Password is included if available to unlock the vault after
|
||||
* authentication. Updated access token will be reflected in [authStateFlow].
|
||||
*/
|
||||
suspend fun login(
|
||||
email: String,
|
||||
password: String?,
|
||||
newDeviceOtp: String,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
* Log out the current user.
|
||||
*/
|
||||
@@ -252,6 +265,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
suspend fun resendVerificationCodeEmail(): ResendEmailResult
|
||||
|
||||
/**
|
||||
* Resend the email with the new device verification code.
|
||||
*/
|
||||
suspend fun resendNewDeviceOtp(): ResendEmailResult
|
||||
|
||||
/**
|
||||
* Switches to the account corresponding to the given [userId] if possible.
|
||||
*/
|
||||
@@ -362,8 +380,10 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
|
||||
/**
|
||||
* Get the password strength for the given [email] and [password] combo.
|
||||
* If no value is passed for the [email] will use the active email of the current active
|
||||
* account via the [userStateFlow].
|
||||
*/
|
||||
suspend fun getPasswordStrength(email: String, password: String): PasswordStrengthResult
|
||||
suspend fun getPasswordStrength(email: String? = null, password: String): PasswordStrengthResult
|
||||
|
||||
/**
|
||||
* Validates the master password for the current logged in user.
|
||||
@@ -401,7 +421,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
/**
|
||||
* Update the value of the onboarding status for the user.
|
||||
*/
|
||||
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
|
||||
fun setOnboardingStatus(status: OnboardingStatus)
|
||||
|
||||
/**
|
||||
* Checks if a new device notice should be displayed.
|
||||
|
||||
@@ -17,11 +17,13 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
|
||||
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.PasswordHintResponseJson
|
||||
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.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
|
||||
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
|
||||
@@ -224,6 +226,11 @@ class AuthRepositoryImpl(
|
||||
*/
|
||||
private var resendEmailRequestJson: ResendEmailRequestJson? = null
|
||||
|
||||
/**
|
||||
* The information necessary to resend the verification code email for new devices.
|
||||
*/
|
||||
private var resendNewDeviceOtpRequestJson: ResendNewDeviceOtpRequestJson? = null
|
||||
|
||||
private var organizationIdentifier: String? = null
|
||||
|
||||
/**
|
||||
@@ -683,6 +690,26 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
?: LoginResult.Error(errorMessage = null)
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String?,
|
||||
newDeviceOtp: String,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = identityTokenAuthModel
|
||||
?.let {
|
||||
loginCommon(
|
||||
email = email,
|
||||
password = password,
|
||||
authModel = it,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
|
||||
deviceData = twoFactorDeviceData,
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
?: LoginResult.Error(errorMessage = null)
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
ssoCode: String,
|
||||
@@ -764,6 +791,16 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
?: ResendEmailResult.Error(message = null)
|
||||
|
||||
override suspend fun resendNewDeviceOtp(): ResendEmailResult =
|
||||
resendNewDeviceOtpRequestJson
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message) },
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(message = null)
|
||||
|
||||
override fun switchAccount(userId: String): SwitchAccountResult {
|
||||
val currentUserState = authDiskSource.userState ?: return SwitchAccountResult.NoChange
|
||||
val previousActiveUserId = currentUserState.activeUserId
|
||||
@@ -873,14 +910,7 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Invalid -> {
|
||||
RegisterResult.Error(
|
||||
errorMessage = it
|
||||
.validationErrors
|
||||
?.values
|
||||
?.firstOrNull()
|
||||
?.firstOrNull()
|
||||
?: it.message,
|
||||
)
|
||||
RegisterResult.Error(errorMessage = it.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1088,6 +1118,7 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.BiometricDecodingError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
VaultUnlockResult.GenericError,
|
||||
-> {
|
||||
@@ -1162,13 +1193,21 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
if (it.token.isNullOrBlank()) {
|
||||
PrevalidateSsoResult.Failure
|
||||
} else {
|
||||
PrevalidateSsoResult.Success(it.token)
|
||||
when (it) {
|
||||
is PrevalidateSsoResponseJson.Error -> {
|
||||
PrevalidateSsoResult.Failure(message = it.message)
|
||||
}
|
||||
|
||||
is PrevalidateSsoResponseJson.Success -> {
|
||||
if (it.token.isNullOrBlank()) {
|
||||
PrevalidateSsoResult.Failure()
|
||||
} else {
|
||||
PrevalidateSsoResult.Success(token = it.token)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { PrevalidateSsoResult.Failure },
|
||||
onFailure = { PrevalidateSsoResult.Failure() },
|
||||
)
|
||||
|
||||
override fun setSsoCallbackResult(result: SsoCallbackResult) {
|
||||
@@ -1195,12 +1234,17 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
|
||||
override suspend fun getPasswordStrength(
|
||||
email: String,
|
||||
email: String?,
|
||||
password: String,
|
||||
): PasswordStrengthResult =
|
||||
authSdkSource
|
||||
.passwordStrength(
|
||||
email = email,
|
||||
email = email
|
||||
?: userStateFlow
|
||||
.value
|
||||
?.activeAccount
|
||||
?.email
|
||||
.orEmpty(),
|
||||
password = password,
|
||||
)
|
||||
.fold(
|
||||
@@ -1333,8 +1377,13 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun setOnboardingStatus(userId: String, status: OnboardingStatus?) {
|
||||
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
|
||||
override fun setOnboardingStatus(status: OnboardingStatus) {
|
||||
activeUserId?.let { userId ->
|
||||
authDiskSource.storeOnboardingStatus(
|
||||
userId = userId,
|
||||
onboardingStatus = status,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNewDeviceNoticeState(): NewDeviceNoticeState? {
|
||||
@@ -1372,6 +1421,7 @@ class AuthRepositoryImpl(
|
||||
// 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
|
||||
@@ -1577,6 +1627,7 @@ class AuthRepositoryImpl(
|
||||
deviceData: DeviceDataModel? = null,
|
||||
orgIdentifier: String? = null,
|
||||
captchaToken: String?,
|
||||
newDeviceOtp: String? = null,
|
||||
): LoginResult = identityService
|
||||
.getToken(
|
||||
uniqueAppId = authDiskSource.uniqueAppId,
|
||||
@@ -1584,6 +1635,7 @@ class AuthRepositoryImpl(
|
||||
authModel = authModel,
|
||||
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
|
||||
captchaToken = captchaToken,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
)
|
||||
.fold(
|
||||
onFailure = { throwable ->
|
||||
@@ -1592,6 +1644,7 @@ class AuthRepositoryImpl(
|
||||
configDiskSource.serverConfig?.isOfficialBitwardenServer == false -> {
|
||||
LoginResult.UnofficialServerError
|
||||
}
|
||||
|
||||
else -> LoginResult.Error(errorMessage = null)
|
||||
}
|
||||
},
|
||||
@@ -1616,9 +1669,22 @@ class AuthRepositoryImpl(
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
|
||||
is GetTokenResponseJson.Invalid -> LoginResult.Error(
|
||||
errorMessage = loginResponse.errorMessage,
|
||||
)
|
||||
is GetTokenResponseJson.Invalid -> {
|
||||
when (loginResponse.invalidType) {
|
||||
is GetTokenResponseJson.Invalid.InvalidType.NewDeviceVerification ->
|
||||
handleLoginCommonNewDeviceVerification(
|
||||
email = email,
|
||||
authModel = authModel,
|
||||
error = loginResponse.errorMessage,
|
||||
)
|
||||
|
||||
is GetTokenResponseJson.Invalid.InvalidType.GenericInvalid -> {
|
||||
LoginResult.Error(
|
||||
errorMessage = loginResponse.errorMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1706,15 +1772,6 @@ 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
|
||||
@@ -1743,6 +1800,7 @@ class AuthRepositoryImpl(
|
||||
twoFactorResponse = null
|
||||
resendEmailRequestJson = null
|
||||
twoFactorDeviceData = null
|
||||
resendNewDeviceOtpRequestJson = null
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
settingsRepository.storeUserHasLoggedInValue(userId)
|
||||
vaultRepository.syncIfNecessary()
|
||||
@@ -1775,6 +1833,24 @@ class AuthRepositoryImpl(
|
||||
return LoginResult.TwoFactorRequired
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method that processes the
|
||||
* [GetTokenResponseJson.Invalid.InvalidType.NewDeviceVerification] when logging in.
|
||||
*/
|
||||
private fun handleLoginCommonNewDeviceVerification(
|
||||
email: String,
|
||||
authModel: IdentityTokenAuthModel,
|
||||
error: String?,
|
||||
): LoginResult {
|
||||
identityTokenAuthModel = authModel
|
||||
resendNewDeviceOtpRequestJson = ResendNewDeviceOtpRequestJson(
|
||||
email = email,
|
||||
passwordHash = authModel.password,
|
||||
)
|
||||
|
||||
return LoginResult.NewDeviceVerification(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the current user's vault with key connector data.
|
||||
*/
|
||||
|
||||
@@ -33,4 +33,9 @@ sealed class LoginResult {
|
||||
* There was an error in validating the certificate chain for the server
|
||||
*/
|
||||
data object CertificateError : LoginResult()
|
||||
|
||||
/**
|
||||
* New device verification is required
|
||||
*/
|
||||
data class NewDeviceVerification(val errorMessage: String?) : LoginResult()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
*/
|
||||
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
|
||||
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
|
||||
VaultUnlockResult.BiometricDecodingError,
|
||||
VaultUnlockResult.GenericError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
-> LoginResult.Error(errorMessage = null)
|
||||
|
||||
@@ -18,5 +18,4 @@ data class Organization(
|
||||
val shouldManageResetPassword: Boolean,
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
val role: OrganizationType,
|
||||
val shouldUsersGetPremium: Boolean,
|
||||
)
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class PrevalidateSsoResult {
|
||||
/**
|
||||
* There was an error in prevalidation.
|
||||
*/
|
||||
data object Failure : PrevalidateSsoResult()
|
||||
data class Failure(
|
||||
val message: String? = null,
|
||||
) : PrevalidateSsoResult()
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
|
||||
shouldUseKeyConnector = this.shouldUseKeyConnector,
|
||||
role = this.type,
|
||||
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
|
||||
shouldUsersGetPremium = this.shouldUsersGetPremium,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.content.Intent
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessor
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
|
||||
@@ -21,9 +23,23 @@ class BitwardenAccessibilityService : AccessibilityService() {
|
||||
@Inject
|
||||
lateinit var processor: BitwardenAccessibilityProcessor
|
||||
|
||||
@Inject
|
||||
lateinit var accessibilityEnabledManager: AccessibilityEnabledManager
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
||||
processor.processAccessibilityEvent(event = event) { rootInActiveWindow }
|
||||
}
|
||||
|
||||
override fun onInterrupt() = Unit
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
return super
|
||||
.onUnbind(intent)
|
||||
.also { accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings() }
|
||||
}
|
||||
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,10 +57,10 @@ object AccessibilityModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilityEnabledManager(
|
||||
accessibilityManager: AccessibilityManager,
|
||||
@ApplicationContext context: Context,
|
||||
): AccessibilityEnabledManager =
|
||||
AccessibilityEnabledManagerImpl(
|
||||
accessibilityManager = accessibilityManager,
|
||||
context = context,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
|
||||
@@ -10,4 +10,9 @@ interface AccessibilityEnabledManager {
|
||||
* Emits updates that track whether the accessibility autofill service is enabled..
|
||||
*/
|
||||
val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Gets the accessibility enabled state from the system settings.
|
||||
*/
|
||||
fun refreshAccessibilityEnabledFromSettings()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import android.content.Context
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -9,18 +10,20 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
* The default implementation of [AccessibilityEnabledManager].
|
||||
*/
|
||||
class AccessibilityEnabledManagerImpl(
|
||||
accessibilityManager: AccessibilityManager,
|
||||
private val context: Context,
|
||||
) : AccessibilityEnabledManager {
|
||||
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
|
||||
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(
|
||||
value = context.isAccessibilityServiceEnabled,
|
||||
)
|
||||
|
||||
init {
|
||||
accessibilityManager.addAccessibilityStateChangeListener(
|
||||
AccessibilityManager.AccessibilityStateChangeListener { isEnabled ->
|
||||
mutableIsAccessibilityEnabledStateFlow.value = isEnabled
|
||||
},
|
||||
)
|
||||
mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled
|
||||
}
|
||||
|
||||
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
|
||||
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()
|
||||
|
||||
override fun refreshAccessibilityEnabledFromSettings() {
|
||||
mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,11 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
|
||||
// 2nd = Anticipation
|
||||
possibleUrlFieldIds = listOf("url_bar_title", "mozac_browser_toolbar_url_view"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.ironfoxoss.ironfox",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
Browser(packageName = "org.mozilla.fenix", urlFieldId = "mozac_browser_toolbar_url_view"),
|
||||
// [DEPRECATED ENTRY]
|
||||
Browser(
|
||||
@@ -191,11 +196,6 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
|
||||
),
|
||||
Browser(packageName = "org.ungoogled.chromium.extensions.stable", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "org.ungoogled.chromium.stable", urlFieldId = "url_bar"),
|
||||
Browser(
|
||||
packageName = "us.spotco.fennec_dos",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
|
||||
// [Section B] Entries only present here
|
||||
// TODO: Test the compatibility of these with Autofill Framework
|
||||
|
||||
@@ -8,6 +8,9 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -23,19 +26,32 @@ import dagger.hilt.android.scopes.ActivityScoped
|
||||
@InstallIn(ActivityComponent::class)
|
||||
object ActivityAutofillModule {
|
||||
|
||||
@ActivityScoped
|
||||
@ActivityScopedManager
|
||||
@Provides
|
||||
fun provideActivityScopedChromeThirdPartyAutofillManager(
|
||||
activity: Activity,
|
||||
): ChromeThirdPartyAutofillManager = ChromeThirdPartyAutofillManagerImpl(
|
||||
context = activity.baseContext,
|
||||
)
|
||||
|
||||
@ActivityScoped
|
||||
@Provides
|
||||
fun provideAutofillActivityManager(
|
||||
@ActivityScopedManager autofillManager: AutofillManager,
|
||||
@ActivityScopedManager chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
|
||||
appStateManager: AppStateManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
|
||||
): AutofillActivityManager =
|
||||
AutofillActivityManagerImpl(
|
||||
autofillManager = autofillManager,
|
||||
chromeThirdPartyAutofillManager = chromeThirdPartyAutofillManager,
|
||||
appStateManager = appStateManager,
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
lifecycleScope = lifecycleScope,
|
||||
chromeThirdPartyAutofillEnabledManager = chromeThirdPartyAutofillEnabledManager,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,12 +15,15 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
@@ -54,6 +57,15 @@ object AutofillModule {
|
||||
fun providesAutofillEnabledManager(): AutofillEnabledManager =
|
||||
AutofillEnabledManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesChromeAutofillEnabledManager(
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): ChromeThirdPartyAutofillEnabledManager =
|
||||
ChromeThirdPartyAutofillEnabledManagerImpl(
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAutofillCompletionManager(
|
||||
|
||||
@@ -13,6 +13,8 @@ import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessorImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -44,6 +46,8 @@ object Fido2ProviderModule {
|
||||
fido2CredentialManager: Fido2CredentialManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
intentManager: IntentManager,
|
||||
biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
clock: Clock,
|
||||
): Fido2ProviderProcessor =
|
||||
Fido2ProviderProcessorImpl(
|
||||
@@ -54,6 +58,8 @@ object Fido2ProviderModule {
|
||||
fido2CredentialManager,
|
||||
intentManager,
|
||||
clock,
|
||||
biometricsEncryptionManager,
|
||||
featureFlagManager,
|
||||
dispatcherManager,
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
@@ -22,10 +23,11 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Primary implementation of [Fido2CredentialManager].
|
||||
@@ -48,41 +50,45 @@ class Fido2CredentialManagerImpl(
|
||||
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val clientData = if (fido2CreateCredentialRequest.callingAppInfo.isOriginPopulated()) {
|
||||
fido2CreateCredentialRequest
|
||||
.callingAppInfo
|
||||
val callingAppInfo = fido2CreateCredentialRequest.callingAppInfo
|
||||
val clientData = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
|
||||
ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.packageName)
|
||||
} else {
|
||||
callingAppInfo
|
||||
.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
} else {
|
||||
ClientData.DefaultWithExtraData(
|
||||
androidPackageName = fido2CreateCredentialRequest
|
||||
.callingAppInfo
|
||||
.packageName,
|
||||
)
|
||||
?: return Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_is_signed_incorrectly.asText(),
|
||||
)
|
||||
}
|
||||
val sdkOrigin = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
|
||||
val host = getOriginUrlFromAttestationOptionsOrNull(
|
||||
requestJson = fido2CreateCredentialRequest.requestJson,
|
||||
)
|
||||
?: return Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
|
||||
.asText(),
|
||||
)
|
||||
Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = callingAppInfo.packageName,
|
||||
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_signature_is_invalid
|
||||
.asText(),
|
||||
),
|
||||
host = host,
|
||||
assetLinkUrl = host,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Origin.Web(fido2CreateCredentialRequest.origin)
|
||||
}
|
||||
val assetLinkUrl = fido2CreateCredentialRequest
|
||||
.origin
|
||||
?: getOriginUrlFromAttestationOptionsOrNull(fido2CreateCredentialRequest.requestJson)
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
|
||||
val origin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = fido2CreateCredentialRequest.packageName,
|
||||
sha256CertFingerprint = fido2CreateCredentialRequest
|
||||
.callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error,
|
||||
host = assetLinkUrl.toHostOrPathOrNull()
|
||||
?: return Fido2RegisterCredentialResult.Error,
|
||||
assetLinkUrl = assetLinkUrl,
|
||||
),
|
||||
)
|
||||
return vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = origin,
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": ${fido2CreateCredentialRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
@@ -96,7 +102,11 @@ class Fido2CredentialManagerImpl(
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
|
||||
onFailure = { Fido2RegisterCredentialResult.Error },
|
||||
onFailure = {
|
||||
Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_registration_failed_due_to_an_internal_error.asText(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -115,8 +125,10 @@ class Fido2CredentialManagerImpl(
|
||||
try {
|
||||
json.decodeFromString<PasskeyAttestationOptions>(requestJson)
|
||||
} catch (e: SerializationException) {
|
||||
Timber.e(e, "Failed to decode passkey attestation options.")
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e, "Failed to decode passkey attestation options.")
|
||||
null
|
||||
}
|
||||
|
||||
@@ -126,11 +138,14 @@ class Fido2CredentialManagerImpl(
|
||||
try {
|
||||
json.decodeFromString<PasskeyAssertionOptions>(requestJson)
|
||||
} catch (e: SerializationException) {
|
||||
Timber.e(e, "Failed to decode passkey assertion options: $e")
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e, "Failed to decode passkey assertion options: $e")
|
||||
null
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun authenticateFido2Credential(
|
||||
userId: String,
|
||||
request: Fido2CredentialAssertionRequest,
|
||||
@@ -140,22 +155,44 @@ class Fido2CredentialManagerImpl(
|
||||
val clientData = request.clientDataHash
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
|
||||
val origin = callingAppInfo.origin
|
||||
?: getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
|
||||
?: return Fido2CredentialAssertionResult.Error
|
||||
val relyingPartyId = json
|
||||
.decodeFromStringOrNull<PasskeyAssertionOptions>(request.requestJson)
|
||||
?.relyingPartyId
|
||||
?: return Fido2CredentialAssertionResult.Error
|
||||
?: return Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
)
|
||||
|
||||
val validateOriginResult = validateOrigin(
|
||||
callingAppInfo = callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
val sdkOrigin = if (!request.origin.isNullOrEmpty()) {
|
||||
Origin.Web(request.origin)
|
||||
} else {
|
||||
val hostUrl = getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
|
||||
?: return Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
|
||||
.asText(),
|
||||
)
|
||||
Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = callingAppInfo.packageName,
|
||||
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_signature_is_invalid
|
||||
.asText(),
|
||||
),
|
||||
host = hostUrl,
|
||||
assetLinkUrl = hostUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return when (validateOriginResult) {
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
Fido2CredentialAssertionResult.Error
|
||||
Fido2CredentialAssertionResult.Error(validateOriginResult.messageResId.asText())
|
||||
}
|
||||
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
@@ -163,16 +200,7 @@ class Fido2CredentialManagerImpl(
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
callingAppInfo.packageName,
|
||||
callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2CredentialAssertionResult.Error,
|
||||
origin.toHostOrPathOrNull()
|
||||
?: return Fido2CredentialAssertionResult.Error,
|
||||
origin,
|
||||
),
|
||||
),
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": ${request.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
@@ -184,7 +212,13 @@ class Fido2CredentialManagerImpl(
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
|
||||
onFailure = { Fido2CredentialAssertionResult.Error },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to authenticate FIDO2 credential.")
|
||||
Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_authentication_failed_due_to_an_internal_error
|
||||
.asText(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -196,13 +230,13 @@ class Fido2CredentialManagerImpl(
|
||||
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
|
||||
getPasskeyAssertionOptionsOrNull(requestJson)
|
||||
?.relyingPartyId
|
||||
?.let { "$HTTPS$it" }
|
||||
?.prefixHttpsIfNecessaryOrNull()
|
||||
|
||||
private fun getOriginUrlFromAttestationOptionsOrNull(requestJson: String) =
|
||||
getPasskeyAttestationOptionsOrNull(requestJson)
|
||||
?.relyingParty
|
||||
?.id
|
||||
?.let { "$HTTPS$it" }
|
||||
?.prefixHttpsIfNecessaryOrNull()
|
||||
}
|
||||
|
||||
private const val MAX_AUTHENTICATION_ATTEMPTS = 5
|
||||
|
||||
@@ -20,13 +20,4 @@ interface Fido2OriginManager {
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult
|
||||
|
||||
/**
|
||||
* Returns the privileged app origin, or null if the calling app is not allowed.
|
||||
*
|
||||
* @param callingAppInfo The calling app info.
|
||||
*
|
||||
* @return The privileged app origin, or null.
|
||||
*/
|
||||
suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String?
|
||||
}
|
||||
|
||||
@@ -32,13 +32,6 @@ class Fido2OriginManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String? {
|
||||
if (!callingAppInfo.isOriginPopulated()) return null
|
||||
return callingAppInfo.getOrigin(getGoogleAllowListOrNull().orEmpty())
|
||||
?: callingAppInfo.getOrigin(getCommunityAllowListOrNull().orEmpty())
|
||||
?.takeUnless { !callingAppInfo.isOriginPopulated() }
|
||||
}
|
||||
|
||||
private suspend fun validateCallingApplicationAssetLinks(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
@@ -123,7 +116,10 @@ class Fido2OriginManagerImpl(
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Fido2ValidateOriginResult.Error.Unknown },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to validate privileged app: ${callingAppInfo.packageName}")
|
||||
Fido2ValidateOriginResult.Error.Unknown
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -138,7 +134,6 @@ class Fido2OriginManagerImpl(
|
||||
target.packageName == rpPackageName &&
|
||||
statement.relation.containsAll(
|
||||
listOf(
|
||||
"delegate_permission/common.get_login_creds",
|
||||
"delegate_permission/common.handle_all_urls",
|
||||
),
|
||||
)
|
||||
@@ -157,16 +152,4 @@ class Fido2OriginManagerImpl(
|
||||
?: false
|
||||
}
|
||||
.takeUnless { it.isEmpty() }
|
||||
|
||||
private suspend fun getGoogleAllowListOrNull(): String? =
|
||||
assetManager
|
||||
.readAsset(GOOGLE_ALLOW_LIST_FILE_NAME)
|
||||
.onFailure { Timber.e(it, "Failed to read Google allow list.") }
|
||||
.getOrNull()
|
||||
|
||||
private suspend fun getCommunityAllowListOrNull(): String? =
|
||||
assetManager
|
||||
.readAsset(COMMUNITY_ALLOW_LIST_FILE_NAME)
|
||||
.onFailure { Timber.e(it, "Failed to read Community allow list.") }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ data class Fido2CreateCredentialRequest(
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
val isUserVerified: Boolean?,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(
|
||||
|
||||
@@ -7,6 +7,19 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 credential authentication request parsed from the launching intent.
|
||||
*
|
||||
* @param userId The ID of the Bitwarden user to authenticate.
|
||||
* @param cipherId The ID of the cipher that contains the passkey to authenticate.
|
||||
* @param credentialId The ID of the credential to authenticate.
|
||||
* @param requestJson The JSON representation of the FIDO 2 request.
|
||||
* @param clientDataHash The hash of the client data.
|
||||
* @param packageName The package name of the calling app.
|
||||
* @param signingInfo The signing info of the calling app.
|
||||
* @param origin The origin of the calling app. Only populated if the calling application is a
|
||||
* privileged application. I.e., a web browser.
|
||||
* @param isUserVerified Whether the user has been verified prior to receiving this request. Only
|
||||
* populated if device biometric verification was performed. If null, the application is responsible
|
||||
* for prompting user verification when it is deemed necessary.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2CredentialAssertionRequest(
|
||||
@@ -18,6 +31,7 @@ data class Fido2CredentialAssertionRequest(
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
val isUserVerified: Boolean?,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(packageName, signingInfo, origin)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
||||
/**
|
||||
* Represents possible outcomes of a FIDO 2 credential assertion request.
|
||||
*/
|
||||
@@ -13,5 +15,5 @@ sealed class Fido2CredentialAssertionResult {
|
||||
/**
|
||||
* Indicates there was an error and the assertion was not successful.
|
||||
*/
|
||||
data object Error : Fido2CredentialAssertionResult()
|
||||
data class Error(val message: Text) : Fido2CredentialAssertionResult()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
||||
/**
|
||||
* Represents the result of a FIDO 2 Get Credentials request.
|
||||
@@ -24,5 +25,7 @@ sealed class Fido2GetCredentialsResult {
|
||||
/**
|
||||
* Indicates an error was encountered when querying for matching credentials.
|
||||
*/
|
||||
data object Error : Fido2GetCredentialsResult()
|
||||
data class Error(
|
||||
val message: Text,
|
||||
) : Fido2GetCredentialsResult()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
||||
/**
|
||||
* Models the data returned from creating a FIDO 2 credential.
|
||||
*/
|
||||
@@ -9,13 +11,13 @@ sealed class Fido2RegisterCredentialResult {
|
||||
* Indicates the credential has been successfully registered.
|
||||
*/
|
||||
data class Success(
|
||||
val registrationResponse: String,
|
||||
val responseJson: String,
|
||||
) : Fido2RegisterCredentialResult()
|
||||
|
||||
/**
|
||||
* Indicates there was an error and the credential was not registered.
|
||||
*/
|
||||
data object Error : Fido2RegisterCredentialResult()
|
||||
data class Error(val message: Text) : Fido2RegisterCredentialResult()
|
||||
|
||||
/**
|
||||
* Indicates the user cancelled the request.
|
||||
|
||||
@@ -13,5 +13,5 @@ data class PublicKeyCredentialDescriptor(
|
||||
@SerialName("id")
|
||||
val id: String,
|
||||
@SerialName("transports")
|
||||
val transports: List<String>,
|
||||
val transports: List<String>?,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.os.OutcomeReceiver
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.credentials.exceptions.ClearCredentialException
|
||||
import androidx.credentials.exceptions.ClearCredentialUnsupportedException
|
||||
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
||||
@@ -22,6 +24,7 @@ import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BiometricPromptData
|
||||
import androidx.credentials.provider.CreateEntry
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||
@@ -34,9 +37,13 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
@@ -45,6 +52,7 @@ import kotlinx.coroutines.flow.fold
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.crypto.Cipher
|
||||
|
||||
private const val CREATE_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY"
|
||||
const val GET_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY"
|
||||
@@ -54,7 +62,7 @@ const val UNLOCK_ACCOUNT_INTENT = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOU
|
||||
* The default implementation of [Fido2ProviderProcessor]. Its purpose is to handle FIDO2 related
|
||||
* processing.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
class Fido2ProviderProcessorImpl(
|
||||
private val context: Context,
|
||||
@@ -64,6 +72,8 @@ class Fido2ProviderProcessorImpl(
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
private val clock: Clock,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : Fido2ProviderProcessor {
|
||||
|
||||
@@ -94,60 +104,6 @@ class Fido2ProviderProcessorImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private fun processCreateCredentialRequest(
|
||||
request: BeginCreateCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
return when (request) {
|
||||
is BeginCreatePublicKeyCredentialRequest -> {
|
||||
handleCreatePasskeyQuery(request)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreatePasskeyQuery(
|
||||
request: BeginCreatePublicKeyCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
val requestJson = request
|
||||
.candidateQueryData
|
||||
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
|
||||
|
||||
if (requestJson.isNullOrEmpty()) return null
|
||||
|
||||
val userState = authRepository.userStateFlow.value ?: return null
|
||||
|
||||
return BeginCreateCredentialResponse.Builder()
|
||||
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
|
||||
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
|
||||
|
||||
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
|
||||
val accountName = name ?: email
|
||||
return CreateEntry
|
||||
.Builder(
|
||||
accountName = accountName,
|
||||
pendingIntent = intentManager.createFido2CreationPendingIntent(
|
||||
CREATE_PASSKEY_INTENT,
|
||||
userId,
|
||||
requestCode.getAndIncrement(),
|
||||
),
|
||||
)
|
||||
.setDescription(
|
||||
context.getString(
|
||||
R.string.your_passkey_will_be_saved_to_your_bitwarden_vault_for_x,
|
||||
accountName,
|
||||
),
|
||||
)
|
||||
// Set the last used time to "now" so the active account is the default option in the
|
||||
// system prompt.
|
||||
.setLastUsedTime(if (isActive) clock.instant() else null)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun processGetCredentialRequest(
|
||||
request: BeginGetCredentialRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
@@ -202,6 +158,78 @@ class Fido2ProviderProcessorImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun processClearCredentialStateRequest(
|
||||
request: ProviderClearCredentialStateRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<Void?, ClearCredentialException>,
|
||||
) {
|
||||
// no-op: RFU
|
||||
callback.onError(ClearCredentialUnsupportedException())
|
||||
}
|
||||
|
||||
private fun processCreateCredentialRequest(
|
||||
request: BeginCreateCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
return when (request) {
|
||||
is BeginCreatePublicKeyCredentialRequest -> {
|
||||
handleCreatePasskeyQuery(request)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreatePasskeyQuery(
|
||||
request: BeginCreatePublicKeyCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
val requestJson = request
|
||||
.candidateQueryData
|
||||
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
|
||||
|
||||
if (requestJson.isNullOrEmpty()) return null
|
||||
|
||||
val userState = authRepository.userStateFlow.value ?: return null
|
||||
|
||||
return BeginCreateCredentialResponse.Builder()
|
||||
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
|
||||
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
|
||||
|
||||
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
|
||||
val accountName = name ?: email
|
||||
val entryBuilder = CreateEntry
|
||||
.Builder(
|
||||
accountName = accountName,
|
||||
pendingIntent = intentManager.createFido2CreationPendingIntent(
|
||||
CREATE_PASSKEY_INTENT,
|
||||
userId,
|
||||
requestCode.getAndIncrement(),
|
||||
),
|
||||
)
|
||||
.setDescription(
|
||||
context.getString(
|
||||
R.string.your_passkey_will_be_saved_to_your_bitwarden_vault_for_x,
|
||||
accountName,
|
||||
),
|
||||
)
|
||||
// Set the last used time to "now" so the active account is the default option in the
|
||||
// system prompt.
|
||||
.setLastUsedTime(if (isActive) clock.instant() else null)
|
||||
.setAutoSelectAllowed(true)
|
||||
|
||||
if (isVaultUnlocked &&
|
||||
featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyCreation)
|
||||
) {
|
||||
biometricsEncryptionManager
|
||||
.getOrCreateCipher(userId)
|
||||
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
|
||||
}
|
||||
return entryBuilder.build()
|
||||
}
|
||||
|
||||
@Throws(GetCredentialUnsupportedException::class)
|
||||
private suspend fun getMatchingFido2CredentialEntries(
|
||||
userId: String,
|
||||
@@ -261,36 +289,70 @@ class Fido2ProviderProcessorImpl(
|
||||
): List<CredentialEntry> =
|
||||
this
|
||||
.map {
|
||||
PublicKeyCredentialEntry
|
||||
val publicKeyEntryBuilder = PublicKeyCredentialEntry
|
||||
.Builder(
|
||||
context = context,
|
||||
username = it.userNameForUi ?: context.getString(R.string.no_username),
|
||||
pendingIntent = intentManager
|
||||
.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
userId = userId,
|
||||
credentialId = it.credentialId.toString(),
|
||||
cipherId = it.cipherId,
|
||||
requestCode = requestCode.getAndIncrement(),
|
||||
),
|
||||
pendingIntent = intentManager.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
userId = userId,
|
||||
credentialId = it.credentialId.toString(),
|
||||
cipherId = it.cipherId,
|
||||
requestCode = requestCode.getAndIncrement(),
|
||||
),
|
||||
beginGetPublicKeyCredentialOption = option,
|
||||
)
|
||||
.setIcon(
|
||||
Icon
|
||||
.createWithResource(
|
||||
context,
|
||||
R.drawable.ic_bw_passkey,
|
||||
),
|
||||
Icon.createWithResource(
|
||||
context,
|
||||
R.drawable.ic_bw_passkey,
|
||||
),
|
||||
)
|
||||
.build()
|
||||
|
||||
if (featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)) {
|
||||
biometricsEncryptionManager
|
||||
.getOrCreateCipher(userId)
|
||||
?.let {
|
||||
publicKeyEntryBuilder
|
||||
.setBiometricPromptDataIfSupported(cipher = it)
|
||||
}
|
||||
}
|
||||
publicKeyEntryBuilder.build()
|
||||
}
|
||||
|
||||
override fun processClearCredentialStateRequest(
|
||||
request: ProviderClearCredentialStateRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<Void?, ClearCredentialException>,
|
||||
) {
|
||||
// no-op: RFU
|
||||
callback.onError(ClearCredentialUnsupportedException())
|
||||
private fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher,
|
||||
): PublicKeyCredentialEntry.Builder {
|
||||
return if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
|
||||
this
|
||||
} else {
|
||||
setBiometricPromptData(
|
||||
biometricPromptData = BiometricPromptData
|
||||
.Builder()
|
||||
.buildPromptDataWithCipher(cipher),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher,
|
||||
): CreateEntry.Builder {
|
||||
return if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
|
||||
this
|
||||
} else {
|
||||
setBiometricPromptData(
|
||||
biometricPromptData = BiometricPromptData
|
||||
.Builder()
|
||||
.buildPromptDataWithCipher(cipher),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
private fun BiometricPromptData.Builder.buildPromptDataWithCipher(
|
||||
cipher: Cipher,
|
||||
): BiometricPromptData = BiometricPromptData.Builder()
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
|
||||
* Checks if this [Intent] contains a [Fido2CreateCredentialRequest] related to an ongoing FIDO 2
|
||||
* credential creation process.
|
||||
*/
|
||||
fun Intent.getFido2CredentialRequestOrNull(): Fido2CreateCredentialRequest? {
|
||||
fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler
|
||||
@@ -39,6 +39,7 @@ fun Intent.getFido2CredentialRequestOrNull(): Fido2CreateCredentialRequest? {
|
||||
packageName = systemRequest.callingAppInfo.packageName,
|
||||
signingInfo = systemRequest.callingAppInfo.signingInfo,
|
||||
origin = systemRequest.callingAppInfo.origin,
|
||||
isUserVerified = systemRequest.biometricPromptResult?.isSuccessful,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -67,6 +68,8 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
val isUserVerified = systemRequest.biometricPromptResult?.isSuccessful
|
||||
|
||||
return Fido2CredentialAssertionRequest(
|
||||
userId = userId,
|
||||
cipherId = cipherId,
|
||||
@@ -76,6 +79,7 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
packageName = systemRequest.callingAppInfo.packageName,
|
||||
signingInfo = systemRequest.callingAppInfo.signingInfo,
|
||||
origin = systemRequest.callingAppInfo.origin,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import android.view.autofill.AutofillManager
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -11,19 +14,31 @@ import kotlinx.coroutines.flow.onEach
|
||||
*/
|
||||
class AutofillActivityManagerImpl(
|
||||
private val autofillManager: AutofillManager,
|
||||
private val autofillEnabledManager: AutofillEnabledManager,
|
||||
private val chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
appStateManager: AppStateManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
|
||||
) : AutofillActivityManager {
|
||||
private val isAutofillEnabledAndSupported: Boolean
|
||||
get() = autofillManager.isEnabled &&
|
||||
autofillManager.hasEnabledAutofillServices() &&
|
||||
autofillManager.isAutofillSupported
|
||||
|
||||
private val chromeAutofillStatus: ChromeThirdPartyAutofillStatus
|
||||
get() = ChromeThirdPartyAutofillStatus(
|
||||
stableStatusData = chromeThirdPartyAutofillManager.stableChromeAutofillStatus,
|
||||
betaChannelStatusData = chromeThirdPartyAutofillManager.betaChromeAutofillStatus,
|
||||
)
|
||||
|
||||
init {
|
||||
appStateManager
|
||||
.appForegroundStateFlow
|
||||
.onEach { autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported }
|
||||
.onEach {
|
||||
autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported
|
||||
chromeThirdPartyAutofillEnabledManager.chromeThirdPartyAutofillStatus =
|
||||
chromeAutofillStatus
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.getOrganizationPremiumStatusMap
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
@@ -25,15 +25,8 @@ class AutofillTotpManagerImpl(
|
||||
) : AutofillTotpManager {
|
||||
override suspend fun tryCopyTotpToClipboard(cipherView: CipherView) {
|
||||
if (settingsRepository.isAutoCopyTotpDisabled) return
|
||||
val organizationPremiumStatusMap = authRepository
|
||||
.userStateFlow
|
||||
.value
|
||||
?.activeAccount
|
||||
?.getOrganizationPremiumStatusMap()
|
||||
.orEmpty()
|
||||
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
|
||||
val premiumStatus = organizationPremiumStatusMap[cipherView.organizationId] ?: isPremium
|
||||
if (!premiumStatus && !cipherView.organizationUseTotp) return
|
||||
if (!isPremium && !cipherView.organizationUseTotp) return
|
||||
val totpCode = cipherView.login?.totp ?: return
|
||||
|
||||
val totpResult = vaultRepository.generateTotp(
|
||||
@@ -42,7 +35,10 @@ class AutofillTotpManagerImpl(
|
||||
)
|
||||
|
||||
if (totpResult is GenerateTotpResult.Success) {
|
||||
clipboardManager.setText(text = totpResult.code)
|
||||
clipboardManager.setText(
|
||||
text = totpResult.code,
|
||||
toastDescriptorOverride = R.string.verification_code_totp.asText(),
|
||||
)
|
||||
Toast
|
||||
.makeText(
|
||||
context.applicationContext,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manager which provides whether specific Chrome versions have third party autofill available and
|
||||
* enabled.
|
||||
*/
|
||||
interface ChromeThirdPartyAutofillEnabledManager {
|
||||
/**
|
||||
* Combined status for all concerned Chrome versions.
|
||||
*/
|
||||
var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus
|
||||
|
||||
/**
|
||||
* An observable [StateFlow] of the combined third party autofill status of all concerned
|
||||
* chrome versions.
|
||||
*/
|
||||
val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* Default implementation of [ChromeThirdPartyAutofillEnabledManager].
|
||||
*/
|
||||
class ChromeThirdPartyAutofillEnabledManagerImpl(
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : ChromeThirdPartyAutofillEnabledManager {
|
||||
override var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus = DEFAULT_STATUS
|
||||
set(value) {
|
||||
field = value
|
||||
mutableChromeThirdPartyAutofillStatusStateFlow.update {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
private val mutableChromeThirdPartyAutofillStatusStateFlow = MutableStateFlow(
|
||||
chromeThirdPartyAutofillStatus,
|
||||
)
|
||||
|
||||
override val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
|
||||
get() = mutableChromeThirdPartyAutofillStatusStateFlow
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.ChromeAutofill),
|
||||
) { data, enabled ->
|
||||
if (enabled) {
|
||||
data
|
||||
} else {
|
||||
DEFAULT_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATUS = ChromeThirdPartyAutofillStatus(
|
||||
ChromeThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
ChromeThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
|
||||
|
||||
/**
|
||||
* Manager class used to determine if a device has installed versions of Chrome (either the
|
||||
* stable release or beta channel) which support and require opt in to third party autofill.
|
||||
*/
|
||||
interface ChromeThirdPartyAutofillManager {
|
||||
|
||||
/**
|
||||
* The data representing the status of the stable chrome version
|
||||
*/
|
||||
val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
|
||||
/**
|
||||
* The data representing the status of the beta chrome version
|
||||
*/
|
||||
val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeReleaseChannel
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
private const val CONTENT_PROVIDER_NAME = ".AutofillThirdPartyModeContentProvider"
|
||||
private const val THIRD_PARTY_MODE_COLUMN = "autofill_third_party_state"
|
||||
private const val THIRD_PARTY_MODE_ACTIONS_URI_PATH = "autofill_third_party_mode"
|
||||
|
||||
/**
|
||||
* Default implementation of the [ChromeThirdPartyAutofillManager] which uses a
|
||||
* [ContentResolver] to determine if the installed Chrome packages support and enable
|
||||
* third party autofill services.
|
||||
*
|
||||
* Based off of [this blog post](https://android-developers.googleblog.com/2025/02/chrome-3p-autofill-services-update.html)
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class ChromeThirdPartyAutofillManagerImpl(
|
||||
private val context: Context,
|
||||
) : ChromeThirdPartyAutofillManager {
|
||||
override val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.STABLE)
|
||||
override val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.BETA)
|
||||
|
||||
private fun getThirdPartyAutoFillStatusForChannel(
|
||||
releaseChannel: ChromeReleaseChannel,
|
||||
): ChromeThirdPartyAutoFillData {
|
||||
val uri = Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(releaseChannel.packageName + CONTENT_PROVIDER_NAME)
|
||||
.path(THIRD_PARTY_MODE_ACTIONS_URI_PATH)
|
||||
.build()
|
||||
val cursor = context
|
||||
.contentResolver
|
||||
.query(
|
||||
/* uri = */ uri,
|
||||
/* projection = */ arrayOf(THIRD_PARTY_MODE_COLUMN),
|
||||
/* selection = */ null,
|
||||
/* selectionArgs = */ null,
|
||||
/* sortOrder = */ null,
|
||||
)
|
||||
var thirdPartyEnabled = false
|
||||
val isThirdPartyAvailable = cursor
|
||||
?.let {
|
||||
it.moveToFirst()
|
||||
val columnIndex = it.getColumnIndex(THIRD_PARTY_MODE_COLUMN)
|
||||
thirdPartyEnabled = it.getInt(columnIndex) != 0
|
||||
it.close()
|
||||
true
|
||||
}
|
||||
?: false
|
||||
return ChromeThirdPartyAutoFillData(
|
||||
isAvailable = isThirdPartyAvailable,
|
||||
isThirdPartyEnabled = thirdPartyEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model.chrome
|
||||
|
||||
private const val BETA_CHANNEL_PACKAGE = "com.chrome.beta"
|
||||
private const val CHROME_CHANNEL_PACKAGE = "com.android.chrome"
|
||||
|
||||
/**
|
||||
* Enumerated values of each version of Chrome supported for third party autofill checks.
|
||||
*
|
||||
* @property packageName the package name of the release channel for the Chrome version.
|
||||
*/
|
||||
enum class ChromeReleaseChannel(val packageName: String) {
|
||||
STABLE(CHROME_CHANNEL_PACKAGE),
|
||||
BETA(BETA_CHANNEL_PACKAGE),
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model.chrome
|
||||
|
||||
/**
|
||||
* Relevant data relating to the third party autofill status of a version of the Chrome browser app.
|
||||
*/
|
||||
data class ChromeThirdPartyAutoFillData(
|
||||
val isAvailable: Boolean,
|
||||
val isThirdPartyEnabled: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* The overall status for all relevant release channels of Chrome.
|
||||
*/
|
||||
data class ChromeThirdPartyAutofillStatus(
|
||||
val stableStatusData: ChromeThirdPartyAutoFillData,
|
||||
val betaChannelStatusData: ChromeThirdPartyAutoFillData,
|
||||
)
|
||||
@@ -6,7 +6,6 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val SERVER_CONFIGURATIONS = "serverConfigurations"
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val PRE_AUTH_URLS_KEY = "preAuthEnvironmentUrls"
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEv
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEventType
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
@@ -18,6 +19,11 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
var appLanguage: AppLanguage?
|
||||
|
||||
/**
|
||||
* Emits updates that track [AppLanguage].
|
||||
*/
|
||||
val appLanguageFlow: Flow<AppLanguage?>
|
||||
|
||||
/**
|
||||
* Has the initial autofill dialog been shown to the user.
|
||||
*/
|
||||
@@ -356,4 +362,44 @@ interface SettingsDiskSource {
|
||||
* Stores the given [count] completed create send actions for the device.
|
||||
*/
|
||||
fun storeCreateSendActionCount(count: Int?)
|
||||
|
||||
/**
|
||||
* Gets the Boolean value of if the Add Login CoachMark tour has been interacted with.
|
||||
*/
|
||||
fun getShouldShowAddLoginCoachMark(): Boolean?
|
||||
|
||||
/**
|
||||
* Stores a value for if the Add Login CoachMark tour has been interacted with
|
||||
*/
|
||||
fun storeShouldShowAddLoginCoachMark(shouldShow: Boolean?)
|
||||
|
||||
/**
|
||||
* Returns an [Flow] to observe updates to the "ShouldShowAddLoginCoachMark" value.
|
||||
*/
|
||||
fun getShouldShowAddLoginCoachMarkFlow(): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets the Boolean value of if the Generator CoachMark tour has been interacted with.
|
||||
*/
|
||||
fun getShouldShowGeneratorCoachMark(): Boolean?
|
||||
|
||||
/**
|
||||
* Stores a value for if the Generator CoachMark tour has been interacted with
|
||||
*/
|
||||
fun storeShouldShowGeneratorCoachMark(shouldShow: Boolean?)
|
||||
|
||||
/**
|
||||
* Returns an [Flow] to observe updates to the "ShouldShowGeneratorCoachMark" value.
|
||||
*/
|
||||
fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Stores the given [screenData] as the screen to resume to identified by [userId].
|
||||
*/
|
||||
fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?)
|
||||
|
||||
/**
|
||||
* Gets the screen data to resume to for the device identified by [userId] or null if no screen
|
||||
*/
|
||||
fun getAppResumeScreen(userId: String): AppResumeScreenData?
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
@@ -10,7 +11,6 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppThem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Instant
|
||||
|
||||
@@ -40,6 +40,9 @@ private const val IS_VAULT_REGISTERED_FOR_EXPORT = "isVaultRegisteredForExport"
|
||||
private const val ADD_ACTION_COUNT = "addActionCount"
|
||||
private const val COPY_ACTION_COUNT = "copyActionCount"
|
||||
private const val CREATE_ACTION_COUNT = "createActionCount"
|
||||
private const val SHOULD_SHOW_ADD_LOGIN_COACH_MARK = "shouldShowAddLoginCoachMark"
|
||||
private const val SHOULD_SHOW_GENERATOR_COACH_MARK = "shouldShowGeneratorCoachMark"
|
||||
private const val RESUME_SCREEN = "resumeScreen"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -50,6 +53,7 @@ class SettingsDiskSourceImpl(
|
||||
private val json: Json,
|
||||
) : BaseDiskSource(sharedPreferences = sharedPreferences),
|
||||
SettingsDiskSource {
|
||||
private val mutableAppLanguageFlow = bufferedMutableSharedFlow<AppLanguage?>(replay = 1)
|
||||
private val mutableAppThemeFlow = bufferedMutableSharedFlow<AppTheme>(replay = 1)
|
||||
|
||||
private val mutableLastSyncFlowMap = mutableMapOf<String, MutableSharedFlow<Instant?>>()
|
||||
@@ -78,6 +82,10 @@ class SettingsDiskSourceImpl(
|
||||
|
||||
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
@@ -94,8 +102,12 @@ class SettingsDiskSourceImpl(
|
||||
key = APP_LANGUAGE_KEY,
|
||||
value = value?.localeName,
|
||||
)
|
||||
mutableAppLanguageFlow.tryEmit(value)
|
||||
}
|
||||
|
||||
override val appLanguageFlow: Flow<AppLanguage?>
|
||||
get() = mutableAppLanguageFlow.onSubscription { emit(appLanguage) }
|
||||
|
||||
override var initialAutofillDialogShown: Boolean?
|
||||
get() = getBoolean(key = INITIAL_AUTOFILL_DIALOG_SHOWN)
|
||||
set(value) {
|
||||
@@ -175,12 +187,15 @@ class SettingsDiskSourceImpl(
|
||||
storeClearClipboardFrequencySeconds(userId = userId, frequency = null)
|
||||
removeWithPrefix(prefix = ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY.appendIdentifier(userId))
|
||||
storeVaultRegisteredForExport(userId = userId, isRegistered = null)
|
||||
storeAppResumeScreen(userId = userId, screenData = null)
|
||||
|
||||
// The following are intentionally not cleared so they can be
|
||||
// restored after logging out and back in:
|
||||
// - screen capture allowed
|
||||
// - show autofill setting badge
|
||||
// - show unlock setting badge
|
||||
// - should show add login coach mark
|
||||
// - should show generator coach mark
|
||||
}
|
||||
|
||||
override fun getAccountBiometricIntegrityValidity(
|
||||
@@ -482,6 +497,48 @@ class SettingsDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getShouldShowAddLoginCoachMark(): Boolean? =
|
||||
getBoolean(key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK)
|
||||
|
||||
override fun storeShouldShowAddLoginCoachMark(shouldShow: Boolean?) {
|
||||
putBoolean(
|
||||
key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK,
|
||||
value = shouldShow,
|
||||
)
|
||||
mutableHasSeenAddLoginCoachMarkFlow.tryEmit(shouldShow)
|
||||
}
|
||||
|
||||
override fun getShouldShowAddLoginCoachMarkFlow(): Flow<Boolean?> =
|
||||
mutableHasSeenAddLoginCoachMarkFlow.onSubscription {
|
||||
emit(getBoolean(key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK))
|
||||
}
|
||||
|
||||
override fun getShouldShowGeneratorCoachMark(): Boolean? =
|
||||
getBoolean(key = SHOULD_SHOW_GENERATOR_COACH_MARK)
|
||||
|
||||
override fun storeShouldShowGeneratorCoachMark(shouldShow: Boolean?) {
|
||||
putBoolean(
|
||||
key = SHOULD_SHOW_GENERATOR_COACH_MARK,
|
||||
value = shouldShow,
|
||||
)
|
||||
mutableHasSeenGeneratorCoachMarkFlow.tryEmit(shouldShow)
|
||||
}
|
||||
|
||||
override fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?> =
|
||||
mutableHasSeenGeneratorCoachMarkFlow.onSubscription {
|
||||
emit(getShouldShowGeneratorCoachMark())
|
||||
}
|
||||
|
||||
override fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?) {
|
||||
putString(
|
||||
key = RESUME_SCREEN.appendIdentifier(userId),
|
||||
value = screenData?.let { json.encodeToString(it) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAppResumeScreen(userId: String): AppResumeScreenData? =
|
||||
getString(RESUME_SCREEN.appendIdentifier(userId))?.let { json.decodeFromStringOrNull(it) }
|
||||
|
||||
private fun getMutableLastSyncFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Instant?> =
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.model
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* Represents a mutual TLS certificate.
|
||||
*/
|
||||
data class MutualTlsCertificate(
|
||||
val alias: String,
|
||||
val privateKey: PrivateKey,
|
||||
val certificateChain: List<X509Certificate>,
|
||||
) {
|
||||
/**
|
||||
* Leaf certificate of the chain.
|
||||
*/
|
||||
val leafCertificate: X509Certificate?
|
||||
get() = certificateChain.lastOrNull()
|
||||
|
||||
/**
|
||||
* Root certificate of the chain.
|
||||
*/
|
||||
val rootCertificate: X509Certificate?
|
||||
get() = certificateChain.firstOrNull()
|
||||
|
||||
override fun toString(): String = leafCertificate
|
||||
?.let {
|
||||
buildString {
|
||||
appendLine("Subject: ${it.subjectDN}")
|
||||
appendLine("Issuer: ${it.issuerDN}")
|
||||
appendLine("Valid From: ${it.notBefore}")
|
||||
appendLine("Valid Until: ${it.notAfter}")
|
||||
}
|
||||
}
|
||||
?: ""
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.model
|
||||
|
||||
/**
|
||||
* Location of the key data.
|
||||
*/
|
||||
enum class MutualTlsKeyHost {
|
||||
/**
|
||||
* Key is stored in the system key chain.
|
||||
*/
|
||||
KEY_CHAIN,
|
||||
|
||||
/**
|
||||
* Key is stored in a private instance of the Android Key Store.
|
||||
*/
|
||||
ANDROID_KEY_STORE,
|
||||
}
|
||||
@@ -14,6 +14,10 @@ import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventServiceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushServiceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.KeyManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -70,6 +74,17 @@ object PlatformNetworkModule {
|
||||
@Singleton
|
||||
fun providesRefreshAuthenticator(): RefreshAuthenticator = RefreshAuthenticator()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSslManager(
|
||||
keyManager: KeyManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
): SslManager =
|
||||
SslManagerImpl(
|
||||
keyManager = keyManager,
|
||||
environmentRepository = environmentRepository,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofits(
|
||||
@@ -77,6 +92,7 @@ object PlatformNetworkModule {
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
headersInterceptor: HeadersInterceptor,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
sslManager: SslManager,
|
||||
json: Json,
|
||||
): Retrofits =
|
||||
RetrofitsImpl(
|
||||
@@ -84,6 +100,7 @@ object PlatformNetworkModule {
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
headersInterceptor = headersInterceptor,
|
||||
refreshAuthenticator = refreshAuthenticator,
|
||||
sslManager = sslManager,
|
||||
json = json,
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthToke
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.HeadersInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -14,6 +15,9 @@ import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import timber.log.Timber
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
/**
|
||||
* Primary implementation of [Retrofits].
|
||||
@@ -24,6 +28,7 @@ class RetrofitsImpl(
|
||||
headersInterceptor: HeadersInterceptor,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
json: Json,
|
||||
private val sslManager: SslManager,
|
||||
) : Retrofits {
|
||||
//region Authenticated Retrofits
|
||||
|
||||
@@ -67,6 +72,10 @@ class RetrofitsImpl(
|
||||
baseClient
|
||||
.newBuilder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.setSslSocketFactory(
|
||||
sslContext = sslManager.sslContext,
|
||||
trustManagers = sslManager.trustManagers,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
@@ -93,6 +102,10 @@ class RetrofitsImpl(
|
||||
.newBuilder()
|
||||
.authenticator(refreshAuthenticator)
|
||||
.addInterceptor(authTokenInterceptor)
|
||||
.setSslSocketFactory(
|
||||
sslContext = sslManager.sslContext,
|
||||
trustManagers = sslManager.trustManagers,
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -133,9 +146,22 @@ class RetrofitsImpl(
|
||||
.newBuilder()
|
||||
.addInterceptor(baseUrlInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.setSslSocketFactory(
|
||||
sslContext = sslManager.sslContext,
|
||||
trustManagers = sslManager.trustManagers,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
private fun OkHttpClient.Builder.setSslSocketFactory(
|
||||
sslContext: SSLContext,
|
||||
trustManagers: Array<TrustManager>,
|
||||
): OkHttpClient.Builder =
|
||||
sslSocketFactory(
|
||||
sslContext.socketFactory,
|
||||
trustManagers.first() as X509TrustManager,
|
||||
)
|
||||
|
||||
//endregion Helper properties and functions
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import kotlinx.serialization.encoding.Encoder
|
||||
*/
|
||||
@Suppress("UnnecessaryAbstractClass")
|
||||
abstract class BaseEnumeratedIntSerializer<T : Enum<T>>(
|
||||
private val className: String,
|
||||
private val values: Array<T>,
|
||||
private val default: T? = null,
|
||||
) : KSerializer<T> {
|
||||
@@ -29,7 +30,7 @@ abstract class BaseEnumeratedIntSerializer<T : Enum<T>>(
|
||||
val decodedValue = decoder.decodeInt().toString()
|
||||
return values.firstOrNull { it.serialNameAnnotation?.value == decodedValue }
|
||||
?: default
|
||||
?: throw IllegalArgumentException("Unknown value $decodedValue")
|
||||
?: throw IllegalArgumentException("Unknown value $decodedValue for $className")
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: T) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
|
||||
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
|
||||
/**
|
||||
* Interface for managing SSL connections.
|
||||
*/
|
||||
interface SslManager {
|
||||
|
||||
/**
|
||||
* The SSL context to use for SSL connections.
|
||||
*/
|
||||
val sslContext: SSLContext
|
||||
|
||||
/**
|
||||
* The trust managers to use for SSL connections.
|
||||
*/
|
||||
val trustManagers: Array<TrustManager>
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
|
||||
import com.x8bit.bitwarden.data.platform.manager.KeyManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import java.net.Socket
|
||||
import java.security.KeyStore
|
||||
import java.security.Principal
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509ExtendedKeyManager
|
||||
|
||||
/**
|
||||
* Primary implementation of [SslManager].
|
||||
*/
|
||||
class SslManagerImpl(
|
||||
private val keyManager: KeyManager,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
) : SslManager {
|
||||
|
||||
/*
|
||||
This property must only be accessed from a background thread. Accessing this property from
|
||||
the main thread will result in an exception being thrown when retrieving the mutual TLS
|
||||
certificate from [KeyManager].
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
@get:WorkerThread
|
||||
internal val mutualTlsCertificate: MutualTlsCertificate?
|
||||
get() {
|
||||
val keyUri = getKeyUri()
|
||||
?: return null
|
||||
|
||||
val host = MutualTlsKeyHost
|
||||
.entries
|
||||
.find { it.name == keyUri.authority }
|
||||
?: return null
|
||||
|
||||
val alias = keyUri.path
|
||||
?.trim('/')
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?: return null
|
||||
|
||||
return keyManager.getMutualTlsCertificateChain(
|
||||
alias = alias,
|
||||
host = host,
|
||||
)
|
||||
}
|
||||
|
||||
override val trustManagers: Array<TrustManager>
|
||||
get() = TrustManagerFactory
|
||||
.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
.apply { init(null as KeyStore?) }
|
||||
.trustManagers
|
||||
|
||||
override val sslContext: SSLContext
|
||||
get() = SSLContext
|
||||
.getInstance("TLS")
|
||||
.apply {
|
||||
init(
|
||||
arrayOf(X509ExtendedKeyManagerImpl()),
|
||||
trustManagers,
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getKeyUri(): Uri? = environmentRepository
|
||||
.environment
|
||||
.environmentUrlData
|
||||
.keyUri
|
||||
?.toUri()
|
||||
|
||||
private inner class X509ExtendedKeyManagerImpl : X509ExtendedKeyManager() {
|
||||
override fun chooseClientAlias(
|
||||
keyType: Array<out String>?,
|
||||
issuers: Array<out Principal>?,
|
||||
socket: Socket?,
|
||||
): String = mutualTlsCertificate?.alias ?: ""
|
||||
|
||||
override fun getCertificateChain(
|
||||
alias: String?,
|
||||
): Array<X509Certificate>? =
|
||||
mutualTlsCertificate
|
||||
?.certificateChain
|
||||
?.toTypedArray()
|
||||
|
||||
override fun getPrivateKey(alias: String?): PrivateKey? =
|
||||
mutualTlsCertificate
|
||||
?.privateKey
|
||||
|
||||
//region Unused server side methods
|
||||
override fun getServerAliases(
|
||||
alias: String?,
|
||||
issuers: Array<out Principal>?,
|
||||
): Array<String> = arrayOf()
|
||||
|
||||
override fun getClientAliases(
|
||||
keyType: String?,
|
||||
issuers: Array<out Principal>?,
|
||||
): Array<String> = emptyArray()
|
||||
|
||||
override fun chooseServerAlias(
|
||||
alias: String?,
|
||||
issuers: Array<out Principal>?,
|
||||
socket: Socket?,
|
||||
): String = ""
|
||||
//endregion Unused server side methods
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,12 @@ import retrofit2.HttpException
|
||||
* will be attempted to be parsed.
|
||||
* @param json [Json] serializer to use.
|
||||
*/
|
||||
inline fun <reified T> BitwardenError.parseErrorBodyOrNull(codes: List<Int>, json: Json): T? =
|
||||
inline fun <reified T> BitwardenError.parseErrorBodyOrNull(
|
||||
codes: List<NetworkErrorCode>,
|
||||
json: Json,
|
||||
): T? =
|
||||
(this as? BitwardenError.Http)
|
||||
?.takeIf { codes.any { it == this.code } }
|
||||
?.takeIf { codes.any { it.code == this.code } }
|
||||
?.responseBodyString
|
||||
?.let { responseBody ->
|
||||
json.decodeFromStringOrNull(responseBody)
|
||||
@@ -28,5 +31,7 @@ inline fun <reified T> BitwardenError.parseErrorBodyOrNull(codes: List<Int>, jso
|
||||
/**
|
||||
* Helper for calling [parseErrorBodyOrNull] with a single code.
|
||||
*/
|
||||
inline fun <reified T> BitwardenError.parseErrorBodyOrNull(code: Int, json: Json): T? =
|
||||
parseErrorBodyOrNull(listOf(code), json)
|
||||
inline fun <reified T> BitwardenError.parseErrorBodyOrNull(
|
||||
code: NetworkErrorCode,
|
||||
json: Json,
|
||||
): T? = parseErrorBodyOrNull(codes = listOf(code), json = json)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user