mirror of
https://github.com/bitwarden/android.git
synced 2026-05-09 21:39:15 -05:00
Compare commits
468 Commits
test-bwa
...
qrcode/fea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ec84fbd46 | ||
|
|
605e0ef023 | ||
|
|
6d196b5214 | ||
|
|
58a2812b74 | ||
|
|
8f345234d9 | ||
|
|
0407972926 | ||
|
|
a6f4717b35 | ||
|
|
89fa10749c | ||
|
|
d65e027210 | ||
|
|
225cb24ac1 | ||
|
|
60913e1a94 | ||
|
|
f4f669683e | ||
|
|
475a82e0fb | ||
|
|
f22156389b | ||
|
|
4f09f5dae4 | ||
|
|
3934bc9ae2 | ||
|
|
72c9149d27 | ||
|
|
a040a38ce8 | ||
|
|
ef3b7730d0 | ||
|
|
ad8d8d271a | ||
|
|
4954e57007 | ||
|
|
6f50fffd17 | ||
|
|
44c5755301 | ||
|
|
b20eece3aa | ||
|
|
869a3b00a5 | ||
|
|
7f8e848c46 | ||
|
|
6c784d28eb | ||
|
|
dfcdc72499 | ||
|
|
db287ddce5 | ||
|
|
9ea85917b1 | ||
|
|
6db4165c4c | ||
|
|
18ce45e7e5 | ||
|
|
6fe9eba620 | ||
|
|
b084987758 | ||
|
|
5d0593026f | ||
|
|
47abeb7843 | ||
|
|
e2779e4edb | ||
|
|
90cc9f77c5 | ||
|
|
40760f270a | ||
|
|
8a773141a4 | ||
|
|
0c149abdd9 | ||
|
|
f540f86b19 | ||
|
|
ca64ce2176 | ||
|
|
da63c9e36b | ||
|
|
e16ad44d5e | ||
|
|
d26a2ee52a | ||
|
|
1eb741ab58 | ||
|
|
e10ca9a6ec | ||
|
|
3fca61ad3e | ||
|
|
409529b9ca | ||
|
|
4568dd53d4 | ||
|
|
b9b90165bf | ||
|
|
778a630012 | ||
|
|
4809066ad7 | ||
|
|
d03c6c243d | ||
|
|
d19ab498ff | ||
|
|
efc3d21fde | ||
|
|
35e585a60e | ||
|
|
39787f9bf0 | ||
|
|
3940997ef9 | ||
|
|
ce482e744d | ||
|
|
b0157d10e2 | ||
|
|
cf3c2fb56d | ||
|
|
a88a173e00 | ||
|
|
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 | ||
|
|
6a3a534304 | ||
|
|
09254a2285 | ||
|
|
e8e6040318 | ||
|
|
5790397a27 | ||
|
|
8b98e8f461 | ||
|
|
f3482bd3e2 | ||
|
|
14ef7351bd | ||
|
|
c5616b9e38 | ||
|
|
0538becf7f | ||
|
|
2ae3d5e100 | ||
|
|
72ac8d3699 | ||
|
|
4eb4d5f256 | ||
|
|
6ffe9df353 | ||
|
|
7b9cbb7bef | ||
|
|
3d12c98969 | ||
|
|
2bbadf8726 | ||
|
|
4575fa96c1 | ||
|
|
555d30645f | ||
|
|
80d04fdf9c | ||
|
|
4ea92223c0 | ||
|
|
f8e41caaf2 | ||
|
|
3963623407 | ||
|
|
bbb2d8dc05 | ||
|
|
db035bbd7d | ||
|
|
a11d8eff4a | ||
|
|
b233505c24 | ||
|
|
ad564f98ef | ||
|
|
d488e7d5dd | ||
|
|
2d66fb5702 | ||
|
|
f92d748de3 | ||
|
|
1316882ef4 | ||
|
|
7fdb669786 | ||
|
|
f595cee7f6 | ||
|
|
43844739a2 | ||
|
|
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.
|
||||
64
.github/ISSUE_TEMPLATE/bug-passkey.yml
vendored
Normal file
64
.github/ISSUE_TEMPLATE/bug-passkey.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Passkey Bug Report
|
||||
description: File a Passkey / FIDO2 related bug report
|
||||
labels: [ "app:password-manager", "bug-passkey" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this Passkey-related bug report!
|
||||
|
||||
Please provide as much detail as possible to help us investigate the issue.
|
||||
|
||||
- type: dropdown
|
||||
id: origin
|
||||
attributes:
|
||||
label: Origin
|
||||
description: Are you using a web browser or a native application?
|
||||
options:
|
||||
- Web (Browser)
|
||||
- Native Application (non-browser app)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: rp-id
|
||||
attributes:
|
||||
label: Web URL or App name
|
||||
description: The website domain or app name you were trying to use the Passkey with
|
||||
placeholder: "e.g. example.com or ExampleApp"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: operation-type
|
||||
attributes:
|
||||
label: Passkey Action
|
||||
description: What passkey related action(s) were you trying to perform?
|
||||
options:
|
||||
- label: Creating new passkey (Registration)
|
||||
- label: Signing in (Authentication)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: build-info
|
||||
attributes:
|
||||
label: Build Information
|
||||
description: Please retrieve the build information from the About screen by tapping the Version number field
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any additional context, steps to reproduce, error messages, or relevant information about the issue
|
||||
|
||||
- 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
|
||||
50
.github/renovate.json
vendored
50
.github/renovate.json
vendored
@@ -1,32 +1,56 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>bitwarden/renovate-config"],
|
||||
"enabledManagers": ["github-actions", "gradle", "bundler"],
|
||||
"extends": [
|
||||
"github>bitwarden/renovate-config"
|
||||
],
|
||||
"enabledManagers": [
|
||||
"github-actions",
|
||||
"gradle",
|
||||
"bundler"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "gh minor",
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
"matchManagers": [
|
||||
"github-actions"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "gradle minor",
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"matchManagers": ["gradle"]
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch"
|
||||
],
|
||||
"matchManagers": [
|
||||
"gradle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "kotlin",
|
||||
"description": "Kotlin and Compose dependencies that must be updated together to maintain compatibility.",
|
||||
"matchPackagePatterns": [
|
||||
"androidx.compose:compose-bom",
|
||||
"org.jetbrains.kotlin.*",
|
||||
"com.google.devtools.ksp"
|
||||
"matchManagers": [
|
||||
"gradle"
|
||||
],
|
||||
"matchManagers": ["gradle"]
|
||||
"matchPackageNames": [
|
||||
"/androidx.compose:compose-bom/",
|
||||
"/androidx.lifecycle:*/",
|
||||
"/org.jetbrains.kotlin.*/",
|
||||
"/com.google.devtools.ksp/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "bundler minor",
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"matchManagers": ["bundler"]
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch"
|
||||
],
|
||||
"matchManagers": [
|
||||
"bundler"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
12
.github/workflows/build-authenticator.yml
vendored
12
.github/workflows/build-authenticator.yml
vendored
@@ -1,9 +1,9 @@
|
||||
name: Build Authenticator
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
@@ -127,6 +127,12 @@ jobs:
|
||||
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:
|
||||
|
||||
@@ -2,8 +2,9 @@ name: Crowdin Sync - Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# schedule:
|
||||
# - cron: '0 0 * * 5'
|
||||
inputs: {}
|
||||
schedule:
|
||||
- cron: '0 0 * * 5'
|
||||
|
||||
jobs:
|
||||
crowdin-sync:
|
||||
|
||||
@@ -2,9 +2,9 @@ name: Crowdin Push - Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches:
|
||||
# - "main"
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 17
|
||||
|
||||
14
.github/workflows/scan-authenticator.yml
vendored
14
.github/workflows/scan-authenticator.yml
vendored
@@ -2,13 +2,13 @@ name: Scan Authenticator
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches:
|
||||
# - "main"
|
||||
# - "rc"
|
||||
# - "hotfix-rc"
|
||||
# pull_request_target:
|
||||
# types: [opened, synchronize]
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
|
||||
16
.github/workflows/test-authenticator.yml
vendored
16
.github/workflows/test-authenticator.yml
vendored
@@ -1,15 +1,13 @@
|
||||
name: Test Authenticator
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches:
|
||||
# - "main"
|
||||
# - "rc"
|
||||
# - "hotfix-rc"
|
||||
# pull_request_target:
|
||||
# types: [opened, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,5 +25,6 @@ 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
|
||||
|
||||
1
.husky/pre-commit
Executable file
1
.husky/pre-commit
Executable file
@@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
27
Gemfile.lock
27
Gemfile.lock
@@ -9,17 +9,18 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1040.0)
|
||||
aws-sdk-core (3.216.0)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1067.0)
|
||||
aws-sdk-core (3.220.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.97.0)
|
||||
aws-sdk-kms (1.99.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.178.0)
|
||||
aws-sdk-s3 (1.182.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -34,7 +35,7 @@ GEM
|
||||
highline (~> 2.0.0)
|
||||
date (3.4.1)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.5)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
@@ -69,7 +70,7 @@ GEM
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.226.0)
|
||||
fastlane (2.227.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -137,12 +138,12 @@ GEM
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
@@ -160,15 +161,17 @@ GEM
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.9.1)
|
||||
json (2.10.2)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.2.1)
|
||||
nkf (0.2.0)
|
||||
@@ -182,7 +185,7 @@ GEM
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.0)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
|
||||
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!
|
||||
|
||||
@@ -68,7 +68,7 @@ android {
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "CI_INFO",
|
||||
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}"
|
||||
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ android {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
@@ -115,7 +115,7 @@ android {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
@@ -180,7 +180,6 @@ android {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
@Suppress("UnstableApiUsage")
|
||||
testOptions {
|
||||
// Required for Robolectric
|
||||
unitTests.isIncludeAndroidResources = true
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,6 +6,11 @@ package com.x8bit.bitwarden
|
||||
const val LEGACY_ACCESSIBILITY_SERVICE_NAME: String =
|
||||
"com.x8bit.bitwarden.Accessibility.AccessibilityService"
|
||||
|
||||
/**
|
||||
* The short form legacy name for the accessibility service.
|
||||
*/
|
||||
const val LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME: String = ".Accessibility.AccessibilityService"
|
||||
|
||||
/**
|
||||
* The legacy name for the autofill service.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
@@ -15,6 +16,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
@@ -24,16 +26,20 @@ 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
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.ROOT_ROUTE
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.platform.util.appLanguage
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Primary entry point for the application.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@OmitFromCoverage
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -69,13 +75,9 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
// Within the app the language and theme will change dynamically and will be managed by the
|
||||
// Within the app the 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()
|
||||
@@ -111,7 +113,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
LocalManagerProvider {
|
||||
LocalManagerProvider(featureFlagsState = state.featureFlagsState) {
|
||||
ObserveScreenDataEffect(
|
||||
onDataUpdate = remember(mainViewModel) {
|
||||
{
|
||||
@@ -122,10 +124,19 @@ class MainActivity : AppCompatActivity() {
|
||||
},
|
||||
)
|
||||
BitwardenTheme(theme = state.theme) {
|
||||
RootNavScreen(
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
NavHost(
|
||||
navController = navController,
|
||||
)
|
||||
startDestination = ROOT_ROUTE,
|
||||
) {
|
||||
// Nothing else should end up at this top level, we just want the ability
|
||||
// to have the debug menu appear on top of the rest of the app without
|
||||
// interacting with the state-based navigation used by the RootNavScreen.
|
||||
rootNavDestination { shouldShowSplashScreen = false }
|
||||
debugMenuDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +151,31 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// When the app resumes check for any app specific language which may have been
|
||||
// set via the device settings. Similar to the theme setting in onCreate this
|
||||
// ensures we properly set the values when upgrading from older versions
|
||||
// that handle this differently or when the activity restarts.
|
||||
val appSpecificLanguage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val locales: LocaleListCompat = AppCompatDelegate.getApplicationLocales()
|
||||
if (locales.isEmpty) {
|
||||
// App is using the system language
|
||||
null
|
||||
} else {
|
||||
// App has specific language settings
|
||||
locales.get(0)?.appLanguage
|
||||
}
|
||||
} else {
|
||||
// For older versions, use what ever language is available from the repository.
|
||||
settingsRepository.appLanguage
|
||||
}
|
||||
|
||||
appSpecificLanguage?.let {
|
||||
mainViewModel.trySendAction(MainAction.AppSpecificLanguageUpdate(it))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
// In some scenarios on an emulator the Activity can leak when recreated
|
||||
|
||||
@@ -19,10 +19,12 @@ 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.FeatureFlagManager
|
||||
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.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -32,8 +34,10 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState
|
||||
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
|
||||
@@ -54,6 +58,7 @@ import java.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
private const val ANIMATION_REFRESH_DELAY = 500L
|
||||
|
||||
/**
|
||||
* A view model that helps launch actions for the [MainActivity].
|
||||
@@ -63,12 +68,13 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
@@ -79,6 +85,9 @@ class MainViewModel @Inject constructor(
|
||||
initialState = MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
|
||||
isErrorReportingDialogEnabled = featureFlagManager.getFeatureFlag(
|
||||
key = FlagKey.MobileErrorReporting,
|
||||
),
|
||||
),
|
||||
) {
|
||||
private var specialCircumstance: SpecialCircumstance?
|
||||
@@ -96,6 +105,12 @@ class MainViewModel @Inject constructor(
|
||||
.onEach { specialCircumstance = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
featureFlagManager
|
||||
.getFeatureFlagFlow(key = FlagKey.MobileErrorReporting)
|
||||
.map { MainAction.Internal.OnMobileErrorReportingReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
accessibilitySelectionManager
|
||||
.accessibilitySelectionFlow
|
||||
.map { MainAction.Internal.AccessibilitySelectionReceive(it) }
|
||||
@@ -134,8 +149,7 @@ class MainViewModel @Inject constructor(
|
||||
// Switching between account states often involves some kind of animation (ex:
|
||||
// account switcher) that we might want to give time to finish before triggering
|
||||
// a refresh.
|
||||
@Suppress("MagicNumber")
|
||||
delay(500)
|
||||
delay(ANIMATION_REFRESH_DELAY)
|
||||
trySendAction(MainAction.Internal.CurrentUserStateChange)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
@@ -147,8 +161,7 @@ class MainViewModel @Inject constructor(
|
||||
is VaultStateEvent.Locked -> {
|
||||
// Similar to account switching, triggering this action too soon can
|
||||
// interfere with animations or navigation logic, so we will delay slightly.
|
||||
@Suppress("MagicNumber")
|
||||
delay(500)
|
||||
delay(ANIMATION_REFRESH_DELAY)
|
||||
trySendAction(MainAction.Internal.VaultUnlockStateChange)
|
||||
}
|
||||
|
||||
@@ -172,6 +185,17 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
override fun handleAction(action: MainAction) {
|
||||
when (action) {
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
|
||||
is MainAction.AppSpecificLanguageUpdate -> handleAppSpecificLanguageUpdate(action)
|
||||
is MainAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: MainAction.Internal) {
|
||||
when (action) {
|
||||
is MainAction.Internal.AccessibilitySelectionReceive -> {
|
||||
handleAccessibilitySelectionReceive(action)
|
||||
@@ -185,13 +209,24 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate(action)
|
||||
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
|
||||
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
|
||||
is MainAction.Internal.OnMobileErrorReportingReceive -> {
|
||||
handleOnMobileErrorReportingReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnMobileErrorReportingReceive(
|
||||
action: MainAction.Internal.OnMobileErrorReportingReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isErrorReportingDialogEnabled = action.isErrorReportingEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAppSpecificLanguageUpdate(action: MainAction.AppSpecificLanguageUpdate) {
|
||||
settingsRepository.appLanguage = action.appLanguage
|
||||
}
|
||||
|
||||
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
|
||||
when (val data = action.screenResumeData) {
|
||||
null -> appResumeManager.clearResumeScreen()
|
||||
@@ -445,7 +480,16 @@ class MainViewModel @Inject constructor(
|
||||
data class MainState(
|
||||
val theme: AppTheme,
|
||||
val isScreenCaptureAllowed: Boolean,
|
||||
) : Parcelable
|
||||
private val isErrorReportingDialogEnabled: Boolean,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Contains all feature flags that are available to the UI.
|
||||
*/
|
||||
val featureFlagsState: FeatureFlagsState
|
||||
get() = FeatureFlagsState(
|
||||
isErrorReportingDialogEnabled = isErrorReportingDialogEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the [MainActivity].
|
||||
@@ -471,6 +515,12 @@ sealed class MainAction {
|
||||
*/
|
||||
data class ResumeScreenDataReceived(val screenResumeData: AppResumeScreenData?) : MainAction()
|
||||
|
||||
/**
|
||||
* Receive if there is an app specific locale selection made by user
|
||||
* in the device's settings.
|
||||
*/
|
||||
data class AppSpecificLanguageUpdate(val appLanguage: AppLanguage) : MainAction()
|
||||
|
||||
/**
|
||||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
@@ -483,6 +533,13 @@ sealed class MainAction {
|
||||
val cipherView: CipherView,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the Mobile Error Reporting feature flag has been updated.
|
||||
*/
|
||||
data class OnMobileErrorReportingReceive(
|
||||
val isErrorReportingEnabled: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the user has manually selected the given [cipherView] for autofill.
|
||||
*/
|
||||
|
||||
@@ -53,7 +53,7 @@ interface UnauthenticatedIdentityApi {
|
||||
@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
|
||||
|
||||
@@ -23,6 +23,15 @@ sealed class DeleteAccountResponseJson {
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: Map<String, List<String?>>?,
|
||||
) : DeleteAccountResponseJson()
|
||||
private val validationErrors: Map<String, List<String?>>?,
|
||||
) : DeleteAccountResponseJson() {
|
||||
/**
|
||||
* A human readable error message.
|
||||
*/
|
||||
val message: String?
|
||||
get() = validationErrors
|
||||
?.values
|
||||
?.firstOrNull()
|
||||
?.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,20 +94,21 @@ 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.
|
||||
@@ -131,24 +134,16 @@ sealed class GetTokenResponseJson {
|
||||
|
||||
/**
|
||||
* 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,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequest
|
||||
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
|
||||
@@ -73,7 +74,7 @@ class AccountsServiceImpl(
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<DeleteAccountResponseJson.Invalid>(
|
||||
code = 400,
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
@@ -104,7 +105,7 @@ class AccountsServiceImpl(
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<PasswordHintResponseJson.Error>(
|
||||
code = 429,
|
||||
code = NetworkErrorCode.TOO_MANY_REQUESTS,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
|
||||
@@ -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,17 +43,19 @@ 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,
|
||||
@@ -85,16 +87,20 @@ class IdentityServiceImpl(
|
||||
.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(
|
||||
@@ -104,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,
|
||||
@@ -116,7 +131,6 @@ class IdentityServiceImpl(
|
||||
.executeForNetworkResult()
|
||||
.toResult()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun registerFinish(
|
||||
body: RegisterFinishRequestJson,
|
||||
): Result<RegisterResponseJson> =
|
||||
@@ -127,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
|
||||
@@ -144,7 +161,7 @@ class IdentityServiceImpl(
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<SendVerificationEmailResponseJson.Invalid>(
|
||||
code = 400,
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
@@ -163,7 +180,7 @@ class IdentityServiceImpl(
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<VerifyEmailTokenResponseJson.Invalid>(
|
||||
code = 400,
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?.checkForExpiredMessage()
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.util.isSso
|
||||
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
@@ -51,7 +52,9 @@ class AuthRequestManagerImpl(
|
||||
override fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult> = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
when (val result = getAuthRequests()) {
|
||||
AuthRequestsResult.Error -> emit(AuthRequestsUpdatesResult.Error)
|
||||
is AuthRequestsResult.Error -> {
|
||||
emit(AuthRequestsUpdatesResult.Error(error = result.error))
|
||||
}
|
||||
|
||||
is AuthRequestsResult.Success -> {
|
||||
emit(AuthRequestsUpdatesResult.Update(authRequests = result.authRequests))
|
||||
@@ -70,9 +73,8 @@ class AuthRequestManagerImpl(
|
||||
email = email,
|
||||
authRequestType = authRequestType.toAuthRequestTypeJson(),
|
||||
)
|
||||
.getOrNull()
|
||||
?: run {
|
||||
emit(CreateAuthRequestResult.Error)
|
||||
.getOrElse {
|
||||
emit(CreateAuthRequestResult.Error(error = it))
|
||||
return@flow
|
||||
}
|
||||
var authRequest = initialResult.authRequest
|
||||
@@ -103,7 +105,7 @@ class AuthRequestManagerImpl(
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { emit(CreateAuthRequestResult.Error) },
|
||||
onFailure = { emit(CreateAuthRequestResult.Error(error = it)) },
|
||||
onSuccess = { updateAuthRequest ->
|
||||
when {
|
||||
updateAuthRequest.requestApproved -> {
|
||||
@@ -182,7 +184,7 @@ class AuthRequestManagerImpl(
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { emit(AuthRequestUpdatesResult.Error) },
|
||||
onFailure = { emit(AuthRequestUpdatesResult.Error(error = it)) },
|
||||
onSuccess = { updateAuthRequest ->
|
||||
when {
|
||||
updateAuthRequest.requestApproved -> {
|
||||
@@ -218,13 +220,18 @@ class AuthRequestManagerImpl(
|
||||
fingerprint: String,
|
||||
): Flow<AuthRequestUpdatesResult> = getAuthRequest {
|
||||
when (val authRequestsResult = getAuthRequests()) {
|
||||
AuthRequestsResult.Error -> AuthRequestUpdatesResult.Error
|
||||
is AuthRequestsResult.Error -> {
|
||||
AuthRequestUpdatesResult.Error(error = authRequestsResult.error)
|
||||
}
|
||||
|
||||
is AuthRequestsResult.Success -> {
|
||||
authRequestsResult
|
||||
.authRequests
|
||||
.firstOrNull { it.fingerprint == fingerprint }
|
||||
?.let { AuthRequestUpdatesResult.Update(it) }
|
||||
?: AuthRequestUpdatesResult.Error
|
||||
?: AuthRequestUpdatesResult.Error(
|
||||
error = IllegalStateException("Could not find the auth request."),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,30 +241,28 @@ class AuthRequestManagerImpl(
|
||||
): Flow<AuthRequestUpdatesResult> = getAuthRequest {
|
||||
authRequestsService
|
||||
.getAuthRequest(requestId)
|
||||
.map { response ->
|
||||
getFingerprintPhrase(response.publicKey).getOrNull()?.let { fingerprint ->
|
||||
AuthRequest(
|
||||
id = response.id,
|
||||
publicKey = response.publicKey,
|
||||
platform = response.platform,
|
||||
ipAddress = response.ipAddress,
|
||||
key = response.key,
|
||||
masterPasswordHash = response.masterPasswordHash,
|
||||
creationDate = response.creationDate,
|
||||
responseDate = response.responseDate,
|
||||
requestApproved = response.requestApproved ?: false,
|
||||
originUrl = response.originUrl,
|
||||
fingerprint = fingerprint,
|
||||
)
|
||||
}
|
||||
.mapCatching { response ->
|
||||
getFingerprintPhrase(response.publicKey)
|
||||
.getOrThrow()
|
||||
.let { fingerprint ->
|
||||
AuthRequest(
|
||||
id = response.id,
|
||||
publicKey = response.publicKey,
|
||||
platform = response.platform,
|
||||
ipAddress = response.ipAddress,
|
||||
key = response.key,
|
||||
masterPasswordHash = response.masterPasswordHash,
|
||||
creationDate = response.creationDate,
|
||||
responseDate = response.responseDate,
|
||||
requestApproved = response.requestApproved ?: false,
|
||||
originUrl = response.originUrl,
|
||||
fingerprint = fingerprint,
|
||||
)
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestUpdatesResult.Error },
|
||||
onSuccess = { authRequest ->
|
||||
authRequest
|
||||
?.let { AuthRequestUpdatesResult.Update(it) }
|
||||
?: AuthRequestUpdatesResult.Error
|
||||
},
|
||||
onFailure = { AuthRequestUpdatesResult.Error(error = it) },
|
||||
onSuccess = { AuthRequestUpdatesResult.Update(authRequest = it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -309,7 +314,7 @@ class AuthRequestManagerImpl(
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestsResult.Error },
|
||||
onFailure = { AuthRequestsResult.Error(error = it) },
|
||||
onSuccess = { AuthRequestsResult.Success(authRequests = it) },
|
||||
)
|
||||
|
||||
@@ -319,7 +324,7 @@ class AuthRequestManagerImpl(
|
||||
publicKey: String,
|
||||
isApproved: Boolean,
|
||||
): AuthRequestResult {
|
||||
val userId = activeUserId ?: return AuthRequestResult.Error
|
||||
val userId = activeUserId ?: return AuthRequestResult.Error(error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.getAuthRequestKey(
|
||||
publicKey = publicKey,
|
||||
@@ -350,7 +355,7 @@ class AuthRequestManagerImpl(
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestResult.Error },
|
||||
onFailure = { AuthRequestResult.Error(error = it) },
|
||||
onSuccess = { AuthRequestResult.Success(authRequest = it) },
|
||||
)
|
||||
}
|
||||
@@ -462,7 +467,7 @@ class AuthRequestManagerImpl(
|
||||
publicKey: String,
|
||||
): Result<String> {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return IllegalStateException("No active account").asFailure()
|
||||
?: return NoActiveUserException().asFailure()
|
||||
return authSdkSource.getUserFingerprint(
|
||||
email = profile.email,
|
||||
publicKey = publicKey,
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class AuthRequestResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestResult()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ sealed class AuthRequestUpdatesResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestUpdatesResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestUpdatesResult()
|
||||
|
||||
/**
|
||||
* The auth request has been declined.
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class AuthRequestsResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestsResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestsResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class AuthRequestsUpdatesResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestsUpdatesResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestsUpdatesResult()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ sealed class CreateAuthRequestResult {
|
||||
/**
|
||||
* There was a generic error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : CreateAuthRequestResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : CreateAuthRequestResult()
|
||||
|
||||
/**
|
||||
* The auth request has been declined.
|
||||
|
||||
@@ -421,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,6 +17,7 @@ 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
|
||||
@@ -98,6 +99,8 @@ import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.isSslHandShakeError
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
@@ -466,7 +469,7 @@ class AuthRepositoryImpl(
|
||||
masterPassword: String,
|
||||
): DeleteAccountResult {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return DeleteAccountResult.Error(message = null)
|
||||
?: return DeleteAccountResult.Error(message = null, error = NoActiveUserException())
|
||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
||||
return authSdkSource
|
||||
.hashPassword(
|
||||
@@ -500,18 +503,13 @@ class AuthRepositoryImpl(
|
||||
fold(
|
||||
onFailure = {
|
||||
clearPendingAccountDeletion()
|
||||
DeleteAccountResult.Error(message = null)
|
||||
DeleteAccountResult.Error(error = it, message = null)
|
||||
},
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is DeleteAccountResponseJson.Invalid -> {
|
||||
clearPendingAccountDeletion()
|
||||
DeleteAccountResult.Error(
|
||||
message = response.validationErrors
|
||||
?.values
|
||||
?.firstOrNull()
|
||||
?.firstOrNull(),
|
||||
)
|
||||
DeleteAccountResult.Error(message = response.message, error = null)
|
||||
}
|
||||
|
||||
DeleteAccountResponseJson.Success -> {
|
||||
@@ -523,8 +521,10 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
|
||||
override suspend fun createNewSsoUser(): NewSsoUserResult {
|
||||
val account = authDiskSource.userState?.activeAccount ?: return NewSsoUserResult.Failure
|
||||
val orgIdentifier = rememberedOrgIdentifier ?: return NewSsoUserResult.Failure
|
||||
val account = authDiskSource.userState?.activeAccount
|
||||
?: return NewSsoUserResult.Failure(error = NoActiveUserException())
|
||||
val orgIdentifier = rememberedOrgIdentifier
|
||||
?: return NewSsoUserResult.Failure(error = MissingPropertyException("OrgIdentifier"))
|
||||
val userId = account.profile.userId
|
||||
return organizationService
|
||||
.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
@@ -576,7 +576,7 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { NewSsoUserResult.Success },
|
||||
onFailure = { NewSsoUserResult.Failure },
|
||||
onFailure = { NewSsoUserResult.Failure(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -585,10 +585,13 @@ class AuthRepositoryImpl(
|
||||
asymmetricalKey: String,
|
||||
): LoginResult {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return LoginResult.Error(errorMessage = null)
|
||||
?: return LoginResult.Error(errorMessage = null, error = NoActiveUserException())
|
||||
val userId = profile.userId
|
||||
val privateKey = authDiskSource.getPrivateKey(userId = userId)
|
||||
?: return LoginResult.Error(errorMessage = null)
|
||||
?: return LoginResult.Error(
|
||||
errorMessage = null,
|
||||
error = MissingPropertyException("Private Key"),
|
||||
)
|
||||
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { error ->
|
||||
@@ -638,7 +641,7 @@ class AuthRepositoryImpl(
|
||||
onFailure = { throwable ->
|
||||
when {
|
||||
throwable.isSslHandShakeError() -> LoginResult.CertificateError
|
||||
else -> LoginResult.Error(errorMessage = null)
|
||||
else -> LoginResult.Error(errorMessage = null, error = throwable)
|
||||
}
|
||||
},
|
||||
onSuccess = { it },
|
||||
@@ -687,7 +690,10 @@ class AuthRepositoryImpl(
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
?: LoginResult.Error(errorMessage = null)
|
||||
?: LoginResult.Error(
|
||||
errorMessage = null,
|
||||
error = MissingPropertyException("Identity Token Auth Model"),
|
||||
)
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
@@ -707,7 +713,10 @@ class AuthRepositoryImpl(
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
?: LoginResult.Error(errorMessage = null)
|
||||
?: LoginResult.Error(
|
||||
errorMessage = null,
|
||||
error = MissingPropertyException("Identity Token Auth Model"),
|
||||
)
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
@@ -766,7 +775,7 @@ class AuthRepositoryImpl(
|
||||
override suspend fun requestOneTimePasscode(): RequestOtpResult =
|
||||
accountsService.requestOneTimePasscode()
|
||||
.fold(
|
||||
onFailure = { RequestOtpResult.Error(it.message) },
|
||||
onFailure = { RequestOtpResult.Error(message = it.message, error = it) },
|
||||
onSuccess = { RequestOtpResult.Success },
|
||||
)
|
||||
|
||||
@@ -776,7 +785,7 @@ class AuthRepositoryImpl(
|
||||
passcode = oneTimePasscode,
|
||||
)
|
||||
.fold(
|
||||
onFailure = { VerifyOtpResult.NotVerified(it.message) },
|
||||
onFailure = { VerifyOtpResult.NotVerified(errorMessage = it.message, error = it) },
|
||||
onSuccess = { VerifyOtpResult.Verified },
|
||||
)
|
||||
|
||||
@@ -784,21 +793,27 @@ class AuthRepositoryImpl(
|
||||
resendEmailRequestJson
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message) },
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(message = null)
|
||||
?: ResendEmailResult.Error(
|
||||
message = null,
|
||||
error = MissingPropertyException("Resend Email Request"),
|
||||
)
|
||||
|
||||
override suspend fun resendNewDeviceOtp(): ResendEmailResult =
|
||||
resendNewDeviceOtpRequestJson
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message) },
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(message = null)
|
||||
?: ResendEmailResult.Error(
|
||||
message = null,
|
||||
error = MissingPropertyException("Resend New Device OTP Request"),
|
||||
)
|
||||
|
||||
override fun switchAccount(userId: String): SwitchAccountResult {
|
||||
val currentUserState = authDiskSource.userState ?: return SwitchAccountResult.NoChange
|
||||
@@ -900,7 +915,10 @@ class AuthRepositoryImpl(
|
||||
is RegisterResponseJson.CaptchaRequired -> {
|
||||
it.validationErrors.captchaKeys.firstOrNull()
|
||||
?.let { key -> RegisterResult.CaptchaRequired(captchaId = key) }
|
||||
?: RegisterResult.Error(errorMessage = null)
|
||||
?: RegisterResult.Error(
|
||||
errorMessage = null,
|
||||
error = MissingPropertyException("Captcha ID"),
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Success -> {
|
||||
@@ -909,18 +927,11 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Invalid -> {
|
||||
RegisterResult.Error(
|
||||
errorMessage = it
|
||||
.validationErrors
|
||||
?.values
|
||||
?.firstOrNull()
|
||||
?.firstOrNull()
|
||||
?: it.message,
|
||||
)
|
||||
RegisterResult.Error(errorMessage = it.message, error = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { RegisterResult.Error(errorMessage = null) },
|
||||
onFailure = { RegisterResult.Error(errorMessage = null, error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -928,11 +939,15 @@ class AuthRepositoryImpl(
|
||||
return accountsService.requestPasswordHint(email).fold(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is PasswordHintResponseJson.Error -> PasswordHintResult.Error(it.errorMessage)
|
||||
is PasswordHintResponseJson.Error -> PasswordHintResult.Error(
|
||||
message = it.errorMessage,
|
||||
error = null,
|
||||
)
|
||||
|
||||
PasswordHintResponseJson.Success -> PasswordHintResult.Success
|
||||
}
|
||||
},
|
||||
onFailure = { PasswordHintResult.Error(null) },
|
||||
onFailure = { PasswordHintResult.Error(message = null, error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -940,12 +955,12 @@ class AuthRepositoryImpl(
|
||||
val activeAccount = authDiskSource
|
||||
.userState
|
||||
?.activeAccount
|
||||
?: return RemovePasswordResult.Error
|
||||
?: return RemovePasswordResult.Error(error = NoActiveUserException())
|
||||
val profile = activeAccount.profile
|
||||
val userId = profile.userId
|
||||
val userKey = authDiskSource
|
||||
.getUserKey(userId = userId)
|
||||
?: return RemovePasswordResult.Error
|
||||
?: return RemovePasswordResult.Error(error = MissingPropertyException("User Key"))
|
||||
val keyConnectorUrl = organizations
|
||||
.find {
|
||||
it.shouldUseKeyConnector &&
|
||||
@@ -953,7 +968,9 @@ class AuthRepositoryImpl(
|
||||
it.type != OrganizationType.ADMIN
|
||||
}
|
||||
?.keyConnectorUrl
|
||||
?: return RemovePasswordResult.Error
|
||||
?: return RemovePasswordResult.Error(
|
||||
error = MissingPropertyException("Key Connector URL"),
|
||||
)
|
||||
return keyConnectorManager
|
||||
.migrateExistingUserToKeyConnector(
|
||||
userId = userId,
|
||||
@@ -971,7 +988,7 @@ class AuthRepositoryImpl(
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { RemovePasswordResult.Error },
|
||||
onFailure = { RemovePasswordResult.Error(error = it) },
|
||||
onSuccess = { RemovePasswordResult.Success },
|
||||
)
|
||||
}
|
||||
@@ -984,7 +1001,7 @@ class AuthRepositoryImpl(
|
||||
val activeAccount = authDiskSource
|
||||
.userState
|
||||
?.activeAccount
|
||||
?: return ResetPasswordResult.Error
|
||||
?: return ResetPasswordResult.Error(error = NoActiveUserException())
|
||||
val currentPasswordHash = currentPassword?.let { password ->
|
||||
authSdkSource
|
||||
.hashPassword(
|
||||
@@ -994,7 +1011,7 @@ class AuthRepositoryImpl(
|
||||
purpose = HashPurpose.SERVER_AUTHORIZATION,
|
||||
)
|
||||
.fold(
|
||||
onFailure = { return ResetPasswordResult.Error },
|
||||
onFailure = { return ResetPasswordResult.Error(error = it) },
|
||||
onSuccess = { it },
|
||||
)
|
||||
}
|
||||
@@ -1040,7 +1057,7 @@ class AuthRepositoryImpl(
|
||||
// Return the success.
|
||||
ResetPasswordResult.Success
|
||||
},
|
||||
onFailure = { ResetPasswordResult.Error },
|
||||
onFailure = { ResetPasswordResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1053,7 +1070,7 @@ class AuthRepositoryImpl(
|
||||
val activeAccount = authDiskSource
|
||||
.userState
|
||||
?.activeAccount
|
||||
?: return SetPasswordResult.Error
|
||||
?: return SetPasswordResult.Error(error = NoActiveUserException())
|
||||
val userId = activeAccount.profile.userId
|
||||
|
||||
// Update the saved master password hash.
|
||||
@@ -1064,7 +1081,7 @@ class AuthRepositoryImpl(
|
||||
kdf = activeAccount.profile.toSdkParams(),
|
||||
purpose = HashPurpose.SERVER_AUTHORIZATION,
|
||||
)
|
||||
.getOrElse { return@setPassword SetPasswordResult.Error }
|
||||
.getOrElse { return@setPassword SetPasswordResult.Error(error = it) }
|
||||
|
||||
return when (activeAccount.profile.forcePasswordResetReason) {
|
||||
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION -> {
|
||||
@@ -1114,7 +1131,7 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
when (vaultRepository.unlockVaultWithMasterPassword(password)) {
|
||||
when (val result = vaultRepository.unlockVaultWithMasterPassword(password)) {
|
||||
is VaultUnlockResult.Success -> {
|
||||
enrollUserInPasswordReset(
|
||||
userId = userId,
|
||||
@@ -1123,12 +1140,9 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.BiometricDecodingError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
VaultUnlockResult.GenericError,
|
||||
-> {
|
||||
IllegalStateException("Failed to unlock vault").asFailure()
|
||||
is VaultUnlockError -> {
|
||||
(result.error ?: IllegalStateException("Failed to unlock vault"))
|
||||
.asFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1138,7 +1152,7 @@ class AuthRepositoryImpl(
|
||||
this.organizationIdentifier = null
|
||||
}
|
||||
.fold(
|
||||
onFailure = { SetPasswordResult.Error },
|
||||
onFailure = { SetPasswordResult.Error(error = it) },
|
||||
onSuccess = { SetPasswordResult.Success },
|
||||
)
|
||||
}
|
||||
@@ -1173,7 +1187,7 @@ class AuthRepositoryImpl(
|
||||
verifiedDate = it.verifiedDate,
|
||||
)
|
||||
},
|
||||
onFailure = { OrganizationDomainSsoDetailsResult.Failure },
|
||||
onFailure = { OrganizationDomainSsoDetailsResult.Failure(error = it) },
|
||||
)
|
||||
|
||||
override suspend fun getVerifiedOrganizationDomainSsoDetails(
|
||||
@@ -1188,7 +1202,7 @@ class AuthRepositoryImpl(
|
||||
verifiedOrganizationDomainSsoDetails = it.verifiedOrganizationDomainSsoDetails,
|
||||
)
|
||||
},
|
||||
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure },
|
||||
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure(error = it) },
|
||||
)
|
||||
|
||||
override suspend fun prevalidateSso(
|
||||
@@ -1199,13 +1213,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, error = null)
|
||||
}
|
||||
|
||||
is PrevalidateSsoResponseJson.Success -> {
|
||||
if (it.token.isNullOrBlank()) {
|
||||
PrevalidateSsoResult.Failure(error = MissingPropertyException("Token"))
|
||||
} else {
|
||||
PrevalidateSsoResult.Success(token = it.token)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { PrevalidateSsoResult.Failure },
|
||||
onFailure = { PrevalidateSsoResult.Failure(error = it) },
|
||||
)
|
||||
|
||||
override fun setSsoCallbackResult(result: SsoCallbackResult) {
|
||||
@@ -1219,15 +1241,15 @@ class AuthRepositoryImpl(
|
||||
deviceId = authDiskSource.uniqueAppId,
|
||||
)
|
||||
.fold(
|
||||
onFailure = { KnownDeviceResult.Error },
|
||||
onSuccess = { KnownDeviceResult.Success(it) },
|
||||
onFailure = { KnownDeviceResult.Error(error = it) },
|
||||
onSuccess = { KnownDeviceResult.Success(isKnownDevice = it) },
|
||||
)
|
||||
|
||||
override suspend fun getPasswordBreachCount(password: String): BreachCountResult =
|
||||
haveIBeenPwnedService
|
||||
.getPasswordBreachCount(password)
|
||||
.fold(
|
||||
onFailure = { BreachCountResult.Error },
|
||||
onFailure = { BreachCountResult.Error(error = it) },
|
||||
onSuccess = { BreachCountResult.Success(it) },
|
||||
)
|
||||
|
||||
@@ -1247,11 +1269,11 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { PasswordStrengthResult.Success(passwordStrength = it) },
|
||||
onFailure = { PasswordStrengthResult.Error },
|
||||
onFailure = { PasswordStrengthResult.Error(error = it) },
|
||||
)
|
||||
|
||||
override suspend fun validatePassword(password: String): ValidatePasswordResult {
|
||||
val userId = activeUserId ?: return ValidatePasswordResult.Error
|
||||
val userId = activeUserId ?: return ValidatePasswordResult.Error(NoActiveUserException())
|
||||
return authDiskSource
|
||||
.getMasterPasswordHash(userId = userId)
|
||||
?.let { masterPasswordHash ->
|
||||
@@ -1263,13 +1285,13 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { ValidatePasswordResult.Success(isValid = it) },
|
||||
onFailure = { ValidatePasswordResult.Error },
|
||||
onFailure = { ValidatePasswordResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
?: run {
|
||||
val encryptedKey = authDiskSource
|
||||
.getUserKey(userId)
|
||||
?: return ValidatePasswordResult.Error
|
||||
?: return ValidatePasswordResult.Error(MissingPropertyException("UserKey"))
|
||||
vaultSdkSource
|
||||
.validatePasswordUserKey(
|
||||
userId = userId,
|
||||
@@ -1299,10 +1321,12 @@ class AuthRepositoryImpl(
|
||||
.userState
|
||||
?.activeAccount
|
||||
?.profile
|
||||
?: return ValidatePinResult.Error
|
||||
?: return ValidatePinResult.Error(error = NoActiveUserException())
|
||||
val pinProtectedUserKey = authDiskSource
|
||||
.getPinProtectedUserKey(userId = activeAccount.userId)
|
||||
?: return ValidatePinResult.Error
|
||||
?: return ValidatePinResult.Error(
|
||||
error = MissingPropertyException("Pin Protected User Key"),
|
||||
)
|
||||
return vaultSdkSource
|
||||
.validatePin(
|
||||
userId = activeAccount.userId,
|
||||
@@ -1311,7 +1335,7 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { ValidatePinResult.Success(isValid = it) },
|
||||
onFailure = { ValidatePinResult.Error },
|
||||
onFailure = { ValidatePinResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1337,7 +1361,10 @@ class AuthRepositoryImpl(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is SendVerificationEmailResponseJson.Invalid -> {
|
||||
SendVerificationEmailResult.Error(it.message)
|
||||
SendVerificationEmailResult.Error(
|
||||
errorMessage = it.message,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
is SendVerificationEmailResponseJson.Success -> {
|
||||
@@ -1345,9 +1372,7 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
SendVerificationEmailResult.Error(null)
|
||||
},
|
||||
onFailure = { SendVerificationEmailResult.Error(errorMessage = null, error = it) },
|
||||
)
|
||||
|
||||
override suspend fun validateEmailToken(email: String, token: String): EmailTokenResult {
|
||||
@@ -1363,20 +1388,23 @@ class AuthRepositoryImpl(
|
||||
when (val json = it) {
|
||||
VerifyEmailTokenResponseJson.Valid -> EmailTokenResult.Success
|
||||
is VerifyEmailTokenResponseJson.Invalid -> {
|
||||
EmailTokenResult.Error(json.message)
|
||||
EmailTokenResult.Error(message = json.message, error = null)
|
||||
}
|
||||
|
||||
VerifyEmailTokenResponseJson.TokenExpired -> EmailTokenResult.Expired
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
EmailTokenResult.Error(message = null)
|
||||
},
|
||||
onFailure = { EmailTokenResult.Error(message = null, error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
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? {
|
||||
@@ -1638,7 +1666,10 @@ class AuthRepositoryImpl(
|
||||
LoginResult.UnofficialServerError
|
||||
}
|
||||
|
||||
else -> LoginResult.Error(errorMessage = null)
|
||||
else -> LoginResult.Error(
|
||||
errorMessage = null,
|
||||
error = throwable,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSuccess = { loginResponse ->
|
||||
@@ -1674,6 +1705,7 @@ class AuthRepositoryImpl(
|
||||
is GetTokenResponseJson.Invalid.InvalidType.GenericInvalid -> {
|
||||
LoginResult.Error(
|
||||
errorMessage = loginResponse.errorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1765,15 +1797,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
|
||||
@@ -1885,7 +1908,7 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
.fold(
|
||||
// If the request failed, we want to abort the login process
|
||||
onFailure = { VaultUnlockResult.GenericError },
|
||||
onFailure = { VaultUnlockResult.GenericError(error = it) },
|
||||
onSuccess = { it },
|
||||
)
|
||||
} else {
|
||||
@@ -1925,7 +1948,7 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
.fold(
|
||||
// If the request failed, we want to abort the login process
|
||||
onFailure = { VaultUnlockResult.GenericError },
|
||||
onFailure = { VaultUnlockResult.GenericError(error = it) },
|
||||
onSuccess = { it },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@ sealed class BreachCountResult {
|
||||
/**
|
||||
* There was an error determining if the password has been breached.
|
||||
*/
|
||||
data object Error : BreachCountResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
val message: String? = null,
|
||||
) : BreachCountResult()
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@ sealed class DeleteAccountResult {
|
||||
/**
|
||||
* There was an error deleting the account.
|
||||
*/
|
||||
data class Error(val message: String?) : DeleteAccountResult()
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable?,
|
||||
) : DeleteAccountResult()
|
||||
}
|
||||
|
||||
@@ -18,5 +18,8 @@ sealed class EmailTokenResult {
|
||||
/**
|
||||
* There was an error validating the token.
|
||||
*/
|
||||
data class Error(val message: String?) : EmailTokenResult()
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable?,
|
||||
) : EmailTokenResult()
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class KnownDeviceResult {
|
||||
/**
|
||||
* There was an error determining if this is a known device.
|
||||
*/
|
||||
data object Error : KnownDeviceResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : KnownDeviceResult()
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ sealed class LoginResult {
|
||||
/**
|
||||
* There was an error logging in.
|
||||
*/
|
||||
data class Error(val errorMessage: String?) : LoginResult()
|
||||
data class Error(
|
||||
val errorMessage: String?,
|
||||
val error: Throwable?,
|
||||
) : LoginResult()
|
||||
|
||||
/**
|
||||
* There was an error while logging into an unofficial Bitwarden server.
|
||||
|
||||
@@ -8,9 +8,12 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
* the necessary `message` if applicable.
|
||||
*/
|
||||
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
|
||||
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
|
||||
VaultUnlockResult.BiometricDecodingError,
|
||||
VaultUnlockResult.GenericError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
-> LoginResult.Error(errorMessage = null)
|
||||
is VaultUnlockResult.AuthenticationError -> {
|
||||
LoginResult.Error(errorMessage = this.message, error = this.error)
|
||||
}
|
||||
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> LoginResult.Error(errorMessage = null, error = this.error)
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class NewSsoUserResult {
|
||||
/**
|
||||
* There was an error while truing to create the new user.
|
||||
*/
|
||||
data object Failure : NewSsoUserResult()
|
||||
data class Failure(
|
||||
val error: Throwable,
|
||||
) : NewSsoUserResult()
|
||||
}
|
||||
|
||||
@@ -22,5 +22,7 @@ sealed class OrganizationDomainSsoDetailsResult {
|
||||
/**
|
||||
* The request failed.
|
||||
*/
|
||||
data object Failure : OrganizationDomainSsoDetailsResult()
|
||||
data class Failure(
|
||||
val error: Throwable,
|
||||
) : OrganizationDomainSsoDetailsResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,8 @@ sealed class PasswordHintResult {
|
||||
/**
|
||||
* There was an error.
|
||||
*/
|
||||
data class Error(val message: String?) : PasswordHintResult()
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable?,
|
||||
) : PasswordHintResult()
|
||||
}
|
||||
|
||||
@@ -16,5 +16,7 @@ sealed class PasswordStrengthResult {
|
||||
/**
|
||||
* There was an error determining the password strength.
|
||||
*/
|
||||
data object Error : PasswordStrengthResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : PasswordStrengthResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,8 @@ sealed class PrevalidateSsoResult {
|
||||
/**
|
||||
* There was an error in prevalidation.
|
||||
*/
|
||||
data object Failure : PrevalidateSsoResult()
|
||||
data class Failure(
|
||||
val message: String? = null,
|
||||
val error: Throwable?,
|
||||
) : PrevalidateSsoResult()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ sealed class RegisterResult {
|
||||
*
|
||||
* @param errorMessage a message describing the error.
|
||||
*/
|
||||
data class Error(val errorMessage: String?) : RegisterResult()
|
||||
data class Error(
|
||||
val errorMessage: String?,
|
||||
val error: Throwable?,
|
||||
) : RegisterResult()
|
||||
|
||||
/**
|
||||
* Password hash was found in a data breach.
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class RemovePasswordResult {
|
||||
/**
|
||||
* There was an error removing the password.
|
||||
*/
|
||||
data object Error : RemovePasswordResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : RemovePasswordResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,8 @@ sealed class RequestOtpResult {
|
||||
/**
|
||||
* Represents a failure to send the one-time passcode.
|
||||
*/
|
||||
data class Error(val message: String?) : RequestOtpResult()
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable,
|
||||
) : RequestOtpResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,8 @@ sealed class ResendEmailResult {
|
||||
/**
|
||||
* There was an error.
|
||||
*/
|
||||
data class Error(val message: String?) : ResendEmailResult()
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable,
|
||||
) : ResendEmailResult()
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class ResetPasswordResult {
|
||||
/**
|
||||
* There was an error resetting the password.
|
||||
*/
|
||||
data object Error : ResetPasswordResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : ResetPasswordResult()
|
||||
}
|
||||
|
||||
@@ -18,5 +18,8 @@ sealed class SendVerificationEmailResult {
|
||||
*
|
||||
* @param errorMessage a message describing the error.
|
||||
*/
|
||||
data class Error(val errorMessage: String?) : SendVerificationEmailResult()
|
||||
data class Error(
|
||||
val errorMessage: String?,
|
||||
val error: Throwable?,
|
||||
) : SendVerificationEmailResult()
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class SetPasswordResult {
|
||||
/**
|
||||
* There was an error setting the password.
|
||||
*/
|
||||
data object Error : SetPasswordResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : SetPasswordResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class UserFingerprintResult {
|
||||
/**
|
||||
* There was an error getting the user fingerprint.
|
||||
*/
|
||||
data object Error : UserFingerprintResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : UserFingerprintResult()
|
||||
}
|
||||
|
||||
@@ -15,5 +15,7 @@ sealed class ValidatePasswordResult {
|
||||
/**
|
||||
* There was an error determining if the validity of the password.
|
||||
*/
|
||||
data object Error : ValidatePasswordResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : ValidatePasswordResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class ValidatePinResult {
|
||||
/**
|
||||
* There was an error determining if the validity of the PIN.
|
||||
*/
|
||||
data object Error : ValidatePinResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : ValidatePinResult()
|
||||
}
|
||||
|
||||
@@ -18,5 +18,7 @@ sealed class VerifiedOrganizationDomainSsoDetailsResult {
|
||||
/**
|
||||
* The request failed.
|
||||
*/
|
||||
data object Failure : VerifiedOrganizationDomainSsoDetailsResult()
|
||||
data class Failure(
|
||||
val error: Throwable,
|
||||
) : VerifiedOrganizationDomainSsoDetailsResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,8 @@ sealed class VerifyOtpResult {
|
||||
/**
|
||||
* Represents a failure to verify the one-time passcode.
|
||||
*/
|
||||
data class NotVerified(val errorMessage: String?) : VerifyOtpResult()
|
||||
data class NotVerified(
|
||||
val errorMessage: String?,
|
||||
val error: Throwable,
|
||||
) : VerifyOtpResult()
|
||||
}
|
||||
|
||||
@@ -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,28 @@ 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 onCreate() {
|
||||
super.onCreate()
|
||||
accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings()
|
||||
}
|
||||
|
||||
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,20 +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 = accessibilityManager.isEnabled,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import com.x8bit.bitwarden.LEGACY_ACCESSIBILITY_SERVICE_NAME
|
||||
import com.x8bit.bitwarden.LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
|
||||
import com.x8bit.bitwarden.data.autofill.util.containsAnyTerms
|
||||
|
||||
/**
|
||||
* Helper method to determine if the [BitwardenAccessibilityService] is enabled.
|
||||
@@ -11,16 +13,25 @@ import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilitySer
|
||||
val Context.isAccessibilityServiceEnabled: Boolean
|
||||
get() {
|
||||
val appContext = this.applicationContext
|
||||
val accessibilityServiceName = appContext
|
||||
.packageName
|
||||
?.let { "$it/$LEGACY_ACCESSIBILITY_SERVICE_NAME" }
|
||||
?: return false
|
||||
val packageName = appContext.packageName
|
||||
val accessibilityServiceName = packageName?.let {
|
||||
"$it/$LEGACY_ACCESSIBILITY_SERVICE_NAME"
|
||||
}
|
||||
val shortAccessibilityServiceName = packageName.let {
|
||||
"$it/$LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME"
|
||||
}
|
||||
return Settings
|
||||
.Secure
|
||||
.getString(
|
||||
appContext.contentResolver,
|
||||
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
|
||||
)
|
||||
?.contains(accessibilityServiceName)
|
||||
?.containsAnyTerms(
|
||||
terms = listOfNotNull(
|
||||
accessibilityServiceName,
|
||||
shortAccessibilityServiceName,
|
||||
),
|
||||
ignoreCase = true,
|
||||
)
|
||||
?: false
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -134,7 +134,6 @@ class Fido2OriginManagerImpl(
|
||||
target.packageName == rpPackageName &&
|
||||
statement.relation.containsAll(
|
||||
listOf(
|
||||
"delegate_permission/common.get_login_creds",
|
||||
"delegate_permission/common.handle_all_urls",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -267,7 +267,7 @@ class Fido2ProviderProcessorImpl(
|
||||
val result = vaultRepository
|
||||
.getDecryptedFido2CredentialAutofillViews(cipherViews)
|
||||
return when (result) {
|
||||
DecryptFido2CredentialAutofillViewResult.Error -> {
|
||||
is DecryptFido2CredentialAutofillViewResult.Error -> {
|
||||
throw GetCredentialUnknownException("Error decrypting credentials.")
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
|
||||
/**
|
||||
* An enum that represents HTTP error codes that we may need to parse for specific responses.
|
||||
*/
|
||||
enum class NetworkErrorCode(
|
||||
val code: Int,
|
||||
) {
|
||||
BAD_REQUEST(code = 400),
|
||||
TOO_MANY_REQUESTS(code = 429),
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.platform.error
|
||||
|
||||
/**
|
||||
* An exception indicating that a required property was missing.
|
||||
*/
|
||||
class MissingPropertyException(
|
||||
propertyName: String,
|
||||
) : IllegalStateException("Missing the required $propertyName property")
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.error
|
||||
|
||||
/**
|
||||
* An exception indicating that there is currently no active user when one is required.
|
||||
*/
|
||||
class NoActiveUserException : IllegalStateException("No current active user!")
|
||||
@@ -158,7 +158,8 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
|
||||
get() = settingsDiskSource
|
||||
.getShouldShowAddLoginCoachMarkFlow()
|
||||
.map { it ?: true }
|
||||
.map { it != false }
|
||||
.mapFalseIfAnyLoginCiphersAvailable()
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
|
||||
) { shouldShow, featureIsEnabled ->
|
||||
@@ -171,10 +172,13 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
override val shouldShowGeneratorCoachMarkFlow: Flow<Boolean>
|
||||
get() = settingsDiskSource
|
||||
.getShouldShowGeneratorCoachMarkFlow()
|
||||
.map { it ?: true }
|
||||
.map { it != false }
|
||||
.mapFalseIfAnyLoginCiphersAvailable()
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
|
||||
) { shouldShow, featureFlagEnabled ->
|
||||
// If the feature flag is off always return true so observers know
|
||||
// the card has not been shown.
|
||||
shouldShow && featureFlagEnabled
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
@@ -294,4 +298,25 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
return settingsDiskSource.getShowAutoFillSettingBadge(userId) ?: false &&
|
||||
!autofillEnabledManager.isAutofillEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are any existing "Login" type ciphers then we'll map the current value
|
||||
* of the receiver Flow to `false`.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun Flow<Boolean>.mapFalseIfAnyLoginCiphersAvailable(): Flow<Boolean> =
|
||||
authDiskSource
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { activeUserId ->
|
||||
combine(
|
||||
flow = this,
|
||||
flow2 = vaultDiskSource.getCiphers(activeUserId),
|
||||
) { receiverCurrentValue, ciphers ->
|
||||
receiverCurrentValue && ciphers.none {
|
||||
it.login != null && it.organizationId == null
|
||||
}
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.security.KeyChain
|
||||
import android.security.KeyChainException
|
||||
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.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
@@ -24,7 +25,7 @@ class KeyManagerImpl(
|
||||
private val context: Context,
|
||||
) : KeyManager {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
override fun importMutualTlsCertificate(
|
||||
key: ByteArray,
|
||||
alias: String,
|
||||
@@ -35,28 +36,29 @@ class KeyManagerImpl(
|
||||
.inputStream()
|
||||
.use { stream ->
|
||||
try {
|
||||
KeyStore.getInstance(KEYSTORE_TYPE_PKCS12)
|
||||
KeyStore
|
||||
.getInstance(KEYSTORE_TYPE_PKCS12)
|
||||
.also { it.load(stream, password.toCharArray()) }
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.Forest.e(e, "Failed to load PKCS12 bytes")
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey(throwable = e)
|
||||
} catch (e: IOException) {
|
||||
Timber.Forest.e(e, "Format or password error while loading PKCS12 bytes")
|
||||
return when (e.cause) {
|
||||
is UnrecoverableKeyException -> {
|
||||
ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
ImportPrivateKeyResult.Error.UnrecoverableKey(throwable = e)
|
||||
}
|
||||
|
||||
else -> {
|
||||
ImportPrivateKeyResult.Error.KeyStoreOperationFailed
|
||||
ImportPrivateKeyResult.Error.KeyStoreOperationFailed(throwable = e)
|
||||
}
|
||||
}
|
||||
} catch (e: CertificateException) {
|
||||
Timber.Forest.e(e, "Unable to load certificate chain")
|
||||
return ImportPrivateKeyResult.Error.InvalidCertificateChain
|
||||
return ImportPrivateKeyResult.Error.InvalidCertificateChain(throwable = e)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Timber.Forest.e(e, "Cryptographic algorithm not supported")
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey(throwable = e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,22 +66,29 @@ class KeyManagerImpl(
|
||||
val internalAlias = pkcs12KeyStore.aliases()
|
||||
?.takeIf { it.hasMoreElements() }
|
||||
?.nextElement()
|
||||
?: return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
?: return ImportPrivateKeyResult.Error.UnsupportedKey(
|
||||
throwable = MissingPropertyException("Internal Alias"),
|
||||
)
|
||||
|
||||
// Step 3: Extract PrivateKey and X.509 certificate from the KeyStore and verify
|
||||
// certificate alias.
|
||||
val privateKey = try {
|
||||
pkcs12KeyStore.getKey(internalAlias, password.toCharArray())
|
||||
?: return ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
pkcs12KeyStore
|
||||
.getKey(internalAlias, password.toCharArray())
|
||||
?: return ImportPrivateKeyResult.Error.UnrecoverableKey(
|
||||
throwable = MissingPropertyException("Private Key"),
|
||||
)
|
||||
} catch (e: UnrecoverableKeyException) {
|
||||
Timber.Forest.e(e, "Failed to get private key")
|
||||
return ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
return ImportPrivateKeyResult.Error.UnrecoverableKey(throwable = e)
|
||||
}
|
||||
|
||||
val certChain: Array<Certificate> = pkcs12KeyStore
|
||||
.getCertificateChain(internalAlias)
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?: return ImportPrivateKeyResult.Error.InvalidCertificateChain
|
||||
?: return ImportPrivateKeyResult.Error.InvalidCertificateChain(
|
||||
throwable = MissingPropertyException("Certificate Chain"),
|
||||
)
|
||||
|
||||
// Step 4: Store the private key and X.509 certificate in the AndroidKeyStore if the alias
|
||||
// does not exists.
|
||||
@@ -92,7 +101,7 @@ class KeyManagerImpl(
|
||||
setKeyEntry(alias, privateKey, null, certChain)
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.Forest.e(e, "Failed to import key into Android KeyStore")
|
||||
return ImportPrivateKeyResult.Error.KeyStoreOperationFailed
|
||||
return ImportPrivateKeyResult.Error.KeyStoreOperationFailed(throwable = e)
|
||||
}
|
||||
}
|
||||
return ImportPrivateKeyResult.Success(alias)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
/**
|
||||
* Manager for loading native libraries.
|
||||
*/
|
||||
interface NativeLibraryManager {
|
||||
|
||||
/**
|
||||
* Loads a native library with the given [libraryName].
|
||||
*/
|
||||
fun loadLibrary(libraryName: String): Result<Unit>
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Primary implementation of [NativeLibraryManager].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class NativeLibraryManagerImpl : NativeLibraryManager {
|
||||
override fun loadLibrary(libraryName: String): Result<Unit> {
|
||||
return try {
|
||||
System.loadLibrary(libraryName)
|
||||
Result.success(Unit)
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
Timber.e(e, "Failed to load native library $libraryName.")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,6 @@ class PolicyManagerImpl(
|
||||
.getOrganizations(userId)
|
||||
?.filter {
|
||||
it.shouldUsePolicies &&
|
||||
it.isEnabled &&
|
||||
it.status >= OrganizationStatusType.ACCEPTED &&
|
||||
!isOrganizationExemptFromPolicies(it, type)
|
||||
}
|
||||
@@ -93,13 +92,21 @@ class PolicyManagerImpl(
|
||||
organization: SyncResponseJson.Profile.Organization,
|
||||
policyType: PolicyTypeJson,
|
||||
): Boolean =
|
||||
if (policyType == PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT) {
|
||||
organization.type == OrganizationType.OWNER
|
||||
} else if (policyType == PolicyTypeJson.PASSWORD_GENERATOR) {
|
||||
false
|
||||
} else {
|
||||
(organization.type == OrganizationType.OWNER ||
|
||||
organization.type == OrganizationType.ADMIN) ||
|
||||
organization.permissions.shouldManagePolicies
|
||||
when (policyType) {
|
||||
PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT -> {
|
||||
organization.type == OrganizationType.OWNER
|
||||
}
|
||||
|
||||
PolicyTypeJson.PASSWORD_GENERATOR,
|
||||
PolicyTypeJson.REMOVE_UNLOCK_WITH_PIN,
|
||||
-> {
|
||||
false
|
||||
}
|
||||
|
||||
else -> {
|
||||
(organization.type == OrganizationType.OWNER ||
|
||||
organization.type == OrganizationType.ADMIN) ||
|
||||
organization.permissions.shouldManagePolicies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.os.Build
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
|
||||
/**
|
||||
* Primary implementation of [SdkClientManager].
|
||||
*/
|
||||
class SdkClientManagerImpl(
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
nativeLibraryManager: NativeLibraryManager,
|
||||
private val clientProvider: suspend () -> Client = {
|
||||
Client(settings = null).apply {
|
||||
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
|
||||
@@ -15,6 +18,15 @@ class SdkClientManagerImpl(
|
||||
) : SdkClientManager {
|
||||
private val userIdToClientMap = mutableMapOf<String?, Client>()
|
||||
|
||||
init {
|
||||
// The SDK requires access to Android APIs that were not made public until API 31. In order
|
||||
// to work around this limitation the SDK must be manually loaded prior to initializing any
|
||||
// [Client] instance.
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.S)) {
|
||||
nativeLibraryManager.loadLibrary("bitwarden_uniffi")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateClient(
|
||||
userId: String?,
|
||||
): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider() }
|
||||
|
||||
@@ -34,6 +34,8 @@ import com.x8bit.bitwarden.data.platform.manager.KeyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.KeyManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
@@ -193,12 +195,18 @@ object PlatformManagerModule {
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNativeLibraryManager(): NativeLibraryManager = NativeLibraryManagerImpl()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSdkClientManager(
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
nativeLibraryManager: NativeLibraryManager,
|
||||
): SdkClientManager = SdkClientManagerImpl(
|
||||
featureFlagManager = featureFlagManager,
|
||||
nativeLibraryManager = nativeLibraryManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -31,7 +31,6 @@ sealed class FlagKey<out T : Any> {
|
||||
OnboardingFlow,
|
||||
OnboardingCarousel,
|
||||
ImportLoginsFlow,
|
||||
SshKeyCipherItems,
|
||||
VerifiedSsoDomainEndpoint,
|
||||
CredentialExchangeProtocolImport,
|
||||
CredentialExchangeProtocolExport,
|
||||
@@ -44,12 +43,14 @@ sealed class FlagKey<out T : Any> {
|
||||
SingleTapPasskeyAuthentication,
|
||||
AnonAddySelfHostAlias,
|
||||
SimpleLoginSelfHostAlias,
|
||||
ChromeAutofill,
|
||||
MobileErrorReporting,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for syncing with the Bitwarden Authenticator app.
|
||||
* Data object holding the key for syncing with the Bitwarden Authenticator app.
|
||||
*/
|
||||
data object AuthenticatorSync : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-pm-bwa-sync"
|
||||
@@ -66,6 +67,15 @@ sealed class FlagKey<out T : Any> {
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for syncing with the Bitwarden Authenticator app.
|
||||
*/
|
||||
data object MobileErrorReporting : FlagKey<Boolean>() {
|
||||
override val keyName: String = "mobile-error-reporting"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Onboarding Carousel feature.
|
||||
*/
|
||||
@@ -93,15 +103,6 @@ sealed class FlagKey<out T : Any> {
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the SSH key cipher items feature.
|
||||
*/
|
||||
data object SshKeyCipherItems : FlagKey<Boolean>() {
|
||||
override val keyName: String = "ssh-key-vault-item"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the new verified SSO domain endpoint feature.
|
||||
*/
|
||||
@@ -145,7 +146,7 @@ sealed class FlagKey<out T : Any> {
|
||||
*/
|
||||
data object CipherKeyEncryption : FlagKey<Boolean>() {
|
||||
override val keyName: String = "cipher-key-encryption"
|
||||
override val defaultValue: Boolean = true
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
@@ -222,6 +223,16 @@ sealed class FlagKey<out T : Any> {
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable the checking for Chrome's third party
|
||||
* autofill.
|
||||
*/
|
||||
data object ChromeAutofill : FlagKey<Boolean>() {
|
||||
override val keyName: String = "android-chrome-autofill"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
//region Dummy keys for testing
|
||||
/**
|
||||
* Data object holding the key for a [Boolean] flag to be used in tests.
|
||||
|
||||
@@ -16,30 +16,44 @@ sealed class ImportPrivateKeyResult {
|
||||
* Represents a generic error during the import process.
|
||||
*/
|
||||
sealed class Error : ImportPrivateKeyResult() {
|
||||
/**
|
||||
* The underlying error.
|
||||
*/
|
||||
abstract val throwable: Throwable?
|
||||
|
||||
/**
|
||||
* Indicates that the provided key is unrecoverable or the password is incorrect.
|
||||
*/
|
||||
data object UnrecoverableKey : Error()
|
||||
data class UnrecoverableKey(
|
||||
override val throwable: Throwable,
|
||||
) : Error()
|
||||
|
||||
/**
|
||||
* Indicates that the certificate chain associated with the key is invalid.
|
||||
*/
|
||||
data object InvalidCertificateChain : Error()
|
||||
data class InvalidCertificateChain(
|
||||
override val throwable: Throwable,
|
||||
) : Error()
|
||||
|
||||
/**
|
||||
* Indicates that the specified alias is already in use.
|
||||
*/
|
||||
data object DuplicateAlias : Error()
|
||||
data object DuplicateAlias : Error() {
|
||||
override val throwable: Throwable? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred during the key store operation.
|
||||
*/
|
||||
data object KeyStoreOperationFailed : Error()
|
||||
data class KeyStoreOperationFailed(
|
||||
override val throwable: Throwable,
|
||||
) : Error()
|
||||
|
||||
/**
|
||||
* Indicates the provided key is not supported.
|
||||
*/
|
||||
data object UnsupportedKey : Error()
|
||||
data class UnsupportedKey(
|
||||
override val throwable: Throwable,
|
||||
) : Error()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,7 @@ import java.time.ZonedDateTime
|
||||
* Note: The data we receive is not always reliable, so everything is nullable and we validate the
|
||||
* data in the [PushManager] as necessary.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
sealed class NotificationPayload {
|
||||
/**
|
||||
@@ -24,12 +26,12 @@ sealed class NotificationPayload {
|
||||
*/
|
||||
@Serializable
|
||||
data class SyncCipherNotification(
|
||||
@SerialName("Id") val cipherId: String?,
|
||||
@SerialName("UserId") override val userId: String?,
|
||||
@SerialName("OrganizationId") val organizationId: String?,
|
||||
@SerialName("CollectionIds") val collectionIds: List<String>?,
|
||||
@JsonNames("Id", "id") val cipherId: String?,
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@JsonNames("OrganizationId", "organizationId") val organizationId: String?,
|
||||
@JsonNames("CollectionIds", "collectionIds") val collectionIds: List<String>?,
|
||||
@Contextual
|
||||
@SerialName("RevisionDate") val revisionDate: ZonedDateTime?,
|
||||
@JsonNames("RevisionDate", "revisionDate") val revisionDate: ZonedDateTime?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
@@ -37,10 +39,10 @@ sealed class NotificationPayload {
|
||||
*/
|
||||
@Serializable
|
||||
data class SyncFolderNotification(
|
||||
@SerialName("Id") val folderId: String?,
|
||||
@SerialName("UserId") override val userId: String?,
|
||||
@JsonNames("Id", "id") val folderId: String?,
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@Contextual
|
||||
@SerialName("RevisionDate") val revisionDate: ZonedDateTime?,
|
||||
@JsonNames("RevisionDate", "revisionDate") val revisionDate: ZonedDateTime?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
@@ -48,9 +50,9 @@ sealed class NotificationPayload {
|
||||
*/
|
||||
@Serializable
|
||||
data class UserNotification(
|
||||
@SerialName("UserId") override val userId: String?,
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@Contextual
|
||||
@SerialName("Date") val date: ZonedDateTime?,
|
||||
@JsonNames("Date", "date") val date: ZonedDateTime?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
@@ -58,10 +60,10 @@ sealed class NotificationPayload {
|
||||
*/
|
||||
@Serializable
|
||||
data class SyncSendNotification(
|
||||
@SerialName("Id") val sendId: String?,
|
||||
@SerialName("UserId") override val userId: String?,
|
||||
@JsonNames("Id", "id") val sendId: String?,
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@Contextual
|
||||
@SerialName("RevisionDate") val revisionDate: ZonedDateTime?,
|
||||
@JsonNames("RevisionDate", "revisionDate") val revisionDate: ZonedDateTime?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
@@ -69,7 +71,7 @@ sealed class NotificationPayload {
|
||||
*/
|
||||
@Serializable
|
||||
data class PasswordlessRequestNotification(
|
||||
@SerialName("UserId") override val userId: String?,
|
||||
@SerialName("Id") val loginRequestId: String?,
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@JsonNames("Id", "id") val loginRequestId: String?,
|
||||
) : NotificationPayload()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.sanitizeTotpUri
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -71,9 +72,9 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
|
||||
when (unlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.BiometricDecodingError,
|
||||
VaultUnlockResult.GenericError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
@@ -96,18 +97,21 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
val totpUris = vaultDiskSource
|
||||
.getCiphers(userId)
|
||||
.first()
|
||||
// Filter out any ciphers without a totp item and also deleted ciphers:
|
||||
// Filter out any ciphers without a totp item and also deleted ciphers
|
||||
.filter { it.login?.totp != null && it.deletedDate == null }
|
||||
.mapNotNull {
|
||||
// Decrypt each cipher and take just totp codes:
|
||||
vaultSdkSource
|
||||
val decryptedCipher = vaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = it.toEncryptedSdkCipher(),
|
||||
)
|
||||
.getOrNull()
|
||||
?.login
|
||||
?.totp
|
||||
|
||||
val rawTotp = decryptedCipher?.login?.totp
|
||||
val cipherName = decryptedCipher?.name
|
||||
val username = decryptedCipher?.login?.username
|
||||
|
||||
rawTotp.sanitizeTotpUri(cipherName, username)
|
||||
}
|
||||
|
||||
// Lock the user's vault if we unlocked it for this operation:
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
@@ -380,12 +381,13 @@ class SettingsRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun getUserFingerprint(): UserFingerprintResult {
|
||||
val userId = activeUserId ?: return UserFingerprintResult.Error
|
||||
val userId = activeUserId
|
||||
?: return UserFingerprintResult.Error(error = NoActiveUserException())
|
||||
|
||||
return vaultSdkSource
|
||||
.getUserFingerprint(userId)
|
||||
.fold(
|
||||
onFailure = { UserFingerprintResult.Error },
|
||||
onFailure = { UserFingerprintResult.Error(error = it) },
|
||||
onSuccess = { UserFingerprintResult.Success(it) },
|
||||
)
|
||||
}
|
||||
@@ -492,7 +494,8 @@ class SettingsRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun setupBiometricsKey(cipher: Cipher): BiometricsKeyResult {
|
||||
val userId = activeUserId ?: return BiometricsKeyResult.Error
|
||||
val userId = activeUserId
|
||||
?: return BiometricsKeyResult.Error(error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.getUserEncryptionKey(userId = userId)
|
||||
.onSuccess { biometricsKey ->
|
||||
@@ -506,7 +509,7 @@ class SettingsRepositoryImpl(
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { BiometricsKeyResult.Success },
|
||||
onFailure = { BiometricsKeyResult.Error },
|
||||
onFailure = { BiometricsKeyResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class BiometricsKeyResult {
|
||||
/**
|
||||
* Generic error while setting up the biometrics key.
|
||||
*/
|
||||
data object Error : BiometricsKeyResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : BiometricsKeyResult()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository.util
|
||||
|
||||
import java.net.URLEncoder
|
||||
|
||||
private const val OTPAUTH_PREFIX = "otpauth://totp/"
|
||||
private const val STEAM_PREFIX = "steam://"
|
||||
|
||||
/**
|
||||
* Utility for ensuring that a given TOTP string is a properly formatted otpauth:// or steam:// URI.
|
||||
* If the input TOTP is already a valid URI, it is returned as-is.
|
||||
* If the TOTP is manually entered and does not follow the URI format,
|
||||
* this function reconstructs it using the provided issuer and username.
|
||||
*
|
||||
* Uses this as a guide for format
|
||||
* https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
*
|
||||
* Replace spaces (+) with %20, and encode the label and issuer (per the above link)
|
||||
* https://datatracker.ietf.org/doc/html/rfc5234
|
||||
* */
|
||||
fun String?.sanitizeTotpUri(
|
||||
issuer: String?,
|
||||
username: String?,
|
||||
): String? {
|
||||
if (this.isNullOrBlank()) return null
|
||||
|
||||
return if (this.startsWith(OTPAUTH_PREFIX) || this.startsWith(STEAM_PREFIX)) {
|
||||
// ✅ Already a valid TOTP or Steam URI, return as-is.
|
||||
this
|
||||
} else {
|
||||
// ❌ Manually entered secret, reconstruct as otpauth://totp/ URI.
|
||||
|
||||
// Trim spaces from issuer and username
|
||||
val trimmedIssuer = issuer
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
val trimmedUsername = username
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
// Determine raw label correctly (avoid empty `:` issue)
|
||||
val rawLabel = if (trimmedIssuer != null && trimmedUsername != null) {
|
||||
"$trimmedIssuer:$trimmedUsername"
|
||||
} else {
|
||||
trimmedUsername
|
||||
}
|
||||
|
||||
// Encode label only if it's not empty
|
||||
val encodedLabel = rawLabel
|
||||
?.let {
|
||||
URLEncoder
|
||||
.encode(it, "UTF-8")
|
||||
.replace("+", "%20")
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
// Encode issuer separately for the query parameter
|
||||
val encodedIssuer = trimmedIssuer?.let {
|
||||
URLEncoder
|
||||
.encode(it, "UTF-8")
|
||||
.replace("+", "%20")
|
||||
}
|
||||
|
||||
// Construct the issuer query parameter.
|
||||
val issuerParameter = encodedIssuer
|
||||
?.let { "&issuer=$it" }
|
||||
.orEmpty()
|
||||
|
||||
// Remove spaces from the manually entered secret
|
||||
val sanitizedSecret = this.filterNot { it.isWhitespace() }
|
||||
|
||||
// Construct final TOTP URI
|
||||
"$OTPAUTH_PREFIX$encodedLabel?secret=$sanitizedSecret$issuerParameter"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import android.os.Build
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
|
||||
/**
|
||||
@@ -7,3 +8,55 @@ import com.x8bit.bitwarden.BuildConfig
|
||||
*/
|
||||
val isFdroid: Boolean
|
||||
get() = BuildConfig.FLAVOR == "fdroid"
|
||||
|
||||
/**
|
||||
* A string that represents a displayable app version.
|
||||
*/
|
||||
val versionData: String
|
||||
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
|
||||
|
||||
/**
|
||||
* A string that represents device data.
|
||||
*/
|
||||
val deviceData: String get() = "$deviceBrandModel $osInfo $buildInfo"
|
||||
|
||||
/**
|
||||
* A string representing the CI information if available.
|
||||
*/
|
||||
val ciBuildInfo: String? get() = BuildConfig.CI_INFO.takeUnless { it.isBlank() }
|
||||
|
||||
/**
|
||||
* A string representing the build flavor or blank if it is the standard configuration.
|
||||
*/
|
||||
private val buildFlavorName: String
|
||||
get() = when (BuildConfig.FLAVOR) {
|
||||
"standard" -> ""
|
||||
else -> "-${BuildConfig.FLAVOR}"
|
||||
}
|
||||
|
||||
/**
|
||||
* A string representing the build type.
|
||||
*/
|
||||
private val buildTypeName: String
|
||||
get() = when (BuildConfig.BUILD_TYPE) {
|
||||
"debug" -> "dev"
|
||||
"release" -> "prod"
|
||||
else -> BuildConfig.BUILD_TYPE
|
||||
}
|
||||
|
||||
/**
|
||||
* A string representing the device brand and model.
|
||||
*/
|
||||
private val deviceBrandModel: String get() = "\uD83D\uDCF1 ${Build.BRAND} ${Build.MODEL}"
|
||||
|
||||
/**
|
||||
* A string representing the operating system information.
|
||||
*/
|
||||
private val osInfo: String get() = "\uD83E\uDD16 ${Build.VERSION.RELEASE}@${Build.VERSION.SDK_INT}"
|
||||
|
||||
/**
|
||||
* A string representing the build information.
|
||||
*/
|
||||
private val buildInfo: String
|
||||
get() = "\uD83D\uDCE6 $buildTypeName" +
|
||||
buildFlavorName.takeUnless { it.isBlank() }?.let { " $it" }.orEmpty()
|
||||
|
||||
@@ -130,7 +130,7 @@ class GeneratorRepositoryImpl(
|
||||
}
|
||||
GeneratedPasswordResult.Success(generatedPassword)
|
||||
},
|
||||
onFailure = { GeneratedPasswordResult.InvalidRequest },
|
||||
onFailure = { GeneratedPasswordResult.InvalidRequest(error = it) },
|
||||
)
|
||||
|
||||
override suspend fun generatePassphrase(
|
||||
@@ -149,7 +149,7 @@ class GeneratorRepositoryImpl(
|
||||
}
|
||||
GeneratedPassphraseResult.Success(generatedPassphrase)
|
||||
},
|
||||
onFailure = { GeneratedPassphraseResult.InvalidRequest },
|
||||
onFailure = { GeneratedPassphraseResult.InvalidRequest(error = it) },
|
||||
)
|
||||
|
||||
override suspend fun generatePlusAddressedEmail(
|
||||
@@ -161,7 +161,7 @@ class GeneratorRepositoryImpl(
|
||||
GeneratedPlusAddressedUsernameResult.Success(generatedEmail)
|
||||
},
|
||||
onFailure = {
|
||||
GeneratedPlusAddressedUsernameResult.InvalidRequest
|
||||
GeneratedPlusAddressedUsernameResult.InvalidRequest(error = it)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -174,7 +174,7 @@ class GeneratorRepositoryImpl(
|
||||
GeneratedCatchAllUsernameResult.Success(generatedEmail)
|
||||
},
|
||||
onFailure = {
|
||||
GeneratedCatchAllUsernameResult.InvalidRequest
|
||||
GeneratedCatchAllUsernameResult.InvalidRequest(error = it)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -187,7 +187,7 @@ class GeneratorRepositoryImpl(
|
||||
GeneratedRandomWordUsernameResult.Success(generatedUsername)
|
||||
},
|
||||
onFailure = {
|
||||
GeneratedRandomWordUsernameResult.InvalidRequest
|
||||
GeneratedRandomWordUsernameResult.InvalidRequest(error = it)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -200,7 +200,7 @@ class GeneratorRepositoryImpl(
|
||||
GeneratedForwardedServiceUsernameResult.Success(generatedEmail)
|
||||
},
|
||||
onFailure = {
|
||||
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message)
|
||||
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message, error = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ sealed class GeneratedCatchAllUsernameResult {
|
||||
/**
|
||||
* There was an error during the operation.
|
||||
*/
|
||||
data object InvalidRequest : GeneratedCatchAllUsernameResult()
|
||||
data class InvalidRequest(val error: Throwable) : GeneratedCatchAllUsernameResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,8 @@ sealed class GeneratedForwardedServiceUsernameResult {
|
||||
/**
|
||||
* There was an error during the operation.
|
||||
*/
|
||||
data class InvalidRequest(val message: String?) : GeneratedForwardedServiceUsernameResult()
|
||||
data class InvalidRequest(
|
||||
val message: String?,
|
||||
val error: Throwable,
|
||||
) : GeneratedForwardedServiceUsernameResult()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user